spectoflow 0.17.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,19 +4,19 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>spectoflow · control</title>
7
- <link rel="icon" type="image/png" href="logo-dark.png" />
8
- <link rel="icon" type="image/png" media="(prefers-color-scheme: dark)" href="logo-white.png" />
9
- <link rel="stylesheet" href="styles.css" />
10
- <link rel="stylesheet" href="designs/console.css" />
11
- <link rel="stylesheet" href="designs/orbit.css" />
7
+ <link rel="icon" type="image/png" href="/logo-dark.png" />
8
+ <link rel="icon" type="image/png" media="(prefers-color-scheme: dark)" href="/logo-white.png" />
9
+ <link rel="stylesheet" href="/styles.css" />
10
+ <link rel="stylesheet" href="/designs/console.css" />
11
+ <link rel="stylesheet" href="/designs/orbit.css" />
12
12
  </head>
13
13
  <body>
14
14
  <header class="topbar">
15
15
  <div class="brand">
16
- <button class="nav-toggle" id="navToggle" aria-label="Toggle menu" aria-expanded="false"><span></span><span></span><span></span></button>
16
+ <button class="nav-toggle" id="navToggle" data-i18n-aria="topbar.navToggle" aria-label="Toggle menu" aria-expanded="false"><span></span><span></span><span></span></button>
17
17
  <a class="brand-logo" href="/board" data-route="board" aria-label="spectoflow home">
18
- <img class="brand-logo-img is-dark" src="logo-white.png" alt="spectoflow" />
19
- <img class="brand-logo-img is-light" src="logo-dark.png" alt="spectoflow" />
18
+ <img class="brand-logo-img is-dark" src="/logo-white.png" alt="spectoflow" />
19
+ <img class="brand-logo-img is-light" src="/logo-dark.png" alt="spectoflow" />
20
20
  </a>
21
21
  <div class="brand-text">
22
22
  <div class="brand-line">
@@ -25,27 +25,42 @@
25
25
  <span class="brand-sep">/</span>
26
26
  <span class="brand-project" id="projectName">—</span>
27
27
  </div>
28
- <div class="brand-sub" id="brandSub">semi · en</div>
28
+ <div class="brand-sub" id="brandSub">
29
+ <select id="topMode" class="brand-mini-select" data-i18n-title="topbar.mode.title" data-i18n-aria="field.mode" aria-label="Autonomy mode" title="Autonomy mode — change from here or in Settings">
30
+ <option value="autopilot">autopilot</option>
31
+ <option value="semi">semi</option>
32
+ <option value="manual">manual</option>
33
+ </select>
34
+ <span class="brand-sub-sep">·</span>
35
+ <select id="topLang" class="brand-mini-select" data-i18n-title="topbar.lang.title" data-i18n-aria="field.lang" aria-label="Output language" title="Output language — change from here or in Settings">
36
+ <option value="en">en</option>
37
+ <option value="fr">fr</option>
38
+ <option value="es">es</option>
39
+ <option value="de">de</option>
40
+ <option value="pt">pt</option>
41
+ <option value="it">it</option>
42
+ </select>
43
+ </div>
29
44
  </div>
30
45
  <div class="progress-meter" id="globalMeter" title="Global progress">
31
46
  <div class="progress-meter-fill" id="globalMeterFill"></div>
32
47
  </div>
33
48
  </div>
34
49
  <nav class="tabs" id="tabs">
35
- <button class="tab is-active" data-tab="board"><span class="tab-ico" data-icon="board"></span><span class="tab-label">Board</span></button>
36
- <button class="tab" data-tab="requests"><span class="tab-ico" data-icon="requests"></span><span class="tab-label">Requests</span></button>
37
- <button class="tab" data-tab="attention"><span class="tab-ico" data-icon="attention"></span><span class="tab-label">Attention</span><span class="tab-badge" id="attnBadge" hidden>0</span></button>
38
- <button class="tab" data-tab="backlog"><span class="tab-ico" data-icon="backlog"></span><span class="tab-label">Backlog</span></button>
39
- <button class="tab" data-tab="workflow"><span class="tab-ico" data-icon="workflow"></span><span class="tab-label">Workflow</span></button>
40
- <button class="tab" data-tab="team"><span class="tab-ico" data-icon="agents"></span><span class="tab-label">Agents &amp; Skills</span></button>
41
- <button class="tab" data-tab="chat"><span class="tab-ico" data-icon="chat"></span><span class="tab-label">Chat</span></button>
42
- <button class="tab" data-tab="info"><span class="tab-ico" data-icon="info"></span><span class="tab-label">Info</span></button>
43
- <button class="tab" data-tab="settings"><span class="tab-ico" data-icon="settings"></span><span class="tab-label">Settings</span></button>
50
+ <button class="tab is-active" data-tab="board"><span class="tab-ico" data-icon="board"></span><span class="tab-label" data-i18n="nav.board">Board</span></button>
51
+ <button class="tab" data-tab="chat"><span class="tab-ico" data-icon="chat"></span><span class="tab-label" data-i18n="nav.chat">Chat</span></button>
52
+ <button class="tab" data-tab="requests"><span class="tab-ico" data-icon="requests"></span><span class="tab-label" data-i18n="nav.requests">Requests</span></button>
53
+ <button class="tab" data-tab="attention"><span class="tab-ico" data-icon="attention"></span><span class="tab-label" data-i18n="nav.attention">Attention</span><span class="tab-badge" id="attnBadge" hidden>0</span></button>
54
+ <button class="tab" data-tab="backlog"><span class="tab-ico" data-icon="backlog"></span><span class="tab-label" data-i18n="nav.backlog">Backlog</span></button>
55
+ <button class="tab" data-tab="workflow"><span class="tab-ico" data-icon="workflow"></span><span class="tab-label" data-i18n="nav.workflow">Workflow</span></button>
56
+ <button class="tab" data-tab="team"><span class="tab-ico" data-icon="agents"></span><span class="tab-label" data-i18n="nav.team">Agents &amp; Skills</span></button>
57
+ <button class="tab" data-tab="info"><span class="tab-ico" data-icon="info"></span><span class="tab-label" data-i18n="nav.info">Info</span></button>
58
+ <button class="tab" data-tab="settings"><span class="tab-ico" data-icon="settings"></span><span class="tab-label" data-i18n="nav.settings">Settings</span></button>
44
59
  </nav>
45
60
  <div class="top-right">
46
61
  <span class="sync" id="sync"><span class="sync-dot"></span><span id="syncLabel">live</span></span>
47
- <button class="btn primary run-btn" id="runQuickBtn" title="Open the run chat"><span class="tab-ico" data-icon="run"></span><span>Run</span></button>
48
- <button class="theme-toggle" id="themeToggle" aria-label="Toggle light / dark theme" title="Toggle light / dark"><span class="tab-ico" data-icon="theme"></span></button>
62
+ <button class="btn primary run-btn" id="runQuickBtn" data-i18n-title="topbar.run.title" title="Open the run chat"><span class="tab-ico" data-icon="run"></span><span data-i18n="action.run">Run</span></button>
63
+ <button class="theme-toggle" id="themeToggle" data-i18n-aria="topbar.theme.toggle" data-i18n-title="topbar.theme.toggle.title" aria-label="Toggle light / dark theme" title="Toggle light / dark"><span class="tab-ico" data-icon="theme"></span></button>
49
64
  </div>
50
65
  </header>
51
66
 
@@ -56,34 +71,35 @@
56
71
  <div class="overview" id="overview"></div>
57
72
  <div class="filters" id="filters">
58
73
  <div class="chip-list" id="statusChips">
59
- <button class="fchip active" data-status="all">All</button>
60
- <button class="fchip" data-status="todo">To do</button>
61
- <button class="fchip" data-status="in_progress">In progress</button>
62
- <button class="fchip" data-status="to_validate">To validate</button>
63
- <button class="fchip" data-status="to_analyze">To analyze</button>
64
- <button class="fchip" data-status="done">Done</button>
65
- <button class="fchip" data-status="blocked">Blocked</button>
74
+ <button class="fchip active" data-status="all" data-i18n="filter.all">All</button>
75
+ <button class="fchip" data-status="todo" data-i18n="status.todo">To do</button>
76
+ <button class="fchip" data-status="in_progress" data-i18n="status.in_progress">In progress</button>
77
+ <button class="fchip" data-status="to_validate" data-i18n="status.to_validate">To validate</button>
78
+ <button class="fchip" data-status="to_analyze" data-i18n="status.to_analyze">To analyze</button>
79
+ <button class="fchip" data-status="done" data-i18n="status.done">Done</button>
80
+ <button class="fchip" data-status="blocked" data-i18n="status.blocked">Blocked</button>
66
81
  </div>
67
82
  <div class="view-toggle" id="boardViewToggle" role="group" aria-label="Board view">
68
- <button class="vt-btn active" data-view="list" title="Grouped by phase"><span class="tab-ico" data-icon="backlog"></span><span>List</span></button>
69
- <button class="vt-btn" data-view="kanban" title="Columns by status"><span class="tab-ico" data-icon="board"></span><span>Kanban</span></button>
83
+ <button class="vt-btn active" data-view="list" data-i18n-title="board.viewList.title" title="Grouped by phase"><span class="tab-ico" data-icon="backlog"></span><span data-i18n="board.viewList">List</span></button>
84
+ <button class="vt-btn" data-view="kanban" data-i18n-title="board.viewKanban.title" title="Columns by status"><span class="tab-ico" data-icon="board"></span><span data-i18n="board.viewKanban">Kanban</span></button>
70
85
  </div>
71
- <button class="mini-btn" id="phaseToggleAll" title="Expand or collapse all phases">Expand all</button>
72
- <input type="search" id="search" class="search" placeholder="Filter tasks…" autocomplete="off" />
86
+ <button class="mini-btn" id="phaseToggleAll" data-i18n-title="board.expandAllTitle" data-i18n="board.expandAll" title="Expand or collapse all phases">Expand all</button>
87
+ <input type="search" id="search" class="search" data-i18n-ph="board.filterPlaceholder" placeholder="Filter tasks…" autocomplete="off" />
73
88
  </div>
74
89
  <div class="board" id="board"></div>
75
90
  </div>
76
91
  <aside class="side" id="side">
77
92
  <div class="rail-block rail-block-journal">
78
- <h2 class="rail-title">Journal <span class="count" id="journalCount">0</span></h2>
93
+ <h2 class="rail-title"><span data-i18n="rail.journal">Journal</span> <span class="count" id="journalCount">0</span></h2>
79
94
  <div class="journal" id="journal"></div>
95
+ <button class="mini-btn journal-more" id="journalMore" hidden></button>
80
96
  </div>
81
97
  <div class="rail-block">
82
- <h2 class="rail-title">Specs <span class="count" id="specsCount">0</span></h2>
98
+ <h2 class="rail-title"><span data-i18n="rail.specs">Specs</span> <span class="count" id="specsCount">0</span></h2>
83
99
  <ul class="flatlist" id="specs"></ul>
84
100
  </div>
85
101
  <div class="rail-block">
86
- <h2 class="rail-title">Running <span class="count" id="runCount">0</span></h2>
102
+ <h2 class="rail-title"><span data-i18n="rail.running">Running</span> <span class="count" id="runCount">0</span></h2>
87
103
  <ul class="flatlist" id="running"></ul>
88
104
  </div>
89
105
  </aside>
@@ -92,8 +108,8 @@
92
108
  <!-- REQUESTS (tasks awaiting review: to_validate / to_analyze) -->
93
109
  <section class="panel" data-panel="requests">
94
110
  <div class="requests-wrap">
95
- <h2 class="panel-title">Requests <span class="count" id="requestsCount">0</span></h2>
96
- <p class="panel-sub">Tasks awaiting your input — status <em>to validate</em> or <em>to analyze</em>.</p>
111
+ <h2 class="panel-title"><span data-i18n="nav.requests">Requests</span> <span class="count" id="requestsCount">0</span></h2>
112
+ <p class="panel-sub" data-i18n-html="requests.sub">Tasks awaiting your input — status <em>to validate</em> or <em>to analyze</em>.</p>
97
113
  <ul class="flatlist requests-list" id="requestsList"></ul>
98
114
  </div>
99
115
  </section>
@@ -101,16 +117,16 @@
101
117
  <!-- ATTENTION (points raised by the agent or by you; validate → task) -->
102
118
  <section class="panel" data-panel="attention">
103
119
  <div class="attn-wrap">
104
- <h2 class="panel-title">Points of attention <span class="count" id="attnCount">0</span></h2>
105
- <p class="panel-sub">Things the agent flagged during work — or you noted — that deserve a look. <em>Validate</em> one to turn it into a task.</p>
120
+ <h2 class="panel-title"><span data-i18n="attn.title">Points of attention</span> <span class="count" id="attnCount">0</span></h2>
121
+ <p class="panel-sub" data-i18n-html="attn.sub">Things the agent flagged during work — or you noted — that deserve a look. <em>Validate</em> one to turn it into a task.</p>
106
122
  <div class="attn-add">
107
- <textarea id="attnInput" class="chat-ta" placeholder="Note something to keep an eye on…"></textarea>
108
- <button id="attnAddBtn" class="btn primary">Add note</button>
123
+ <textarea id="attnInput" class="chat-ta" data-i18n-ph="attn.inputPlaceholder" placeholder="Note something to keep an eye on…"></textarea>
124
+ <button id="attnAddBtn" class="btn primary" data-i18n="attn.addNote">Add note</button>
109
125
  </div>
110
126
  <div class="attn-filters">
111
- <button class="fchip active" data-attn="open">Open</button>
112
- <button class="fchip" data-attn="all">All</button>
113
- <button class="fchip" data-attn="resolved">Resolved</button>
127
+ <button class="fchip active" data-attn="open" data-i18n="filter.open">Open</button>
128
+ <button class="fchip" data-attn="all" data-i18n="filter.all">All</button>
129
+ <button class="fchip" data-attn="resolved" data-i18n="filter.resolved">Resolved</button>
114
130
  </div>
115
131
  <div class="attn-list" id="attnList"></div>
116
132
  </div>
@@ -119,31 +135,31 @@
119
135
  <!-- BACKLOG (flat, sortable, filterable table of every task) -->
120
136
  <section class="panel" data-panel="backlog">
121
137
  <div class="backlog-wrap">
122
- <h2 class="panel-title">Backlog <span class="count" id="backlogCount">0</span></h2>
123
- <p class="panel-sub">Every task, across every plan, in one flat table.</p>
138
+ <h2 class="panel-title"><span data-i18n="nav.backlog">Backlog</span> <span class="count" id="backlogCount">0</span></h2>
139
+ <p class="panel-sub" data-i18n="backlog.sub">Every task, across every plan, in one flat table.</p>
124
140
  <div class="filters" id="backlogFilters">
125
141
  <div class="chip-list" id="backlogStatusChips">
126
- <button class="fchip active" data-status="open">Open</button>
127
- <button class="fchip" data-status="all">All</button>
128
- <button class="fchip" data-status="todo">To do</button>
129
- <button class="fchip" data-status="in_progress">In progress</button>
130
- <button class="fchip" data-status="to_validate">To validate</button>
131
- <button class="fchip" data-status="to_analyze">To analyze</button>
132
- <button class="fchip" data-status="done">Done</button>
133
- <button class="fchip" data-status="blocked">Blocked</button>
142
+ <button class="fchip active" data-status="open" data-i18n="filter.open">Open</button>
143
+ <button class="fchip" data-status="all" data-i18n="filter.all">All</button>
144
+ <button class="fchip" data-status="todo" data-i18n="status.todo">To do</button>
145
+ <button class="fchip" data-status="in_progress" data-i18n="status.in_progress">In progress</button>
146
+ <button class="fchip" data-status="to_validate" data-i18n="status.to_validate">To validate</button>
147
+ <button class="fchip" data-status="to_analyze" data-i18n="status.to_analyze">To analyze</button>
148
+ <button class="fchip" data-status="done" data-i18n="status.done">Done</button>
149
+ <button class="fchip" data-status="blocked" data-i18n="status.blocked">Blocked</button>
134
150
  </div>
135
- <input type="search" id="backlogSearch" class="search" placeholder="Filter by id or title…" autocomplete="off" />
151
+ <input type="search" id="backlogSearch" class="search" data-i18n-ph="backlog.searchPlaceholder" placeholder="Filter by id or title…" autocomplete="off" />
136
152
  </div>
137
153
  <div class="table-scroll">
138
154
  <table class="backlog-table" id="backlogTable">
139
155
  <thead>
140
156
  <tr>
141
- <th data-col="id">ID</th>
142
- <th data-col="title">Title</th>
143
- <th data-col="phase">Phase</th>
144
- <th data-col="status">Status</th>
145
- <th data-col="owner">Owner</th>
146
- <th data-col="level">Level</th>
157
+ <th data-col="id" data-i18n="col.id">ID</th>
158
+ <th data-col="title" data-i18n="col.title">Title</th>
159
+ <th data-col="phase" data-i18n="col.phase">Phase</th>
160
+ <th data-col="status" data-i18n="col.status">Status</th>
161
+ <th data-col="owner" data-i18n="col.owner">Owner</th>
162
+ <th data-col="level" data-i18n="col.level">Level</th>
147
163
  <th data-col="comments">💬</th>
148
164
  </tr>
149
165
  </thead>
@@ -157,8 +173,8 @@
157
173
  <!-- WORKFLOW -->
158
174
  <section class="panel" data-panel="workflow">
159
175
  <div class="wf-wrap">
160
- <h2 class="panel-title">Active workflow</h2>
161
- <p class="panel-sub">The pipeline your requests flow through — from idea to shipped, spec-driven. <strong>Click a step</strong> to see what it does and turn it on or off (this edits <code>.spectoflow/workflow.md</code>). Enabled steps run in order; the orchestrator gates each one by your <em>mode</em> and <em>policy</em>. Optional steps stay off until you switch them on.</p>
176
+ <h2 class="panel-title" data-i18n="workflow.title">Active workflow</h2>
177
+ <p class="panel-sub" data-i18n-html="workflow.sub">The pipeline your requests flow through — from idea to shipped, spec-driven. <strong>Click a step</strong> to see what it does and turn it on or off (this edits <code>.spectoflow/workflow.md</code>). Enabled steps run in order; the orchestrator gates each one by your <em>mode</em> and <em>policy</em>. Optional steps stay off until you switch them on.</p>
162
178
  <div class="wf-diagram" id="wfDiagram"></div>
163
179
  </div>
164
180
  </section>
@@ -166,11 +182,11 @@
166
182
  <!-- AGENTS & SKILLS -->
167
183
  <section class="panel" data-panel="team">
168
184
  <div class="team-wrap">
169
- <h2 class="panel-title">Agents <span class="count" id="agentsCount">0</span></h2>
170
- <p class="panel-sub">Stable team personas — the <em>who</em>.</p>
185
+ <h2 class="panel-title"><span data-i18n="team.agentsTitle">Agents</span> <span class="count" id="agentsCount">0</span></h2>
186
+ <p class="panel-sub" data-i18n-html="team.agentsSub">Stable team personas — the <em>who</em>.</p>
171
187
  <div class="cards" id="agents"></div>
172
- <h2 class="panel-title" style="margin-top:28px">Skills <span class="count" id="skillsCount">0</span></h2>
173
- <p class="panel-sub">Evolving procedures — the <em>how</em>.</p>
188
+ <h2 class="panel-title" style="margin-top:28px"><span data-i18n="team.skillsTitle">Skills</span> <span class="count" id="skillsCount">0</span></h2>
189
+ <p class="panel-sub" data-i18n-html="team.skillsSub">Evolving procedures — the <em>how</em>.</p>
174
190
  <div class="cards" id="skills"></div>
175
191
  </div>
176
192
  </section>
@@ -180,31 +196,31 @@
180
196
  <div class="chat-tab-wrap">
181
197
  <div class="chat-tab-head">
182
198
  <div>
183
- <h2 class="panel-title">Chat</h2>
184
- <p class="panel-sub">Full conversation with the runner — the same run as the widget, more room to read it.</p>
199
+ <h2 class="panel-title" data-i18n="nav.chat">Chat</h2>
200
+ <p class="panel-sub" data-i18n="chat.tabSub">Full conversation with the runner — the same run as the widget, more room to read it.</p>
185
201
  </div>
186
202
  <select id="tabRunAgent" class="chat-agent" title="Agent"></select>
187
203
  </div>
188
204
  <div class="chat-tab-log" id="chatTabLog">
189
- <div class="chat-idle">Type a request — the agent runs headless in this project with full memory
205
+ <div class="chat-idle" data-i18n-html="chat.idle">Type a request — the agent runs headless in this project with full memory
190
206
  (<code>CLAUDE.md → AGENTS.md</code>) and updates the board live.</div>
191
207
  </div>
192
208
  <div class="chat-tab-input">
193
- <textarea id="tabRunPrompt" class="chat-ta" placeholder="e.g. Add a login feature with email + password"></textarea>
209
+ <textarea id="tabRunPrompt" class="chat-ta" data-i18n-ph="chat.inputPlaceholder" placeholder="e.g. Add a login feature with email + password"></textarea>
194
210
  <div class="chat-tab-actions">
195
- <button id="tabRunBtn" class="btn primary chat-send">Send</button>
196
- <button id="tabOrchBtn" class="btn chat-send" title="Walk the enabled workflow">Orchestrate</button>
211
+ <button id="tabRunBtn" class="btn primary chat-send" data-i18n="action.send">Send</button>
212
+ <button id="tabOrchBtn" class="btn chat-send" data-i18n-title="chat.orchestrateTitle" data-i18n="action.orchestrate" title="Walk the enabled workflow">Orchestrate</button>
197
213
  </div>
198
214
  </div>
199
- <p class="chat-warn">⚠ Launches a real agent (<code>config.json → runners</code>) that can modify files &amp; run commands.</p>
215
+ <p class="chat-warn" data-i18n-html="chat.warn">⚠ Launches a real agent (<code>config.json → runners</code>) that can modify files &amp; run commands.</p>
200
216
  </div>
201
217
  </section>
202
218
 
203
219
  <!-- INFO (project overview — filled by renderInfo() in app.js) -->
204
220
  <section class="panel" data-panel="info">
205
221
  <div class="info-wrap">
206
- <h2 class="panel-title">Info</h2>
207
- <p class="panel-sub">Project overview — configuration, counts, specs and active workflow.</p>
222
+ <h2 class="panel-title" data-i18n="info.title">Info</h2>
223
+ <p class="panel-sub" data-i18n="info.sub">Project overview — configuration, counts, specs and active workflow.</p>
208
224
  <div class="info-grid" id="infoGrid"></div>
209
225
  </div>
210
226
  </section>
@@ -212,23 +228,23 @@
212
228
  <!-- SETTINGS (change autonomy mode + output language → config.json) -->
213
229
  <section class="panel" data-panel="settings">
214
230
  <div class="settings-wrap">
215
- <h2 class="panel-title">Settings</h2>
216
- <p class="panel-sub">Change how spectoflow runs. Writes <code>.spectoflow/config.json</code> — the agent picks it up on its next run.</p>
231
+ <h2 class="panel-title" data-i18n="nav.settings">Settings</h2>
232
+ <p class="panel-sub" data-i18n-html="settings.sub">Change how spectoflow runs. Writes <code>.spectoflow/config.json</code> — the agent picks it up on its next run.</p>
217
233
  <div class="settings-card">
218
234
  <label class="settings-field">
219
- <span class="settings-field-label">Dashboard design</span>
235
+ <span class="settings-field-label" data-i18n="field.design">Dashboard design</span>
220
236
  <select id="setDesign" class="settings-select"></select>
221
237
  </label>
222
238
  <label class="settings-field">
223
- <span class="settings-field-label">Autonomy mode</span>
239
+ <span class="settings-field-label" data-i18n="field.mode">Autonomy mode</span>
224
240
  <select id="setMode" class="settings-select">
225
- <option value="autopilot">autopilot — run freely, ask only on policy gates</option>
226
- <option value="semi">semi — autopilot + policy approvals</option>
227
- <option value="manual">manual — approve every step</option>
241
+ <option value="autopilot" data-i18n="mode.autopilot.desc">autopilot — run freely, ask only on policy gates</option>
242
+ <option value="semi" data-i18n="mode.semi.desc">semi — autopilot + policy approvals</option>
243
+ <option value="manual" data-i18n="mode.manual.desc">manual — approve every step</option>
228
244
  </select>
229
245
  </label>
230
246
  <label class="settings-field">
231
- <span class="settings-field-label">Output language</span>
247
+ <span class="settings-field-label" data-i18n="field.lang">Output language</span>
232
248
  <select id="setLang" class="settings-select">
233
249
  <option value="en">en · English</option>
234
250
  <option value="fr">fr · Français</option>
@@ -238,9 +254,17 @@
238
254
  <option value="it">it · Italiano</option>
239
255
  </select>
240
256
  </label>
241
- <div class="settings-saved" id="settingsSaved" hidden>✓ saved</div>
257
+ <div class="settings-saved" id="settingsSaved" data-i18n="settings.saved" hidden>✓ saved</div>
242
258
  </div>
243
259
  <div class="settings-readonly" id="settingsReadonly"></div>
260
+
261
+ <!-- Customize: project-specific dashboards, skills and agents — described or auto-proposed,
262
+ generated by the configured agent through the same Run/Chat pipeline as any other ask. -->
263
+ <div class="cz-wrap">
264
+ <h2 class="panel-title" data-i18n="customize.title">Customize</h2>
265
+ <p class="panel-sub" data-i18n="customize.sub">Add project-specific dashboards, skills and agents — describe what you want, or let Auto propose candidates from this project.</p>
266
+ <div id="czRoot"></div>
267
+ </div>
244
268
  </div>
245
269
  </section>
246
270
  </main>
@@ -248,8 +272,8 @@
248
272
  <footer class="app-footer">
249
273
  <div class="footer-left">
250
274
  <a class="footer-logo" href="/board" data-route="board" aria-label="spectoflow">
251
- <img class="brand-logo-img is-dark" src="logo-white.png" alt="spectoflow" />
252
- <img class="brand-logo-img is-light" src="logo-dark.png" alt="spectoflow" />
275
+ <img class="brand-logo-img is-dark" src="/logo-white.png" alt="spectoflow" />
276
+ <img class="brand-logo-img is-light" src="/logo-dark.png" alt="spectoflow" />
253
277
  </a>
254
278
  <span class="footer-name">spectoflow</span>
255
279
  <span class="footer-ver" id="footerVer"></span>
@@ -259,31 +283,31 @@
259
283
  <div class="footer-right">
260
284
  <a href="https://github.com/georgesmomo/spectoflow" target="_blank" rel="noopener">GitHub</a>
261
285
  <a href="https://www.npmjs.com/package/spectoflow" target="_blank" rel="noopener">npm</a>
262
- <button class="footer-link" id="footerSettings">Settings</button>
286
+ <button class="footer-link" id="footerSettings" data-i18n="nav.settings">Settings</button>
263
287
  </div>
264
288
  </footer>
265
289
 
266
290
  <!-- CHAT WIDGET (floating agent runner; entry point to the group-chat) -->
267
- <button class="chat-fab" id="chatFab" aria-label="Open run chat" title="Run an agent">💬</button>
291
+ <button class="chat-fab" id="chatFab" data-i18n-title="chat.widgetTitle" aria-label="Open run chat" title="Run an agent">💬</button>
268
292
  <div class="chat" id="chat" aria-hidden="true" role="dialog" aria-label="Run an agent">
269
293
  <div class="chat-head">
270
294
  <div class="chat-head-text">
271
- <span class="chat-title">Run an agent</span>
272
- <span class="chat-sub">Quick access · full view in the Chat tab</span>
295
+ <span class="chat-title" data-i18n="chat.widgetTitle">Run an agent</span>
296
+ <span class="chat-sub" data-i18n="chat.widgetSub">Quick access · full view in the Chat tab</span>
273
297
  </div>
274
298
  <select id="runAgent" class="chat-agent" title="Agent"></select>
275
299
  <button class="chat-close" id="chatClose" aria-label="Close">×</button>
276
300
  </div>
277
301
  <div class="chat-log" id="chatLog">
278
- <div class="chat-idle">Type a request — the agent runs headless in this project with full memory
302
+ <div class="chat-idle" data-i18n-html="chat.idle">Type a request — the agent runs headless in this project with full memory
279
303
  (<code>CLAUDE.md → AGENTS.md</code>) and updates the board live.</div>
280
304
  </div>
281
305
  <div class="chat-input">
282
- <textarea id="runPrompt" class="chat-ta" placeholder="e.g. Add a login feature with email + password"></textarea>
283
- <button id="runBtn" class="btn primary chat-send">Send</button>
284
- <button id="orchBtn" class="btn chat-send" title="Walk the enabled workflow">Orchestrate</button>
306
+ <textarea id="runPrompt" class="chat-ta" data-i18n-ph="chat.inputPlaceholder" placeholder="e.g. Add a login feature with email + password"></textarea>
307
+ <button id="runBtn" class="btn primary chat-send" data-i18n="action.send">Send</button>
308
+ <button id="orchBtn" class="btn chat-send" data-i18n-title="chat.orchestrateTitle" data-i18n="action.orchestrate" title="Walk the enabled workflow">Orchestrate</button>
285
309
  </div>
286
- <p class="chat-warn">⚠ Launches a real agent (<code>config.json → runners</code>) that can modify files &amp; run commands.</p>
310
+ <p class="chat-warn" data-i18n-html="chat.warn">⚠ Launches a real agent (<code>config.json → runners</code>) that can modify files &amp; run commands.</p>
287
311
  </div>
288
312
 
289
313
  <div class="drawer" id="drawer" aria-hidden="true">
@@ -307,12 +331,13 @@
307
331
  </defs>
308
332
  </svg>
309
333
 
310
- <script src="stats.js"></script>
311
- <script src="charts.js"></script>
312
- <script src="icons.js"></script>
313
- <script src="designs.js"></script>
314
- <script src="app.js"></script>
315
- <script src="designs/console.js"></script>
316
- <script src="designs/orbit.js"></script>
334
+ <script src="/stats.js"></script>
335
+ <script src="/charts.js"></script>
336
+ <script src="/icons.js"></script>
337
+ <script src="/designs.js"></script>
338
+ <script src="/i18n.js"></script>
339
+ <script src="/app.js"></script>
340
+ <script src="/designs/console.js"></script>
341
+ <script src="/designs/orbit.js"></script>
317
342
  </body>
318
343
  </html>
@@ -26,7 +26,20 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
26
26
  .brand-text { display:flex; flex-direction:column; gap:2px; min-width:0; }
27
27
  .brand-line { display:flex; align-items:center; gap:7px; min-width:0; }
28
28
  .brand-name { font-weight:700; } .brand-sep{ color:var(--faint); } .brand-project{ color:var(--muted); font-weight:500; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
29
- .brand-sub { font-family:var(--mono); font-size:9.5px; text-transform:uppercase; letter-spacing:.08em; color:var(--faint); }
29
+ .brand-sub { display:flex; align-items:center; gap:4px; font-family:var(--mono); font-size:9.5px; text-transform:uppercase; letter-spacing:.08em; color:var(--faint); }
30
+ .brand-sub-sep { color:var(--faint); }
31
+ /* Quick mode/language switch, right on the bar — same source of truth as Settings (renderSettings()
32
+ keeps both pairs synced; a change here saves via the same /api/settings call). Kept visually quiet
33
+ (transparent, no border) until hovered/focused, so it reads as bar text until you reach for it. */
34
+ .brand-mini-select {
35
+ appearance:none; -webkit-appearance:none; border:1px solid transparent; border-radius:5px;
36
+ background:transparent; color:inherit; font:inherit; text-transform:inherit; letter-spacing:inherit;
37
+ padding:1px 14px 1px 3px; cursor:pointer;
38
+ background-image:linear-gradient(45deg,transparent 50%,currentColor 50%),linear-gradient(135deg,currentColor 50%,transparent 50%);
39
+ background-position:right 5px top 55%,right 2px top 55%; background-size:3px 3px,3px 3px; background-repeat:no-repeat;
40
+ }
41
+ .brand-mini-select:hover,.brand-mini-select:focus-visible { color:var(--ink); border-color:var(--line); background-color:var(--surface-2); outline:none; }
42
+ .brand-mini-select option { color:var(--ink); background:var(--surface); text-transform:none; }
30
43
  .progress-meter { width:76px; height:5px; border-radius:999px; background:var(--surface-2); overflow:hidden; flex-shrink:0; margin-left:4px; }
31
44
  .progress-meter-fill { height:100%; width:0%; background:linear-gradient(90deg,var(--cool),var(--signal)); border-radius:999px; transition:width .5s ease; }
32
45
  .tabs { justify-self:center; display:flex; gap:2px; min-width:0; max-width:100%; overflow-x:auto; overflow-y:hidden; scrollbar-width:none; }
@@ -544,6 +557,33 @@ body.booting .ring-svg circle:last-of-type { transform-origin:center; animation:
544
557
  .settings-ro-k { color:var(--muted); font-size:13px; }
545
558
  .settings-ro-v { font-family:var(--mono); font-size:12.5px; color:var(--ink); }
546
559
 
560
+ /* ---- Customize (Settings → dashboards/skills/agents) — reuses .card/.chat-ta/.btn styling so it
561
+ reads as part of the same system, not a bolted-on sub-app. ---- */
562
+ .cz-wrap { margin-top:28px; }
563
+ .cz-block { background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); padding:14px 16px; margin-top:12px; }
564
+ .cz-head { display:flex; align-items:center; justify-content:space-between; gap:12px; }
565
+ .cz-head h3 { margin:0; font-size:13.5px; font-weight:700; }
566
+ .cz-add { font-size:12.5px; padding:6px 13px; }
567
+ .cz-add[aria-expanded="true"] { background:var(--signal); color:var(--on-accent); border-color:transparent; }
568
+ .cz-list { display:flex; flex-direction:column; gap:6px; margin-top:10px; }
569
+ .cz-list .empty { padding:10px 2px; font-size:12.5px; color:var(--muted); }
570
+ .cz-item { display:flex; align-items:baseline; gap:10px; padding:8px 10px; border-radius:8px; cursor:pointer; transition:background .15s; }
571
+ .cz-item:hover, .cz-item:focus-visible { background:var(--surface-2); outline:none; }
572
+ .cz-item-title { font-size:13px; font-weight:600; }
573
+ .cz-item-sub { font-size:11.5px; color:var(--muted); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
574
+ .cz-form { margin-top:12px; padding-top:12px; border-top:1px solid var(--line); display:flex; flex-direction:column; gap:10px; }
575
+ .cz-form .chat-ta { min-height:64px; }
576
+ .cz-form-actions { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
577
+ .cz-form-actions .chat-agent { flex-shrink:0; }
578
+
579
+ /* ---- Custom dashboard pages (Customize → generate-dashboard) — every block below is one of the
580
+ dashboard's own existing components (.ocard/.kpi-row/.stat-tiles/.flatlist/table), so a generated
581
+ page needs no page-specific CSS: it inherits the active design automatically. ---- */
582
+ .custom-dash-wrap { display:flex; flex-direction:column; gap:14px; }
583
+ .custom-dash-body { display:flex; flex-direction:column; gap:14px; }
584
+ .cd-markdown { background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); padding:16px 18px; font-size:13.5px; line-height:1.6; }
585
+ .cd-markdown :first-child { margin-top:0; }
586
+
547
587
  /* App footer — professional, always at the bottom of the content */
548
588
  .app-footer { display:flex; align-items:center; justify-content:space-between; gap:16px; flex-wrap:wrap; padding:14px 22px; border-top:1px solid var(--line); background:var(--surface); color:var(--muted); font-size:12.5px; }
549
589
  .footer-left { display:flex; align-items:center; gap:9px; }
@@ -818,6 +858,7 @@ body.booting .wf-step2 { opacity:0; animation:rise .4s cubic-bezier(.2,.8,.2,1)
818
858
 
819
859
  /* ---- board compactness: expand/collapse-all button + kanban column scroll (v0.13.5) ---- */
820
860
  .mini-btn { font:inherit; font-size:11.5px; padding:5px 12px; border:1px solid var(--line); border-radius:999px; background:var(--surface); color:var(--muted); cursor:pointer; flex-shrink:0; }
861
+ .journal-more { display:block; width:100%; margin-top:8px; text-align:center; }
821
862
  .mini-btn:hover { color:var(--ink); border-color:var(--cool); }
822
863
  .main:has(.board.is-kanban) #phaseToggleAll { display:none; } /* phases are a List concept */
823
864
  .kanban-col-body { max-height:64vh; overflow-y:auto; } /* keep columns compact on a big project */
@@ -29,6 +29,7 @@ function frameworkVersion(){
29
29
  function project(){
30
30
  const p = store.readProject(ROOT);
31
31
  const v = frameworkVersion(); if (v) p.version = v;
32
+ p.projectName = path.basename(ROOT); // the actual project folder, always shown in the topbar
32
33
  return p;
33
34
  }
34
35
  function sendJSON(res,code,obj){ res.writeHead(code,{'Content-Type':'application/json; charset=utf-8'}); res.end(JSON.stringify(obj)); }
@@ -69,7 +70,13 @@ function promoteAttention(item){
69
70
  }
70
71
 
71
72
  function watch(dir){ try{ fs.watch(dir,{recursive:false},()=>emit({type:'change'})); }catch(_){} }
72
- ['plans','specs','.spectoflow'].forEach(d=>{ const p=path.join(ROOT,d); if(fs.existsSync(p)) watch(p); });
73
+ // Custom dashboards (Customize page) live in their own subdirectory of .spectoflow, which the
74
+ // top-level `.spectoflow` watch below does NOT cover — fs.watch here is non-recursive on purpose
75
+ // (a recursive watch on the whole .spectoflow tree would also fire on every runtime.json write).
76
+ // Ensure the directory exists before watching it: a project that hasn't used Customize yet won't
77
+ // have it on disk, and `spectoflow init` on an older install won't have created it either.
78
+ try { fs.mkdirSync(path.join(ROOT,'.spectoflow','dashboard','custom'), { recursive: true }); } catch (_) {}
79
+ ['plans','specs','.spectoflow','.spectoflow/dashboard/custom'].forEach(d=>{ const p=path.join(ROOT,d); if(fs.existsSync(p)) watch(p); });
73
80
 
74
81
  // A process restart loses any in-flight orchestration; without this, a stale 'running' or
75
82
  // 'awaiting_approval' status wedges the /api/orchestrate 409 guard forever. Not a real
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+ /*
3
+ * Pure helpers for user-generated custom dashboards (.spectoflow/dashboard/custom/<id>.json).
4
+ *
5
+ * A custom dashboard is a DECLARATIVE block spec, never raw HTML/CSS/JS: the generating agent picks
6
+ * blocks from a fixed vocabulary (BLOCK_TYPES) that the dashboard already knows how to render, using
7
+ * the exact same token-driven components (kpi cards, bars, donut, tables…) the built-in Board uses.
8
+ * That is what guarantees a custom dashboard always matches the active design — including any design
9
+ * the user switches to later — with zero per-dashboard styling to keep in sync, and no arbitrary code
10
+ * ever running in the dashboard.
11
+ *
12
+ * Zero dependency; consumed by templates/dashboard/server.js (Node) via readCustomDashboards() in
13
+ * store.js. The browser-side renderer (dashboard/public/app.js) re-implements the tiny `resolveBind`
14
+ * walk independently — sharing code across the Node/browser boundary would need a build step, which
15
+ * this project avoids on purpose (see CLAUDE.md's zero-runtime-dependency invariant).
16
+ */
17
+
18
+ // Every block a generated dashboard may use. Adding a new type here is how the vocabulary grows —
19
+ // pair it with a matching case in app.js's renderCustomBlock().
20
+ const BLOCK_TYPES = new Set([
21
+ 'markdown', // rendered prose — a spec excerpt, an explanation, a summary
22
+ 'kpi-row', // a row of big-number stat cards, each optionally live-bound
23
+ 'chart-bars', // horizontal progress/comparison bars
24
+ 'chart-donut', // a status/category breakdown donut + legend
25
+ 'table', // a simple column/row data table
26
+ 'list', // a flat bullet list
27
+ 'stat-tile-row', // a row of compact stat tiles (value/label/sub)
28
+ ]);
29
+
30
+ // A small, explicit allow-list of live data paths a block may `bind` to, resolved against the same
31
+ // stats object SpectoStats.stats(P) already computes for the built-in Board — never an arbitrary
32
+ // expression, just a dotted property walk, so there is nothing to sandbox or evaluate.
33
+ // Mirrors the exact shape SpectoStats.stats(P) returns (dashboard/public/stats.js):
34
+ // { total, done, pct, byStatus, phases, toAsk, running, statuses }.
35
+ const BIND_ROOTS = new Set(['pct', 'done', 'total', 'byStatus', 'phases', 'toAsk', 'running', 'statuses']);
36
+
37
+ const ID_RE = /^[a-z][a-z0-9-]{0,39}$/;
38
+
39
+ // A conservative, curated icon key set — the same ICON map the rest of the dashboard already uses
40
+ // (icons.js), so a custom dashboard's tab never introduces a one-off, unstyled icon.
41
+ const ICON_KEYS = new Set(['board', 'requests', 'backlog', 'workflow', 'agents', 'chat', 'info', 'attention', 'settings']);
42
+
43
+ function isPlainObject(v) { return v != null && typeof v === 'object' && !Array.isArray(v); }
44
+
45
+ // Validates one block. Returns a list of error strings (empty = valid). Deliberately permissive on
46
+ // the *content* fields (labels, values, markdown text are free text) — it only enforces the block's
47
+ // *shape* (a known type, and that any `bind` path starts from an allowed root) so a slightly unusual
48
+ // but well-typed spec still renders rather than being rejected outright.
49
+ function validateBlock(b, i) {
50
+ const errs = [];
51
+ const at = `blocks[${i}]`;
52
+ if (!isPlainObject(b)) { errs.push(`${at} is not an object`); return errs; }
53
+ if (!BLOCK_TYPES.has(b.type)) errs.push(`${at}.type "${b.type}" is not a known block type (${[...BLOCK_TYPES].join(', ')})`);
54
+ const binds = [];
55
+ if (typeof b.bind === 'string') binds.push(b.bind);
56
+ if (Array.isArray(b.items)) b.items.forEach((it) => { if (it && typeof it.bind === 'string') binds.push(it.bind); });
57
+ if (Array.isArray(b.rows)) b.rows.forEach((r) => { if (r && typeof r.bind === 'string') binds.push(r.bind); });
58
+ if (Array.isArray(b.segments)) b.segments.forEach((s) => { if (s && typeof s.bind === 'string') binds.push(s.bind); });
59
+ binds.forEach((p) => { const root = String(p).split('.')[0]; if (!BIND_ROOTS.has(root)) errs.push(`${at} has an unbound bind path "${p}" (must start with one of: ${[...BIND_ROOTS].join(', ')})`); });
60
+ return errs;
61
+ }
62
+
63
+ // Validates a whole dashboard spec as read from disk. Never throws — callers (store.js) should skip
64
+ // an invalid file rather than let one bad custom dashboard break the whole /api/project response.
65
+ function validateSpec(spec) {
66
+ const errors = [];
67
+ if (!isPlainObject(spec)) return { valid: false, errors: ['not an object'] };
68
+ if (!ID_RE.test(String(spec.id || ''))) errors.push('id must be lowercase kebab-case, starting with a letter, 1-40 chars');
69
+ if (!spec.title || typeof spec.title !== 'string') errors.push('title is required (a short display name)');
70
+ if (spec.icon != null && !ICON_KEYS.has(spec.icon)) errors.push(`icon "${spec.icon}" is not one of: ${[...ICON_KEYS].join(', ')}`);
71
+ if (!Array.isArray(spec.blocks) || !spec.blocks.length) errors.push('blocks must be a non-empty array');
72
+ else spec.blocks.forEach((b, i) => errors.push(...validateBlock(b, i)));
73
+ return { valid: errors.length === 0, errors };
74
+ }
75
+
76
+ module.exports = { BLOCK_TYPES, BIND_ROOTS, ICON_KEYS, validateSpec, validateBlock };