@hanzlaa/rcode 4.1.2 → 4.3.1
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.
- package/cli/install.js +176 -13
- package/cli/lib/config.cjs +4 -2
- package/cli/lib/fsutil.cjs +13 -2
- package/cli/lib/homedir.cjs +21 -0
- package/cli/lib/schemas.cjs +6 -1
- package/cli/nuke.js +13 -8
- package/cli/postinstall.js +14 -4
- package/cli/rcode-slash-router.cjs +118 -0
- package/cli/uninstall.js +59 -1
- package/cli/update.js +10 -5
- package/dist/rcode.js +234 -230
- package/package.json +1 -1
- package/rcode/references/auto-init-guard.md +2 -2
- package/rcode/references/output-format.md +5 -5
- package/rcode/skills/actions/2-plan/rcode-create-milestone/steps/step-10-complete.md +1 -1
- package/server/dashboard.js +33 -13
- package/server/lib/api.js +62 -4
- package/server/lib/html/client/agents-data.js +22 -18
- package/server/lib/html/client/app.js +3 -0
- package/server/lib/html/client/components/AgentCard.js +127 -0
- package/server/lib/html/client/components/App.js +104 -39
- package/server/lib/html/client/components/CommandPalette.js +133 -0
- package/server/lib/html/client/components/FileReader.js +116 -0
- package/server/lib/html/client/components/FilterChips.js +94 -0
- package/server/lib/html/client/components/NotifyCenter.js +117 -0
- package/server/lib/html/client/components/OrchPanel.js +80 -52
- package/server/lib/html/client/components/PhaseGraph.js +300 -0
- package/server/lib/html/client/components/RejectDialog.js +78 -0
- package/server/lib/html/client/components/RunnerPicker.js +190 -0
- package/server/lib/html/client/components/Sidebar.js +106 -61
- package/server/lib/html/client/components/StatusSummaryBar.js +76 -0
- package/server/lib/html/client/components/TaskPipeline.js +83 -0
- package/server/lib/html/client/components/Topbar.js +86 -39
- package/server/lib/html/client/components/dashboard/Blockers.js +57 -0
- package/server/lib/html/client/components/dashboard/CompletedTasks.js +47 -0
- package/server/lib/html/client/components/dashboard/CurrentPhase.js +107 -0
- package/server/lib/html/client/components/dashboard/InProgress.js +72 -0
- package/server/lib/html/client/components/dashboard/ProgressDonut.js +101 -0
- package/server/lib/html/client/components/dashboard/ProgressTimeline.js +101 -0
- package/server/lib/html/client/components/dashboard/ProjectHealth.js +80 -0
- package/server/lib/html/client/components/dashboard/RecentDecisions.js +57 -0
- package/server/lib/html/client/components/dashboard/Timeline.js +143 -0
- package/server/lib/html/client/components/shared.js +47 -11
- package/server/lib/html/client/filter-state.js +72 -0
- package/server/lib/html/client/icons-client.js +7 -0
- package/server/lib/html/client/notify.js +75 -0
- package/server/lib/html/client/orchestrator.js +168 -41
- package/server/lib/html/client/preact.js +13 -8
- package/server/lib/html/client/store.js +70 -6
- package/server/lib/html/client/util.js +78 -0
- package/server/lib/html/client/vendor/htm.js +1 -0
- package/server/lib/html/client/vendor/preact-hooks.js +2 -0
- package/server/lib/html/client/vendor/preact.js +2 -0
- package/server/lib/html/client/views/AgentsView.js +144 -51
- package/server/lib/html/client/views/FilesView.js +20 -103
- package/server/lib/html/client/views/KanbanView.js +40 -21
- package/server/lib/html/client/views/MemoryView.js +26 -9
- package/server/lib/html/client/views/MilestonesView.js +4 -4
- package/server/lib/html/client/views/OrchestrationView.js +154 -19
- package/server/lib/html/client/views/OverviewView.js +47 -239
- package/server/lib/html/client/views/PhasesView.js +50 -6
- package/server/lib/html/client/views/RoadmapView.js +6 -3
- package/server/lib/html/client/views/SprintsView.js +50 -6
- package/server/lib/html/client/views/TasksView.js +4 -3
- package/server/lib/html/client.js +21 -4
- package/server/lib/html/css.js +2761 -8
- package/server/lib/html/icons.js +7 -0
- package/server/lib/html/shell.js +10 -3
- package/server/lib/scanner.js +376 -39
- package/server/orchestrator.js +346 -7
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
import { html } from '../preact.js';
|
|
11
11
|
import { useStore } from '../store.js';
|
|
12
|
-
import { pct, humanDate, allSprints, allTasks } from '../util.js';
|
|
13
|
-
import { CompletionRing, Breadcrumb, Tag, PhaseCard } from '../components/shared.js';
|
|
12
|
+
import { pct, humanDate, allSprints, allTasks, currentPhaseName } from '../util.js';
|
|
13
|
+
import { CompletionRing, Breadcrumb, Tag, PhaseCard, pressable } from '../components/shared.js';
|
|
14
14
|
import { runningTotal } from '../orchestrator.js';
|
|
15
15
|
import { Icon } from '../icons-client.js';
|
|
16
16
|
|
|
@@ -94,7 +94,7 @@ export function MilestonesView({ subId }) {
|
|
|
94
94
|
<div class="attr-grid">
|
|
95
95
|
<${AttrItem} label="Total Phases" value=${phases.length}/>
|
|
96
96
|
<${AttrItem} label="Completed Phases" value=${doneP}/>
|
|
97
|
-
<${AttrItem} label="Current Phase" value=${S.currentPhase || '—'}/>
|
|
97
|
+
<${AttrItem} label="Current Phase" value=${currentPhaseName(S.currentPhase) || '—'}/>
|
|
98
98
|
<${AttrItem} label="Current Sprint" value=${S.currentSprint || '—'}/>
|
|
99
99
|
<${AttrItem} label="Tasks Done" value=${done.length + '/' + total.length}/>
|
|
100
100
|
<${AttrItem} label="Progress" value=${pct(done.length, total.length)}/>
|
|
@@ -116,7 +116,7 @@ export function MilestonesView({ subId }) {
|
|
|
116
116
|
<div id="view-milestones" class="view active">
|
|
117
117
|
<div class="view-title">Milestones</div>
|
|
118
118
|
<div class="phase-list">
|
|
119
|
-
<div class="item item-clickable"
|
|
119
|
+
<div class="item item-clickable" ...${pressable(() => { location.hash = 'milestones/M1'; })}>
|
|
120
120
|
<div style="display:flex;align-items:center;gap:var(--space-4);">
|
|
121
121
|
<${CompletionRing} done=${done.length} total=${total.length}/>
|
|
122
122
|
<div>
|
|
@@ -12,18 +12,37 @@
|
|
|
12
12
|
|
|
13
13
|
import { html, useState, useEffect } from '../preact.js';
|
|
14
14
|
import { useStore } from '../store.js';
|
|
15
|
-
import { stopSession, openTermPanel,
|
|
16
|
-
import {
|
|
15
|
+
import { stopSession, openTermPanel, ALLOWED_COMMANDS, isSessionRunning, mergeSessionsAndHistory } from '../orchestrator.js';
|
|
16
|
+
import { openRunnerPicker } from '../components/RunnerPicker.js';
|
|
17
|
+
import { RejectDialog } from '../components/RejectDialog.js';
|
|
18
|
+
import { orchElapsed, humanDate } from '../util.js';
|
|
17
19
|
import { Icon } from '../icons-client.js';
|
|
18
20
|
|
|
19
21
|
// ── Session card ──────────────────────────────────────────────────────────────
|
|
20
22
|
|
|
23
|
+
// Tooltip text per session status — shown on the colored status dot.
|
|
24
|
+
const DOT_TITLES = {
|
|
25
|
+
running: 'Running — output streaming',
|
|
26
|
+
blocked: 'Blocked — waiting for your input',
|
|
27
|
+
done: 'Done — exited cleanly',
|
|
28
|
+
exited: 'Exited',
|
|
29
|
+
stopped: 'Stopped',
|
|
30
|
+
error: 'Error',
|
|
31
|
+
};
|
|
32
|
+
|
|
21
33
|
function OrchCard({ session: s }) {
|
|
22
|
-
const
|
|
23
|
-
|
|
34
|
+
const blocked = s.status === 'blocked';
|
|
35
|
+
// 'blocked' is a live PTY (server-side classification of running) — keep
|
|
36
|
+
// the Stop button available for it.
|
|
37
|
+
const running = s.status === 'running' || blocked;
|
|
38
|
+
const waiting = blocked || !!s.waiting;
|
|
39
|
+
const [showReject, setShowReject] = useState(false);
|
|
24
40
|
const cardCls = 'orch-card orch-' + s.status + (waiting ? ' orch-waiting' : '');
|
|
25
|
-
const badge =
|
|
26
|
-
|
|
41
|
+
const badge = blocked
|
|
42
|
+
? html`<${Icon} name="alert-triangle" size=${12}/> blocked — needs input`
|
|
43
|
+
: waiting ? html`<${Icon} name="hourglass" size=${12}/> waiting for input` : s.status;
|
|
44
|
+
const dotCls = 'term-status-dot ' + (blocked ? 'blocked' : waiting ? 'waiting' : s.status);
|
|
45
|
+
const dotTitle = DOT_TITLES[s.status] || s.status;
|
|
27
46
|
|
|
28
47
|
function handleTerminal(e) {
|
|
29
48
|
e.stopPropagation();
|
|
@@ -38,8 +57,13 @@ function OrchCard({ session: s }) {
|
|
|
38
57
|
return html`
|
|
39
58
|
<div class=${cardCls}>
|
|
40
59
|
<div class="orch-card-head">
|
|
41
|
-
<span class=${dotCls}></span>
|
|
60
|
+
<span class=${dotCls} title=${dotTitle}></span>
|
|
42
61
|
<span class="orch-card-id">${s.storyId}</span>
|
|
62
|
+
${s.runner ? html`
|
|
63
|
+
<span class="runner-badge" title=${'Launched with ' + s.runner + (s.model ? ' (' + s.model + ')' : '')}>
|
|
64
|
+
${s.runner}${s.model ? ' · ' + s.model : ''}
|
|
65
|
+
</span>
|
|
66
|
+
` : null}
|
|
43
67
|
<span class="orch-card-badge">${badge}</span>
|
|
44
68
|
</div>
|
|
45
69
|
<div class="orch-card-cmd">${s.cmd || ''}</div>
|
|
@@ -49,6 +73,11 @@ function OrchCard({ session: s }) {
|
|
|
49
73
|
${' · '}<${Icon} name="eye" size=${12}/> ${s.clients || 0}
|
|
50
74
|
${s.pid ? html` · pid ${s.pid}` : null}
|
|
51
75
|
</div>
|
|
76
|
+
${s.rejection ? html`
|
|
77
|
+
<div class="orch-card-rejection">
|
|
78
|
+
Rejected: ${s.rejection.reason}
|
|
79
|
+
</div>
|
|
80
|
+
` : null}
|
|
52
81
|
<div class="orch-card-actions">
|
|
53
82
|
<button class="term-run-btn outline" onClick=${handleTerminal}>
|
|
54
83
|
<${Icon} name="monitor" size=${14}/> Terminal
|
|
@@ -56,7 +85,13 @@ function OrchCard({ session: s }) {
|
|
|
56
85
|
${running ? html`
|
|
57
86
|
<button class="term-run-btn danger" onClick=${handleStop}>■ Stop</button>
|
|
58
87
|
` : null}
|
|
88
|
+
${waiting ? html`
|
|
89
|
+
<button class="term-run-btn danger" onClick=${e => { e.stopPropagation(); setShowReject(true); }}>
|
|
90
|
+
<${Icon} name="alert-triangle" size=${14}/> Reject
|
|
91
|
+
</button>
|
|
92
|
+
` : null}
|
|
59
93
|
</div>
|
|
94
|
+
${showReject ? html`<${RejectDialog} session=${s} onClose=${() => setShowReject(false)}/>` : null}
|
|
60
95
|
</div>
|
|
61
96
|
`;
|
|
62
97
|
}
|
|
@@ -65,7 +100,11 @@ function OrchCard({ session: s }) {
|
|
|
65
100
|
|
|
66
101
|
function sortSessions(sessions) {
|
|
67
102
|
return [...sessions].sort((a, b) => {
|
|
68
|
-
//
|
|
103
|
+
// Blocked-on-input first (needs immediate attention)
|
|
104
|
+
if ((a.status === 'blocked') !== (b.status === 'blocked')) {
|
|
105
|
+
return a.status === 'blocked' ? -1 : 1;
|
|
106
|
+
}
|
|
107
|
+
// Then idle-waiting
|
|
69
108
|
if (!!a.waiting !== !!b.waiting) return a.waiting ? -1 : 1;
|
|
70
109
|
// Then running
|
|
71
110
|
if ((a.status === 'running') !== (b.status === 'running')) {
|
|
@@ -84,14 +123,16 @@ function sortSessions(sessions) {
|
|
|
84
123
|
* all session and terminal state via runCommandFromUI → runSession.
|
|
85
124
|
*/
|
|
86
125
|
function CommandRunner() {
|
|
87
|
-
|
|
126
|
+
// Subscribe to store updates so isSessionRunning() + orchOnline re-evaluate on each poll.
|
|
127
|
+
const { orchOnline } = useStore();
|
|
88
128
|
const [selected, setSelected] = useState(ALLOWED_COMMANDS[0]?.cmd || '');
|
|
89
129
|
const [busy, setBusy] = useState(false);
|
|
90
130
|
|
|
91
131
|
const slug = selected ? selected.replace(/^\//, '').replace(/\//g, '-') : '';
|
|
92
132
|
const sessionId = slug ? 'cmd-' + slug : '';
|
|
93
133
|
const isRunning = sessionId ? isSessionRunning(sessionId) : false;
|
|
94
|
-
const
|
|
134
|
+
const orchDown = orchOnline === false;
|
|
135
|
+
const disabled = busy || isRunning || orchDown;
|
|
95
136
|
|
|
96
137
|
// Reset busy 2 s after a Run click — the terminal panel is now open and the
|
|
97
138
|
// session is streaming. Managed via useEffect so the timer is cancelled if
|
|
@@ -102,10 +143,10 @@ function CommandRunner() {
|
|
|
102
143
|
return () => clearTimeout(t);
|
|
103
144
|
}, [busy]);
|
|
104
145
|
|
|
105
|
-
function handleRun() {
|
|
146
|
+
function handleRun(e) {
|
|
106
147
|
if (!selected || disabled) return;
|
|
107
148
|
setBusy(true);
|
|
108
|
-
|
|
149
|
+
openRunnerPicker(e.currentTarget, { kind: 'command', cmd: selected, title: selected });
|
|
109
150
|
}
|
|
110
151
|
|
|
111
152
|
return html`
|
|
@@ -132,12 +173,96 @@ function CommandRunner() {
|
|
|
132
173
|
</button>
|
|
133
174
|
</div>
|
|
134
175
|
<div class="cmd-runner-hint">
|
|
135
|
-
${
|
|
136
|
-
? html`
|
|
137
|
-
:
|
|
138
|
-
? html`
|
|
139
|
-
:
|
|
176
|
+
${orchDown
|
|
177
|
+
? html`Orchestrator is unreachable — commands cannot run until it is back.`
|
|
178
|
+
: isRunning
|
|
179
|
+
? html`Command is running — output is streaming to the terminal panel.`
|
|
180
|
+
: busy
|
|
181
|
+
? html`Starting — the terminal panel will open shortly.`
|
|
182
|
+
: html`Select a command and press Run. Output streams live to the terminal panel.`}
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── Run history panel ─────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
function durationLabel(ms) {
|
|
191
|
+
if (!ms || !isFinite(ms) || ms <= 0) return '—';
|
|
192
|
+
if (ms < 60000) return Math.round(ms / 1000) + 's';
|
|
193
|
+
if (ms < 3600000) return Math.floor(ms / 60000) + 'm ' + Math.round((ms % 60000) / 1000) + 's';
|
|
194
|
+
return Math.floor(ms / 3600000) + 'h ' + Math.floor((ms % 3600000) / 60000) + 'm';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function HistoryRow({ run }) {
|
|
198
|
+
return html`
|
|
199
|
+
<div class="hist-row" key=${run.storyId}>
|
|
200
|
+
<span class=${'term-status-dot ' + run.status}></span>
|
|
201
|
+
<span class="hist-row-id">${run.storyId}</span>
|
|
202
|
+
<span class="hist-row-cmd">${run.cmd}</span>
|
|
203
|
+
<span class="hist-row-duration"><${Icon} name="clock" size=${12}/> ${durationLabel(run.durationMs)}</span>
|
|
204
|
+
<span class="hist-row-status">${run.status}</span>
|
|
205
|
+
</div>
|
|
206
|
+
`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const STATUS_ORDER = ['done', 'exited', 'stopped', 'error'];
|
|
210
|
+
|
|
211
|
+
function HistoryPanel() {
|
|
212
|
+
const { activeSessions, history } = useStore();
|
|
213
|
+
const merged = mergeSessionsAndHistory(activeSessions, history);
|
|
214
|
+
// 'blocked' is a live session (waiting for input), not an ended run.
|
|
215
|
+
const ended = merged.filter(r => r.status !== 'running' && r.status !== 'blocked');
|
|
216
|
+
|
|
217
|
+
if (ended.length === 0) {
|
|
218
|
+
return html`
|
|
219
|
+
<div class="hist-panel">
|
|
220
|
+
<div class="hist-panel-title">
|
|
221
|
+
<${Icon} name="history" size=${16}/> Run History
|
|
222
|
+
</div>
|
|
223
|
+
<div class="empty">No past runs yet.</div>
|
|
224
|
+
</div>
|
|
225
|
+
`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Group by status (STATUS_ORDER), then within each group by date label
|
|
229
|
+
const byStatus = new Map();
|
|
230
|
+
for (const status of STATUS_ORDER) byStatus.set(status, new Map());
|
|
231
|
+
|
|
232
|
+
for (const run of ended) {
|
|
233
|
+
const bucket = byStatus.get(run.status) || byStatus.get('error');
|
|
234
|
+
const dateKey = humanDate(run.endTime || run.startTime) || 'Unknown date';
|
|
235
|
+
if (!bucket.has(dateKey)) bucket.set(dateKey, []);
|
|
236
|
+
bucket.get(dateKey).push(run);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Sort runs within each date group: newest first
|
|
240
|
+
for (const dateMap of byStatus.values()) {
|
|
241
|
+
for (const runs of dateMap.values()) {
|
|
242
|
+
runs.sort((a, b) => String(b.endTime || b.startTime || '').localeCompare(String(a.endTime || a.startTime || '')));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return html`
|
|
247
|
+
<div class="hist-panel">
|
|
248
|
+
<div class="hist-panel-title">
|
|
249
|
+
<${Icon} name="history" size=${16}/> Run History
|
|
140
250
|
</div>
|
|
251
|
+
${STATUS_ORDER.map(status => {
|
|
252
|
+
const dateMap = byStatus.get(status);
|
|
253
|
+
if (!dateMap || dateMap.size === 0) return null;
|
|
254
|
+
return html`
|
|
255
|
+
<div class="hist-group" key=${status}>
|
|
256
|
+
<div class="hist-group-title">${status}</div>
|
|
257
|
+
${[...dateMap.entries()].map(([dateKey, runs]) => html`
|
|
258
|
+
<div key=${dateKey}>
|
|
259
|
+
<div class="hist-date">${dateKey}</div>
|
|
260
|
+
${runs.map(run => html`<${HistoryRow} key=${run.storyId} run=${run}/>`)}
|
|
261
|
+
</div>
|
|
262
|
+
`)}
|
|
263
|
+
</div>
|
|
264
|
+
`;
|
|
265
|
+
})}
|
|
141
266
|
</div>
|
|
142
267
|
`;
|
|
143
268
|
}
|
|
@@ -145,8 +270,9 @@ function CommandRunner() {
|
|
|
145
270
|
// ── Root view ─────────────────────────────────────────────────────────────────
|
|
146
271
|
|
|
147
272
|
export function OrchestrationView() {
|
|
148
|
-
const { activeSessions } = useStore();
|
|
273
|
+
const { activeSessions, orchOnline } = useStore();
|
|
149
274
|
const sessions = sortSessions(activeSessions || []);
|
|
275
|
+
const orchDown = orchOnline === false;
|
|
150
276
|
|
|
151
277
|
return html`
|
|
152
278
|
<div class="view active" id="view-orchestration">
|
|
@@ -155,11 +281,18 @@ export function OrchestrationView() {
|
|
|
155
281
|
Live agent sessions — run, watch, communicate, stop.
|
|
156
282
|
</div>
|
|
157
283
|
|
|
284
|
+
${orchDown ? html`
|
|
285
|
+
<div class="orch-down-banner" role="alert">
|
|
286
|
+
⚠ Orchestrator unreachable (port 7718) — Run buttons are disabled.
|
|
287
|
+
Restart the dashboard, or set ORCH_PORT if the port is in use.
|
|
288
|
+
</div>
|
|
289
|
+
` : null}
|
|
290
|
+
|
|
158
291
|
<${CommandRunner}/>
|
|
159
292
|
|
|
160
293
|
${sessions.length === 0 ? html`
|
|
161
294
|
<div class="empty">
|
|
162
|
-
No active execution.
|
|
295
|
+
${orchDown ? 'Session status unavailable while the orchestrator is down.' : 'No active execution.'}
|
|
163
296
|
<div class="empty-action">
|
|
164
297
|
Use the Command Runner above, or run <code>/rcode-execute</code> to
|
|
165
298
|
start a phase or sprint.
|
|
@@ -172,6 +305,8 @@ export function OrchestrationView() {
|
|
|
172
305
|
`)}
|
|
173
306
|
</div>
|
|
174
307
|
`}
|
|
308
|
+
|
|
309
|
+
<${HistoryPanel}/>
|
|
175
310
|
</div>
|
|
176
311
|
`;
|
|
177
312
|
}
|
|
@@ -1,261 +1,69 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OverviewView — Preact component.
|
|
2
|
+
* OverviewView — Preact component (dashboard redesign).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Retargeted onto the mockup: a 12-col, 3-row card grid that composes the nine
|
|
5
|
+
* dashboard slot components. The components are empty placeholders for now —
|
|
6
|
+
* other agents fill each one with real content. Layout follows
|
|
7
|
+
* .planning/campaign/MOCKUP-SPEC.md:
|
|
8
|
+
* Row 1: ProgressDonut · CurrentPhase · Timeline
|
|
9
|
+
* Row 2: CompletedTasks · InProgress · Blockers
|
|
10
|
+
* Row 3: RecentDecisions · ProgressTimeline
|
|
11
|
+
*
|
|
12
|
+
* State is read via useStore() and flows down to the slot components as they
|
|
13
|
+
* are filled in; none of them fetch (see DATA-CONTRACT.md).
|
|
14
|
+
*
|
|
15
|
+
* First run: when the server scanned and found no .rcode directory
|
|
16
|
+
* (store.initialized === false) the card grid is replaced by an honest
|
|
17
|
+
* get-started state pointing at /rcode-init — no fabricated dashboard.
|
|
6
18
|
*/
|
|
7
19
|
|
|
8
20
|
import { html } from '../preact.js';
|
|
9
21
|
import { useStore } from '../store.js';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
22
|
+
import { ProgressDonut } from '../components/dashboard/ProgressDonut.js';
|
|
23
|
+
import { CurrentPhase } from '../components/dashboard/CurrentPhase.js';
|
|
24
|
+
import { Timeline } from '../components/dashboard/Timeline.js';
|
|
25
|
+
import { CompletedTasks } from '../components/dashboard/CompletedTasks.js';
|
|
26
|
+
import { InProgress } from '../components/dashboard/InProgress.js';
|
|
27
|
+
import { Blockers } from '../components/dashboard/Blockers.js';
|
|
28
|
+
import { RecentDecisions } from '../components/dashboard/RecentDecisions.js';
|
|
29
|
+
import { ProgressTimeline } from '../components/dashboard/ProgressTimeline.js';
|
|
15
30
|
|
|
16
31
|
export function OverviewView() {
|
|
32
|
+
// Subscribe to the store so this view re-renders on state changes; the slot
|
|
33
|
+
// components will read their slices from it as they are built out.
|
|
17
34
|
const S = useStore();
|
|
18
|
-
const sprints = allSprints(S.phases);
|
|
19
|
-
const curSprint = sprints.find(s => s.id === S.currentSprint) || null;
|
|
20
|
-
|
|
21
|
-
// Velocity sparkline data
|
|
22
|
-
const completedSprints = sprints.filter(s => s.velocity_actual != null);
|
|
23
|
-
const showVelocity = completedSprints.length > 1;
|
|
24
|
-
|
|
25
|
-
// Chains & workstreams
|
|
26
|
-
const chains = S.chains || [];
|
|
27
|
-
const workstreams = S.workstreams || [];
|
|
28
|
-
|
|
29
|
-
// Cmd hints
|
|
30
|
-
const baseHints = [
|
|
31
|
-
['/rcode-next', 'What should I do next?'],
|
|
32
|
-
['/rcode-status', 'Quick project status'],
|
|
33
|
-
['/rcode-council','Ask the team a question'],
|
|
34
|
-
];
|
|
35
|
-
const sprintHints = getSprintHints(curSprint);
|
|
36
|
-
let hints = [...sprintHints, ...baseHints];
|
|
37
|
-
if (S.pendingHandoff) {
|
|
38
|
-
hints = [['/rcode-resume-work','Resume from the pending handoff'], ...hints];
|
|
39
|
-
}
|
|
40
35
|
|
|
41
|
-
|
|
42
|
-
function StatusSummary() {
|
|
43
|
-
const curPhase = (S.phases || []).find(
|
|
44
|
-
p => String(p.id) === String(S.currentPhase),
|
|
45
|
-
) || null;
|
|
46
|
-
const blockedCount = allTasks(S.phases).filter(t => t.status === 'blocked').length;
|
|
47
|
-
const lastExec = S.last_session
|
|
48
|
-
? humanDate(S.last_session.date || S.last_session.timestamp)
|
|
49
|
-
: null;
|
|
36
|
+
if (S.initialized === false) {
|
|
50
37
|
return html`
|
|
51
|
-
<div class="
|
|
52
|
-
<div class="
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
38
|
+
<div id="view-overview" class="view active">
|
|
39
|
+
<div class="firstrun">
|
|
40
|
+
<div class="firstrun-badge" aria-hidden="true">r</div>
|
|
41
|
+
<h2 class="firstrun-title">No project initialized</h2>
|
|
42
|
+
<p class="firstrun-sub">
|
|
43
|
+
This directory has no <code>.rcode</code> project yet, so there is
|
|
44
|
+
no data to show. Initialize one to start tracking phases, sprints,
|
|
45
|
+
and decisions.
|
|
46
|
+
</p>
|
|
47
|
+
<code class="firstrun-cmd">/rcode-init</code>
|
|
56
48
|
</div>
|
|
57
49
|
</div>
|
|
58
|
-
<div class="stat">
|
|
59
|
-
<div class="label">Active Sprint</div>
|
|
60
|
-
<div class="value">${curSprint ? curSprint.id : '—'}</div>
|
|
61
|
-
<div class="sub">${curSprint ? (curSprint.goal || curSprint.status || 'in progress') : 'No active sprint'}</div>
|
|
62
|
-
</div>
|
|
63
|
-
<div class="stat" style=${blockedCount ? 'border-left-color:var(--red,#eb5757)' : ''}>
|
|
64
|
-
<div class="label">Blocked Tasks</div>
|
|
65
|
-
<div class="value" style=${blockedCount ? 'color:var(--red,#eb5757)' : ''}>${blockedCount}</div>
|
|
66
|
-
<div class="sub">${blockedCount ? 'needs attention' : 'all clear'}</div>
|
|
67
|
-
</div>
|
|
68
|
-
<div class="stat">
|
|
69
|
-
<div class="label">Last Execution</div>
|
|
70
|
-
<div class="value" style="font-size:var(--text-lg,1rem);">${lastExec || '—'}</div>
|
|
71
|
-
<div class="sub">${lastExec ? 'most recent session' : 'no sessions yet'}</div>
|
|
72
|
-
</div>
|
|
73
|
-
`;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Current sprint progress
|
|
77
|
-
function SprintProgress() {
|
|
78
|
-
if (!curSprint) return null;
|
|
79
|
-
const sts = curSprint.stories || [];
|
|
80
|
-
const d = sts.filter(t => t.status === 'done' || t.status === 'completed').length;
|
|
81
|
-
return html`
|
|
82
|
-
<section>
|
|
83
|
-
<h2 class="section-icon"><${Icon} name="zap" size=${16}/> Current Sprint — ${curSprint.id}</h2>
|
|
84
|
-
<div class="body">
|
|
85
|
-
<div style="margin-bottom:8px;font-size:var(--text-sm);color:var(--text-secondary);">
|
|
86
|
-
${curSprint.goal || ''}
|
|
87
|
-
</div>
|
|
88
|
-
<div style="display:flex;align-items:center;gap:var(--space-3);">
|
|
89
|
-
<div style="flex:1;"><${ProgressBar} done=${d} total=${sts.length}/></div>
|
|
90
|
-
<span style="font-size:var(--text-sm);font-weight:600;">
|
|
91
|
-
${d}/${sts.length} (${pct(d, sts.length)})
|
|
92
|
-
</span>
|
|
93
|
-
</div>
|
|
94
|
-
</div>
|
|
95
|
-
</section>
|
|
96
|
-
`;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Velocity sparkline (inline SVG, same as client-render.js:267-272)
|
|
100
|
-
function VelocitySpark() {
|
|
101
|
-
if (!showVelocity) return null;
|
|
102
|
-
const vals = completedSprints.map(s => s.velocity_actual);
|
|
103
|
-
const max = Math.max(...vals, 1);
|
|
104
|
-
const w = 200, h = 40, step = w / (vals.length - 1);
|
|
105
|
-
const points = vals.map((v, i) => (i * step) + ',' + (h - (v / max) * h)).join(' ');
|
|
106
|
-
return html`
|
|
107
|
-
<div class="stat">
|
|
108
|
-
<div class="label">Sprint Velocity</div>
|
|
109
|
-
<svg width=${w} height=${h + 4} style="margin-top:8px;">
|
|
110
|
-
<polyline points=${points} fill="none" stroke="var(--accent-blue)" stroke-width="2"/>
|
|
111
|
-
</svg>
|
|
112
|
-
<div class="sub">Last ${vals.length} sprints</div>
|
|
113
|
-
</div>
|
|
114
|
-
`;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Council sessions
|
|
118
|
-
function CouncilSessions() {
|
|
119
|
-
if (!Array.isArray(S.council_sessions) || !S.council_sessions.length) return null;
|
|
120
|
-
return html`
|
|
121
|
-
<section>
|
|
122
|
-
<h2 class="section-icon"><${Icon} name="building" size=${16}/> Council Sessions</h2>
|
|
123
|
-
<div class="body">
|
|
124
|
-
<div class="phase-list">
|
|
125
|
-
${S.council_sessions.slice(-5).reverse().map((cs, i) => html`
|
|
126
|
-
<div key=${i} class="item">
|
|
127
|
-
<div class="item-title">${cs.topic || cs.title || 'Session'}</div>
|
|
128
|
-
<div class="item-meta">
|
|
129
|
-
${cs.date ? humanDate(cs.date) : ''}
|
|
130
|
-
${cs.participants ? ' · ' + cs.participants.join(', ') : ''}
|
|
131
|
-
</div>
|
|
132
|
-
</div>
|
|
133
|
-
`)}
|
|
134
|
-
</div>
|
|
135
|
-
</div>
|
|
136
|
-
</section>
|
|
137
|
-
`;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Chains & workstreams
|
|
141
|
-
function ChainsSection() {
|
|
142
|
-
if (!chains.length && !workstreams.length) return null;
|
|
143
|
-
const { cls, label } = chip('active');
|
|
144
|
-
return html`
|
|
145
|
-
<section>
|
|
146
|
-
<h2 class="section-icon"><${Icon} name="link" size=${16}/> Chains & Workstreams</h2>
|
|
147
|
-
<div class="body">
|
|
148
|
-
${chains.length ? html`
|
|
149
|
-
<div style="margin-bottom:var(--space-4);">
|
|
150
|
-
<strong>Chains</strong>
|
|
151
|
-
<div class="phase-list" style="margin-top:var(--space-2);">
|
|
152
|
-
${chains.map((c, i) => html`
|
|
153
|
-
<div key=${i} class="item">
|
|
154
|
-
<div class="item-title">${c.name || c.id || 'Chain'}</div>
|
|
155
|
-
</div>
|
|
156
|
-
`)}
|
|
157
|
-
</div>
|
|
158
|
-
</div>
|
|
159
|
-
` : null}
|
|
160
|
-
${workstreams.length ? html`
|
|
161
|
-
<div>
|
|
162
|
-
<strong>Workstreams</strong>
|
|
163
|
-
<div class="phase-list" style="margin-top:var(--space-2);">
|
|
164
|
-
${workstreams.map((w, i) => {
|
|
165
|
-
const wChip = chip(w.status || 'active');
|
|
166
|
-
return html`
|
|
167
|
-
<div key=${i} class="item">
|
|
168
|
-
<div class="item-title">
|
|
169
|
-
${w.name || w.id || 'Workstream'}
|
|
170
|
-
${' '}<span class=${'status-chip ' + wChip.cls}>● ${wChip.label}</span>
|
|
171
|
-
</div>
|
|
172
|
-
</div>
|
|
173
|
-
`;
|
|
174
|
-
})}
|
|
175
|
-
</div>
|
|
176
|
-
</div>
|
|
177
|
-
` : null}
|
|
178
|
-
</div>
|
|
179
|
-
</section>
|
|
180
|
-
`;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Pending handoff banner
|
|
184
|
-
function HandoffBanner() {
|
|
185
|
-
if (!S.pendingHandoff) return null;
|
|
186
|
-
const ho = S.pendingHandoff;
|
|
187
|
-
const when = ho.ts ? humanDate(ho.ts) : '';
|
|
188
|
-
const summary = ho.summary ? ' — ' + String(ho.summary).slice(0, 120) : '';
|
|
189
|
-
const where = ho.sprint ? ' [sprint ' + ho.sprint + ']' : ho.phase ? ' [phase ' + ho.phase + ']' : '';
|
|
190
|
-
return html`
|
|
191
|
-
<section style="border-left:4px solid var(--accent-orange,#f59e0b);padding-left:var(--space-3);">
|
|
192
|
-
<h2 class="section-icon"><${Icon} name="alert-triangle" size=${16}/> Pending Handoff</h2>
|
|
193
|
-
<div class="body">
|
|
194
|
-
<div>${when}${where}${summary}</div>
|
|
195
|
-
${ho.resume_hint ? html`
|
|
196
|
-
<div style="margin-top:var(--space-2);color:var(--text-secondary);font-size:var(--text-sm);">
|
|
197
|
-
${ho.resume_hint}
|
|
198
|
-
</div>
|
|
199
|
-
` : null}
|
|
200
|
-
<div style="margin-top:var(--space-3);font-size:var(--text-sm);">
|
|
201
|
-
<code>/rcode-resume-work</code>
|
|
202
|
-
</div>
|
|
203
|
-
</div>
|
|
204
|
-
</section>
|
|
205
|
-
`;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Memory bank summary
|
|
209
|
-
function MemorySection() {
|
|
210
|
-
if (!S.memoryBank || !S.memoryBank.active) return null;
|
|
211
|
-
const m = S.memoryBank.active;
|
|
212
|
-
return html`
|
|
213
|
-
<section
|
|
214
|
-
class="item-clickable"
|
|
215
|
-
style="cursor:pointer;"
|
|
216
|
-
onClick=${() => { location.hash = 'memory'; }}
|
|
217
|
-
>
|
|
218
|
-
<h2 class="section-icon"><${Icon} name="brain" size=${16}/> Memory Bank →</h2>
|
|
219
|
-
<div class="body">
|
|
220
|
-
<div class="attr-grid">
|
|
221
|
-
<div class="attr-item">
|
|
222
|
-
<span class="attr-label">active.md</span>
|
|
223
|
-
<span class="attr-value">${m.lines} lines · ${Math.round(m.bytes / 1024 * 10) / 10} KB</span>
|
|
224
|
-
</div>
|
|
225
|
-
<div class="attr-item">
|
|
226
|
-
<span class="attr-label">Updated</span>
|
|
227
|
-
<span class="attr-value">${humanDate(m.updated)}</span>
|
|
228
|
-
</div>
|
|
229
|
-
</div>
|
|
230
|
-
</div>
|
|
231
|
-
</section>
|
|
232
|
-
`;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Last session line
|
|
236
|
-
function LastSession() {
|
|
237
|
-
if (!S.last_session) return null;
|
|
238
|
-
const ls = S.last_session;
|
|
239
|
-
return html`
|
|
240
|
-
<span style="color:var(--text-muted);font-size:var(--text-xs);margin-left:var(--space-3);">
|
|
241
|
-
Last session: ${humanDate(ls.date || ls.timestamp) || '—'}
|
|
242
|
-
</span>
|
|
243
50
|
`;
|
|
244
51
|
}
|
|
245
52
|
|
|
246
53
|
return html`
|
|
247
54
|
<div id="view-overview" class="view active">
|
|
248
|
-
<div class="
|
|
249
|
-
|
|
250
|
-
|
|
55
|
+
<div class="dash-grid">
|
|
56
|
+
<div class="col-4"><${ProgressDonut}/></div>
|
|
57
|
+
<div class="col-4"><${CurrentPhase}/></div>
|
|
58
|
+
<div class="col-4"><${Timeline}/></div>
|
|
59
|
+
|
|
60
|
+
<div class="col-4"><${CompletedTasks}/></div>
|
|
61
|
+
<div class="col-4"><${InProgress}/></div>
|
|
62
|
+
<div class="col-4"><${Blockers}/></div>
|
|
63
|
+
|
|
64
|
+
<div class="col-6"><${RecentDecisions}/></div>
|
|
65
|
+
<div class="col-6"><${ProgressTimeline}/></div>
|
|
251
66
|
</div>
|
|
252
|
-
<${HandoffBanner}/>
|
|
253
|
-
<${SprintProgress}/>
|
|
254
|
-
<${MemorySection}/>
|
|
255
|
-
<${CouncilSessions}/>
|
|
256
|
-
<${ChainsSection}/>
|
|
257
|
-
<${LastSession}/>
|
|
258
|
-
<${CmdHints} hints=${hints}/>
|
|
259
67
|
</div>
|
|
260
68
|
`;
|
|
261
69
|
}
|