@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.
Files changed (70) hide show
  1. package/cli/install.js +176 -13
  2. package/cli/lib/config.cjs +4 -2
  3. package/cli/lib/fsutil.cjs +13 -2
  4. package/cli/lib/homedir.cjs +21 -0
  5. package/cli/lib/schemas.cjs +6 -1
  6. package/cli/nuke.js +13 -8
  7. package/cli/postinstall.js +14 -4
  8. package/cli/rcode-slash-router.cjs +118 -0
  9. package/cli/uninstall.js +59 -1
  10. package/cli/update.js +10 -5
  11. package/dist/rcode.js +234 -230
  12. package/package.json +1 -1
  13. package/rcode/references/auto-init-guard.md +2 -2
  14. package/rcode/references/output-format.md +5 -5
  15. package/rcode/skills/actions/2-plan/rcode-create-milestone/steps/step-10-complete.md +1 -1
  16. package/server/dashboard.js +33 -13
  17. package/server/lib/api.js +62 -4
  18. package/server/lib/html/client/agents-data.js +22 -18
  19. package/server/lib/html/client/app.js +3 -0
  20. package/server/lib/html/client/components/AgentCard.js +127 -0
  21. package/server/lib/html/client/components/App.js +104 -39
  22. package/server/lib/html/client/components/CommandPalette.js +133 -0
  23. package/server/lib/html/client/components/FileReader.js +116 -0
  24. package/server/lib/html/client/components/FilterChips.js +94 -0
  25. package/server/lib/html/client/components/NotifyCenter.js +117 -0
  26. package/server/lib/html/client/components/OrchPanel.js +80 -52
  27. package/server/lib/html/client/components/PhaseGraph.js +300 -0
  28. package/server/lib/html/client/components/RejectDialog.js +78 -0
  29. package/server/lib/html/client/components/RunnerPicker.js +190 -0
  30. package/server/lib/html/client/components/Sidebar.js +106 -61
  31. package/server/lib/html/client/components/StatusSummaryBar.js +76 -0
  32. package/server/lib/html/client/components/TaskPipeline.js +83 -0
  33. package/server/lib/html/client/components/Topbar.js +86 -39
  34. package/server/lib/html/client/components/dashboard/Blockers.js +57 -0
  35. package/server/lib/html/client/components/dashboard/CompletedTasks.js +47 -0
  36. package/server/lib/html/client/components/dashboard/CurrentPhase.js +107 -0
  37. package/server/lib/html/client/components/dashboard/InProgress.js +72 -0
  38. package/server/lib/html/client/components/dashboard/ProgressDonut.js +101 -0
  39. package/server/lib/html/client/components/dashboard/ProgressTimeline.js +101 -0
  40. package/server/lib/html/client/components/dashboard/ProjectHealth.js +80 -0
  41. package/server/lib/html/client/components/dashboard/RecentDecisions.js +57 -0
  42. package/server/lib/html/client/components/dashboard/Timeline.js +143 -0
  43. package/server/lib/html/client/components/shared.js +47 -11
  44. package/server/lib/html/client/filter-state.js +72 -0
  45. package/server/lib/html/client/icons-client.js +7 -0
  46. package/server/lib/html/client/notify.js +75 -0
  47. package/server/lib/html/client/orchestrator.js +168 -41
  48. package/server/lib/html/client/preact.js +13 -8
  49. package/server/lib/html/client/store.js +70 -6
  50. package/server/lib/html/client/util.js +78 -0
  51. package/server/lib/html/client/vendor/htm.js +1 -0
  52. package/server/lib/html/client/vendor/preact-hooks.js +2 -0
  53. package/server/lib/html/client/vendor/preact.js +2 -0
  54. package/server/lib/html/client/views/AgentsView.js +144 -51
  55. package/server/lib/html/client/views/FilesView.js +20 -103
  56. package/server/lib/html/client/views/KanbanView.js +40 -21
  57. package/server/lib/html/client/views/MemoryView.js +26 -9
  58. package/server/lib/html/client/views/MilestonesView.js +4 -4
  59. package/server/lib/html/client/views/OrchestrationView.js +154 -19
  60. package/server/lib/html/client/views/OverviewView.js +47 -239
  61. package/server/lib/html/client/views/PhasesView.js +50 -6
  62. package/server/lib/html/client/views/RoadmapView.js +6 -3
  63. package/server/lib/html/client/views/SprintsView.js +50 -6
  64. package/server/lib/html/client/views/TasksView.js +4 -3
  65. package/server/lib/html/client.js +21 -4
  66. package/server/lib/html/css.js +2761 -8
  67. package/server/lib/html/icons.js +7 -0
  68. package/server/lib/html/shell.js +10 -3
  69. package/server/lib/scanner.js +376 -39
  70. package/server/orchestrator.js +346 -7
@@ -1,55 +1,102 @@
1
1
  /**
2
- * Topbar component — brand, live dot, updated-ago, action buttons.
2
+ * Topbar component — redesigned header chrome to match the mockup.
3
3
  *
4
- * Reuses existing CSS classes: header-actions, header-btn, live, hamburger-btn.
4
+ * Public API is UNCHANGED App.js still calls:
5
+ * <${Topbar} projectName updatedAgo refreshing onRefresh
6
+ * onToggleTheme onToggleSidebar themeLabel />
5
7
  *
6
- * Props:
7
- * projectName {string} — shown in the brand subtitle
8
- * updatedAgo {string} text for the "updated N ago" span
9
- * onRefresh {function} called when Refresh button is clicked
10
- * onToggleTheme {function} called when theme button is clicked
11
- * onToggleSidebar {function} called when hamburger is clicked
12
- * themeLabel {string} — 'light' or 'dark' — controls which icon the theme button shows
8
+ * Layout:
9
+ * - hamburger (mobile sidebar toggle)
10
+ * - greeting: "Welcome back, {user.name}! 👋" + subtitle with project name
11
+ * - right group: "Auto-synced {ago}" status dot (click = refresh),
12
+ * [Ask rcode] (primary, runs /rcode-next via orchestrator), [Share]
13
+ * (copy URL + toast), [...] (more / theme toggle)
14
+ *
15
+ * Reads `project { name, user { name } }` from the store; falls back to the
16
+ * projectName prop. No sample data — without a configured user the greeting
17
+ * is generic, and without a project name the subtitle stays generic too.
18
+ * No inline style= attributes — all styling via .tb-* classes in css.js.
13
19
  */
14
20
 
15
21
  import { html } from '../preact.js';
16
22
  import { Icon } from '../icons-client.js';
23
+ import { useStore } from '../store.js';
24
+ import { runCommandFromUI } from '../orchestrator.js';
25
+ import { showToast } from './shared.js';
26
+ import { BlockedBell } from './NotifyCenter.js';
27
+
28
+ /**
29
+ * Ask rcode — reuse the existing orchestrator command runner (token-guarded
30
+ * POST /api/run via window.__ORCH_TOKEN__). "/rcode-next" asks rcode for the
31
+ * suggested next action and streams it into the terminal panel. No new endpoint.
32
+ */
33
+ function askRcode() {
34
+ runCommandFromUI('/rcode-next');
35
+ }
36
+
37
+ /** Share — copy the dashboard URL to the clipboard and confirm via toast. */
38
+ function shareDashboard() {
39
+ const url = location.href;
40
+ if (navigator.clipboard && navigator.clipboard.writeText) {
41
+ navigator.clipboard.writeText(url)
42
+ .then(() => showToast('Dashboard link copied'))
43
+ .catch(() => showToast(url));
44
+ } else {
45
+ showToast(url);
46
+ }
47
+ }
17
48
 
18
49
  export function Topbar({ projectName, updatedAgo, refreshing, onRefresh, onToggleTheme, onToggleSidebar, themeLabel }) {
50
+ const S = useStore();
51
+ const project = (S && S.project) || {};
52
+ const name = project.name || projectName || '';
53
+ const firstName = (project.user && project.user.name) || '';
54
+
19
55
  return html`
20
- <header>
21
- <div class="topbar-start-group">
56
+ <header class="topbar">
57
+ <button
58
+ class="hamburger-btn"
59
+ id="hamburger-btn"
60
+ onClick=${onToggleSidebar}
61
+ aria-label="Toggle menu"
62
+ >
63
+ <span></span><span></span><span></span>
64
+ </button>
65
+
66
+ <div class="tb-greeting">
67
+ <h1 class="tb-welcome">${firstName ? 'Welcome back, ' + firstName + '!' : 'Welcome back!'} <span class="tb-wave" aria-hidden="true">👋</span></h1>
68
+ <p class="tb-sub">${name ? "Here's what's happening with " + name : "Here's what's happening with your project"}</p>
69
+ </div>
70
+
71
+ <div class="tb-actions">
22
72
  <button
23
- class="hamburger-btn"
24
- id="hamburger-btn"
25
- onClick=${onToggleSidebar}
26
- aria-label="Toggle menu"
73
+ class=${'tb-synced' + (refreshing ? ' tb-synced--busy' : '')}
74
+ onClick=${onRefresh}
75
+ title="Click to refresh"
27
76
  >
28
- <span></span><span></span><span></span>
77
+ <span class="tb-dot"></span>
78
+ ${refreshing ? 'Syncing…' : 'Auto-synced ' + (updatedAgo || 'just now')}
79
+ </button>
80
+
81
+ <${BlockedBell} />
82
+
83
+ <button class="tb-btn tb-btn--primary" type="button" onClick=${askRcode} title="Ask rcode for the next action">
84
+ <${Icon} name="brain" size=${15} /> Ask rcode
85
+ </button>
86
+
87
+ <button class="tb-btn tb-btn--share" type="button" onClick=${shareDashboard} title="Copy dashboard link">
88
+ <${Icon} name="link" size=${15} /> Share
89
+ </button>
90
+
91
+ <button
92
+ class="tb-btn tb-btn--icon"
93
+ type="button"
94
+ onClick=${onToggleTheme}
95
+ title=${'More — switch to ' + (themeLabel === 'light' ? 'dark' : 'light') + ' theme'}
96
+ aria-label="More options"
97
+ >
98
+ <span class="tb-kebab">⋯</span>
29
99
  </button>
30
- <div class="brand">
31
- <div class="icon"><${Icon} name="building" size=${16} cls="brand-icon"/></div>
32
- <div>
33
- <h1>Majlis — The Council</h1>
34
- <div class="arabic">مجلس · ${projectName || ''}</div>
35
- </div>
36
- </div>
37
- </div>
38
- <div class="header-actions">
39
- <span class="live" id="live-dot" title="Live"
40
- style=${refreshing ? 'animation-duration:0.7s;background:var(--accent-blue);' : ''}></span>
41
- <span id="updated-ago" class="updated-ago">
42
- ${refreshing ? '⟳ syncing…' : (updatedAgo || 'just now')}
43
- </span>
44
- <button class="header-btn" id="refresh-btn" onClick=${onRefresh}>↺ Refresh</button>
45
- <!-- icon shows TARGET state (not current): dark→sun means "click to go light"; light→moon means "click to go dark" -->
46
- <button class="header-btn" id="theme-btn" onClick=${onToggleTheme} title="Toggle theme"><${Icon} name=${themeLabel === 'light' ? 'moon' : 'sun'} size=${14}/></button>
47
- <button class="header-btn" onClick=${() => {
48
- navigator.clipboard.writeText(location.href);
49
- // Show a toast if available
50
- const toast = document.getElementById('toast');
51
- if (toast) { toast.textContent = 'URL copied!'; toast.classList.add('show'); setTimeout(() => toast.classList.remove('show'), 2500); }
52
- }} title="Copy URL">⎘ Link</button>
53
100
  </div>
54
101
  </header>
55
102
  `;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Blockers — Overview redesign, Row 2 Card 3 (Blockers list by severity).
3
+ *
4
+ * Reads `blockers[{ title, desc, severity }]` from the store (severity one of
5
+ * high | medium | low). Each row: warning icon + bold title, a right-aligned
6
+ * severity pill (High = red, Medium = amber, Low = gray), and a muted one-line
7
+ * description below. "View all" sits top-right in the card header.
8
+ * An empty array is the real, good state — rendered as "No blockers 🎉",
9
+ * never substituted with sample data.
10
+ * See .planning/campaign/DATA-CONTRACT.md. Reads store only — no fetch.
11
+ */
12
+
13
+ import { html } from '../../preact.js';
14
+ import { useStore } from '../../store.js';
15
+ import { rowLink } from '../../util.js';
16
+
17
+ // Severity → pill label (lowercase enum to human-facing label).
18
+ const SEV_LABEL = { high: 'High', medium: 'Medium', low: 'Low' };
19
+
20
+ export function Blockers() {
21
+ const S = useStore();
22
+ const blockers = Array.isArray(S.blockers) ? S.blockers : [];
23
+
24
+ return html`
25
+ <section class="dash-card">
26
+ <div class="bk-head">
27
+ <p class="dash-card-title">Blockers</p>
28
+ <button class="bk-viewall" type="button"
29
+ onClick=${() => { location.hash = 'tasks'; }}>View all</button>
30
+ </div>
31
+ ${blockers.length === 0
32
+ ? html`
33
+ <div class="dash-empty">
34
+ <span class="dash-empty-emoji" aria-hidden="true">🎉</span>
35
+ <span>No blockers</span>
36
+ </div>
37
+ `
38
+ : html`
39
+ <ul class="bk-list">
40
+ ${blockers.map((b) => {
41
+ const sev = SEV_LABEL[b.severity] ? b.severity : 'low';
42
+ return html`
43
+ <li class="bk-row ovr-link" key=${b.title} ...${rowLink('tasks')}>
44
+ <span class=${'bk-icon bk-sev-' + sev} aria-hidden="true">⚠</span>
45
+ <div class="bk-body">
46
+ <p class="bk-title">${b.title}</p>
47
+ <p class="bk-desc">${b.desc}</p>
48
+ </div>
49
+ <span class=${'bk-pill bk-sev-' + sev}>${SEV_LABEL[sev]}</span>
50
+ </li>
51
+ `;
52
+ })}
53
+ </ul>
54
+ `}
55
+ </section>
56
+ `;
57
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * CompletedTasks — Overview redesign, Row 2 Card 1.
3
+ *
4
+ * "Completed Tasks" card with a "View all" link top-right and a list of rows,
5
+ * each = green check icon + task title + right-aligned date.
6
+ *
7
+ * Reads `tasks.completed[{ title, date }]` from the store. Pure — never fetches.
8
+ * An absent or empty slice renders an honest "No completed tasks yet" state —
9
+ * never sample data. See .planning/campaign/DATA-CONTRACT.md.
10
+ */
11
+
12
+ import { html } from '../../preact.js';
13
+ import { useStore } from '../../store.js';
14
+ import { humanDate } from '../../util.js';
15
+
16
+ export function CompletedTasks() {
17
+ const S = useStore();
18
+ const items = (S.tasks && Array.isArray(S.tasks.completed)) ? S.tasks.completed : [];
19
+
20
+ return html`
21
+ <section class="dash-card ct-card">
22
+ <div class="ct-head">
23
+ <p class="dash-card-title">Completed Tasks</p>
24
+ <button class="ct-viewall" onClick=${() => { location.hash = 'tasks'; }}>
25
+ View all
26
+ </button>
27
+ </div>
28
+ ${items.length === 0
29
+ ? html`<p class="dash-card-sub">No completed tasks yet</p>`
30
+ : html`
31
+ <ul class="ct-list">
32
+ ${items.map((t, i) => html`
33
+ <li class="ct-row" key=${t.title + i}>
34
+ <svg class="ct-check" width="16" height="16" viewBox="0 0 24 24"
35
+ fill="none" stroke="currentColor" stroke-width="2.5"
36
+ stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
37
+ <polyline points="20 6 9 17 4 12"/>
38
+ </svg>
39
+ <span class="ct-title">${t.title}</span>
40
+ <span class="ct-date">${humanDate(t.date)}</span>
41
+ </li>
42
+ `)}
43
+ </ul>
44
+ `}
45
+ </section>
46
+ `;
47
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * CurrentPhase — Overview redesign, Row 1 Card 2 (Current Phase stepper).
3
+ *
4
+ * Reads `currentPhase { id, name, status, next, startedDaysAgo, currentTask,
5
+ * milestones[{name,state}] }` from the store (null when the project has no
6
+ * phases; a legacy plain string from old state.json is tolerated). Renders a
7
+ * rocket tile, the phase name with a status pill ("Up Next" when nothing is
8
+ * active and this is the upcoming phase), the in-flight sprint goal as
9
+ * subtitle, a completion line, then a horizontal milestone stepper built from
10
+ * the phase's real sprints.
11
+ *
12
+ * Honest states: null phase → "No active phase" + command hint; a phase with
13
+ * no sprints → "No sprints planned yet" instead of a fabricated stepper.
14
+ * Pure: reads props/store only — never fetches. See DATA-CONTRACT.md.
15
+ */
16
+
17
+ import { html } from '../../preact.js';
18
+ import { useStore } from '../../store.js';
19
+
20
+ // Humanise a free-form status into a short pill label.
21
+ function statusLabel(status) {
22
+ if (!status) return 'Planned';
23
+ return String(status)
24
+ .replace(/[_-]+/g, ' ')
25
+ .replace(/\b\w/g, c => c.toUpperCase());
26
+ }
27
+
28
+ // Subtitle derived from the real phase status — never claims "active" for a
29
+ // phase that isn't. "executing" is what /rcode-execute writes mid-phase.
30
+ function statusSubtitle(status) {
31
+ const s = String(status || '').toLowerCase();
32
+ if (/complete|done/.test(s)) return 'Completed phase';
33
+ if (/active|progress|executing/.test(s)) return 'Active development phase';
34
+ return 'Not started yet';
35
+ }
36
+
37
+ export function CurrentPhase() {
38
+ const S = useStore();
39
+ const cp = S.currentPhase;
40
+ // Tolerate the legacy plain-string shape from old state.json snapshots.
41
+ const phase = cp == null ? null : (typeof cp === 'object' ? cp : { name: String(cp), status: '' });
42
+
43
+ if (!phase || !phase.name) {
44
+ return html`
45
+ <section class="dash-card cp-card">
46
+ <p class="dash-card-title">Current Phase</p>
47
+ <div class="dash-empty">
48
+ <span>No active phase</span>
49
+ <code class="dash-empty-hint">/rcode-plan</code>
50
+ </div>
51
+ </section>
52
+ `;
53
+ }
54
+
55
+ const milestones = Array.isArray(phase.milestones) ? phase.milestones : [];
56
+ const done = milestones.filter(m => m.state === 'done').length;
57
+ const pct = milestones.length
58
+ ? Math.round((done / milestones.length) * 100)
59
+ : null;
60
+ const days = phase.startedDaysAgo;
61
+
62
+ return html`
63
+ <section class="dash-card cp-card">
64
+ <p class="dash-card-title">Current Phase</p>
65
+
66
+ <div class="cp-head">
67
+ <div class="cp-rocket" aria-hidden="true">${phase.next ? '🧭' : '🚀'}</div>
68
+ <div class="cp-headtext">
69
+ <div class="cp-titlerow">
70
+ <span class="cp-name">${phase.name}</span>
71
+ <span class=${'cp-pill' + (phase.next ? ' cp-pill--next' : '')}>
72
+ ● ${phase.next ? 'Up Next' : statusLabel(phase.status)}
73
+ </span>
74
+ </div>
75
+ <p class="cp-sub">${phase.next
76
+ ? 'No phase is active yet — this one is next in line'
77
+ : (phase.currentTask || statusSubtitle(phase.status))}</p>
78
+ </div>
79
+ </div>
80
+
81
+ ${pct != null ? html`
82
+ <p class="cp-progress">
83
+ ${days != null ? html`${days === 0 ? 'Started today' : `Started ${days} day${days === 1 ? '' : 's'} ago`}<span class="cp-dot">•</span>` : null}
84
+ <span class="cp-pct">${done}/${milestones.length} sprints done<span class="cp-dot">•</span>${pct}% complete</span>
85
+ </p>
86
+ ` : null}
87
+
88
+ ${milestones.length
89
+ ? html`
90
+ <ol class="cp-stepper">
91
+ ${milestones.map((m, i) => html`
92
+ <li class=${'cp-step cp-' + (m.state || 'todo')} key=${i}>
93
+ <span class="cp-node">${m.state === 'done' ? '✓' : ''}</span>
94
+ <span class="cp-label">${m.name}</span>
95
+ </li>
96
+ `)}
97
+ </ol>
98
+ `
99
+ : html`
100
+ <div class="dash-empty">
101
+ <span>No sprints planned yet</span>
102
+ <code class="dash-empty-hint">/rcode-sprint-planning</code>
103
+ </div>
104
+ `}
105
+ </section>
106
+ `;
107
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * InProgress — Overview redesign, Row 2 Card 2.
3
+ *
4
+ * "In Progress" card with a "View all" link top-right and a list of rows.
5
+ *
6
+ * Two sources, live first:
7
+ * 1. Live orchestrator sessions (store.activeSessions, status==='running') —
8
+ * pulsing dot, title from storyId (or the command for cmd-* runs),
9
+ * elapsed time since startTime; clicking opens the session's orchestrator
10
+ * panel (existing openOrchPanel mechanism).
11
+ * 2. Scanned tasks from `tasks.inProgress[{ title, pct }]` (pct null → the
12
+ * percent pill is omitted).
13
+ *
14
+ * Pure — never fetches; the 4s session poll keeps the store fresh. An absent
15
+ * or empty slice renders an honest "Nothing in progress" state — never sample
16
+ * data. See .planning/campaign/DATA-CONTRACT.md.
17
+ */
18
+
19
+ import { html } from '../../preact.js';
20
+ import { useStore } from '../../store.js';
21
+ import { orchElapsed, rowLink } from '../../util.js';
22
+ import { openOrchPanel } from '../../orchestrator.js';
23
+ import { pressable } from '../shared.js';
24
+ import { TaskPipeline } from '../TaskPipeline.js';
25
+
26
+ /** Display title for a live session row — command runs show the command. */
27
+ function sessionTitle(s) {
28
+ if (String(s.storyId || '').startsWith('cmd-')) return s.cmd || s.storyId;
29
+ return s.storyId;
30
+ }
31
+
32
+ function LiveRow({ session: s }) {
33
+ return html`
34
+ <li class="ip-row ip-live-row" title=${'Open session ' + s.storyId}
35
+ ...${pressable(() => openOrchPanel(s.storyId))}>
36
+ <span class="live-dot"></span>
37
+ <span class="ip-title ip-live-title">${sessionTitle(s)}</span>
38
+ <span class="ip-live-elapsed">${orchElapsed(s.startTime)}</span>
39
+ </li>
40
+ `;
41
+ }
42
+
43
+ export function InProgress() {
44
+ const S = useStore();
45
+ const items = (S.tasks && Array.isArray(S.tasks.inProgress)) ? S.tasks.inProgress : [];
46
+ const live = (S.activeSessions || []).filter(s => s.status === 'running' || s.status === 'blocked');
47
+
48
+ return html`
49
+ <section class="dash-card ip-card">
50
+ <div class="ip-head">
51
+ <p class="dash-card-title">In Progress</p>
52
+ <button class="ip-viewall" onClick=${() => { location.hash = 'tasks'; }}>
53
+ View all
54
+ </button>
55
+ </div>
56
+ ${live.length === 0 && items.length === 0
57
+ ? html`<p class="dash-card-sub">Nothing in progress</p>`
58
+ : html`
59
+ <ul class="ip-list">
60
+ ${live.map(s => html`<${LiveRow} key=${'live-' + s.storyId} session=${s}/>`)}
61
+ ${items.map((t, i) => html`
62
+ <li class="ip-row ovr-link" key=${t.title + i} ...${rowLink('tasks')}>
63
+ <span class="ip-title">${t.title}</span>
64
+ <${TaskPipeline} task=${t} mini=${true}/>
65
+ ${Number.isFinite(t.pct) ? html`<span class="ip-badge">${t.pct}%</span>` : null}
66
+ </li>
67
+ `)}
68
+ </ul>
69
+ `}
70
+ </section>
71
+ `;
72
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * ProgressDonut — Overview redesign, Row 1 Card 1 (Project Progress donut).
3
+ *
4
+ * Reads the `progress { completed, inProgress, notStarted, total, pct }` slice
5
+ * from the store and renders a teal-gradient donut ring with the percentage
6
+ * centered, a colored-dot legend, and a thin segmented progress bar.
7
+ * See .planning/campaign/DATA-CONTRACT.md. Reads store only — no fetch.
8
+ */
9
+
10
+ import { html } from '../../preact.js';
11
+ import { useStore } from '../../store.js';
12
+
13
+ function pctOf(n, total) {
14
+ return total > 0 ? Math.round((n / total) * 100) : 0;
15
+ }
16
+
17
+ export function ProgressDonut() {
18
+ const S = useStore();
19
+ const p = S.progress;
20
+ // Live orchestrator sessions (derived map, refreshed by the 4s poll).
21
+ const liveCount = Object.keys(S.runningByStory || {}).length;
22
+
23
+ // No tracked work yet (or no project) — honest empty state, no sample donut.
24
+ if (!p || !p.total) {
25
+ return html`
26
+ <section class="dash-card donut-card">
27
+ <p class="dash-card-title">Project Progress</p>
28
+ <div class="dash-empty">
29
+ <span>No tasks tracked yet</span>
30
+ <code class="dash-empty-hint">/rcode-plan</code>
31
+ </div>
32
+ </section>
33
+ `;
34
+ }
35
+ const completed = p.completed ?? 0;
36
+ const inProgress = p.inProgress ?? 0;
37
+ const notStarted = p.notStarted ?? 0;
38
+ const total = p.total ?? (completed + inProgress + notStarted);
39
+ const pct = p.pct ?? pctOf(completed, total);
40
+
41
+ // Donut geometry — track + teal-gradient arc for the completed percentage.
42
+ const r = 52;
43
+ const c = 2 * Math.PI * r;
44
+ const offset = c - (pct / 100) * c;
45
+
46
+ const legend = [
47
+ { label: 'Completed', cls: 'donut-dot--done', pct: pctOf(completed, total) },
48
+ { label: 'In Progress', cls: 'donut-dot--prog', pct: pctOf(inProgress, total) },
49
+ { label: 'Not Started', cls: 'donut-dot--idle', pct: pctOf(notStarted, total) },
50
+ ];
51
+
52
+ // Segmented bar widths — teal (completed) then purple (in progress).
53
+ const tealW = pctOf(completed, total);
54
+ const purpleW = pctOf(inProgress, total);
55
+
56
+ return html`
57
+ <section class="dash-card donut-card">
58
+ <p class="dash-card-title">Project Progress</p>
59
+ <div class="donut-body">
60
+ <div class="donut-ring">
61
+ <svg width="132" height="132" viewBox="0 0 132 132">
62
+ <defs>
63
+ <linearGradient id="donutTeal" x1="0" y1="0" x2="1" y2="1">
64
+ <stop offset="0%" stop-color="var(--dash-teal)"/>
65
+ <stop offset="100%" stop-color="var(--dash-purple)"/>
66
+ </linearGradient>
67
+ </defs>
68
+ <circle cx="66" cy="66" r=${r} fill="none"
69
+ stroke="var(--dash-border)" stroke-width="10"/>
70
+ <circle cx="66" cy="66" r=${r} fill="none"
71
+ stroke="url(#donutTeal)" stroke-width="10" stroke-linecap="round"
72
+ stroke-dasharray=${c} stroke-dashoffset=${offset}
73
+ transform="rotate(-90 66 66)"/>
74
+ </svg>
75
+ <div class="donut-center">
76
+ <span class="donut-pct">${pct}%</span>
77
+ <span class="donut-pct-label">complete</span>
78
+ </div>
79
+ </div>
80
+ <ul class="donut-legend">
81
+ ${legend.map(item => html`
82
+ <li class="donut-legend-row" key=${item.label}>
83
+ <span class=${'donut-dot ' + item.cls}></span>
84
+ <span class="donut-legend-label">${item.label}</span>
85
+ <span class="donut-legend-pct">${item.pct}%</span>
86
+ </li>
87
+ `)}
88
+ </ul>
89
+ </div>
90
+ <p class="donut-summary">
91
+ <strong>${completed}/${total}</strong> tasks completed
92
+ ${liveCount > 0 ? html` <span class="donut-live">· ${liveCount} running now</span>` : null}
93
+ </p>
94
+ <svg class="donut-bar" width="100%" height="6" viewBox="0 0 100 6" preserveAspectRatio="none">
95
+ <rect x="0" y="0" width="100" height="6" rx="3" fill="var(--dash-border)"/>
96
+ <rect x="0" y="0" width=${tealW} height="6" fill="var(--dash-teal)"/>
97
+ <rect x=${tealW} y="0" width=${purpleW} height="6" fill="var(--dash-purple)"/>
98
+ </svg>
99
+ </section>
100
+ `;
101
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * ProgressTimeline — Overview redesign, Row 3 Card 2 (horizontal phases).
3
+ *
4
+ * "Progress Timeline" card + "View full timeline" link. A horizontal track with
5
+ * date ticks across the top and one segment per phase (Planning, Design,
6
+ * Development, Testing, Launch). Each segment shows its date range and a state
7
+ * badge: Completed → green, In Progress → purple, Upcoming → gray.
8
+ * Reads `phases[{ name, range, state }]` from the store. An empty array is
9
+ * real data — rendered as an honest "No phases yet" state with a command hint,
10
+ * never substituted with sample phases.
11
+ * See .planning/campaign/DATA-CONTRACT.md. Reads props/store only — no fetch.
12
+ */
13
+
14
+ import { html } from '../../preact.js';
15
+ import { useStore } from '../../store.js';
16
+ import { rowLink } from '../../util.js';
17
+
18
+ // Map phase state → label + badge/segment modifier.
19
+ function stateMeta(state) {
20
+ const s = String(state || '').toLowerCase();
21
+ if (s === 'done') return { label: 'Completed', mod: 'pt-seg--done' };
22
+ if (s === 'active') return { label: 'In Progress', mod: 'pt-seg--active' };
23
+ return { label: 'Upcoming', mod: 'pt-seg--todo' };
24
+ }
25
+
26
+ // Max segments shown at once — the mockup track is designed for ~5; real
27
+ // projects can have 20+ phases, which would cram into unreadable slivers.
28
+ const MAX_SEGMENTS = 6;
29
+
30
+ /** Window of at most MAX_SEGMENTS phases centered on the active one (or the
31
+ * done/todo boundary when nothing is active), so the card shows where the
32
+ * project currently is. "View full timeline" covers the rest. */
33
+ function visibleWindow(phases) {
34
+ if (phases.length <= MAX_SEGMENTS) return phases;
35
+ let center = phases.findIndex(p => String(p.state).toLowerCase() === 'active');
36
+ if (center === -1) {
37
+ const firstTodo = phases.findIndex(p => String(p.state).toLowerCase() !== 'done');
38
+ center = firstTodo === -1 ? phases.length - 1 : firstTodo;
39
+ }
40
+ let start = Math.max(0, center - Math.floor(MAX_SEGMENTS / 2));
41
+ start = Math.min(start, phases.length - MAX_SEGMENTS);
42
+ return phases.slice(start, start + MAX_SEGMENTS);
43
+ }
44
+
45
+ export function ProgressTimeline() {
46
+ const S = useStore();
47
+ const all = Array.isArray(S.phases) ? S.phases : [];
48
+
49
+ if (!all.length) {
50
+ return html`
51
+ <section class="dash-card">
52
+ <div class="pt-head">
53
+ <p class="dash-card-title">Progress Timeline</p>
54
+ </div>
55
+ <div class="dash-empty">
56
+ <span>No phases yet</span>
57
+ <code class="dash-empty-hint">/rcode-plan</code>
58
+ </div>
59
+ </section>
60
+ `;
61
+ }
62
+
63
+ const phases = visibleWindow(all);
64
+
65
+ // Date ticks across the top — the start of each visible phase range, plus
66
+ // the end of the last range, so the axis brackets the visible window.
67
+ const ticks = phases.map(p => String(p.range || '').split('–')[0].trim());
68
+ const lastEnd = String(phases[phases.length - 1]?.range || '').split('–')[1];
69
+ if (lastEnd) ticks.push(lastEnd.trim());
70
+
71
+ return html`
72
+ <section class="dash-card">
73
+ <div class="pt-head">
74
+ <p class="dash-card-title">Progress Timeline</p>
75
+ <button class="pt-viewall" onClick=${() => { location.hash = 'phases'; }}>
76
+ View full timeline
77
+ </button>
78
+ </div>
79
+
80
+ <div class="pt-ticks">
81
+ ${ticks.map((t, i) => html`<span class="pt-tick" key=${'tick' + i}>${t}</span>`)}
82
+ </div>
83
+
84
+ <div class="pt-track">
85
+ ${phases.map((p, i) => {
86
+ const m = stateMeta(p.state);
87
+ // Segment deep-links to its phase detail; fall back to the list
88
+ // when the phase has no id.
89
+ const target = p.id != null ? 'phases/' + p.id : 'phases';
90
+ return html`
91
+ <div class=${'pt-seg ovr-link ' + m.mod} key=${p.name + i} ...${rowLink(target)}>
92
+ <span class="pt-seg-name">${p.name}</span>
93
+ <span class="pt-seg-range">${p.range || ''}</span>
94
+ <span class="pt-seg-badge">${m.label}</span>
95
+ </div>
96
+ `;
97
+ })}
98
+ </div>
99
+ </section>
100
+ `;
101
+ }