@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
|
@@ -1,55 +1,102 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Topbar component —
|
|
2
|
+
* Topbar component — redesigned header chrome to match the mockup.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Public API is UNCHANGED — App.js still calls:
|
|
5
|
+
* <${Topbar} projectName updatedAgo refreshing onRefresh
|
|
6
|
+
* onToggleTheme onToggleSidebar themeLabel />
|
|
5
7
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
<
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
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
|
+
}
|