@hanzlaa/rcode 3.4.33 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +6 -6
- package/CONTRIBUTING.md +2 -0
- package/LICENSE +21 -0
- package/README.md +66 -403
- package/cli/doctor.js +87 -1
- package/cli/install.js +122 -31
- package/cli/lib/schemas.cjs +318 -0
- package/cli/postinstall.js +19 -3
- package/dist/rcode.js +316 -23
- package/package.json +14 -4
- package/rihal/agents/rihal-cross-platform-auditor.md +1 -1
- package/rihal/agents/rihal-dep-auditor.md +1 -1
- package/rihal/agents/rihal-docs-auditor.md +3 -145
- package/rihal/agents/rihal-i18n-auditor.md +1 -1
- package/rihal/agents/rihal-nyquist-auditor.md +4 -156
- package/rihal/agents/rihal-observability-auditor.md +1 -1
- package/rihal/bin/rihal-hooks.cjs +394 -4
- package/rihal/bin/rihal-tools.cjs +891 -24
- package/rihal/commands/create-prd.md +18 -0
- package/rihal/commands/execute-milestone.md +18 -0
- package/rihal/commands/plan-milestone.md +18 -0
- package/rihal/commands/scaffold-milestone.md +18 -0
- package/rihal/commands/scaffold-skill.md +18 -0
- package/rihal/references/REFERENCES_INDEX.md +49 -7
- package/rihal/references/agent-contracts.md +10 -0
- package/rihal/references/design-tokens.md +98 -0
- package/rihal/references/docs-auditor-playbook.md +148 -0
- package/rihal/references/git-preflight.md +117 -0
- package/rihal/references/iterative-retrieval.md +85 -0
- package/rihal/references/nyquist-auditor-playbook.md +157 -0
- package/rihal/references/workstream-flag.md +2 -2
- package/rihal/skills/actions/1-analysis/rihal-prfaq/SKILL.md +9 -0
- package/rihal/skills/actions/4-implementation/rihal-checkpoint-preview/SKILL.md +9 -0
- package/rihal/skills/actions/4-implementation/rihal-ci/SKILL.md +4 -0
- package/rihal/skills/actions/4-implementation/rihal-code-review/steps/step-02-review.md +2 -2
- package/rihal/skills/actions/4-implementation/rihal-harden/SKILL.md +4 -0
- package/rihal/skills/actions/4-implementation/rihal-migrate/SKILL.md +4 -0
- package/rihal/skills/agents/haitham-frontend/SKILL.md +2 -0
- package/rihal/templates/settings-hooks.json +39 -0
- package/rihal/workflows/check-todos.md +4 -0
- package/rihal/workflows/code-review-fix.md +4 -3
- package/rihal/workflows/code-review.md +1 -1
- package/rihal/workflows/debug.md +1 -1
- package/rihal/workflows/dev-story.md +4 -0
- package/rihal/workflows/diff.md +2 -2
- package/rihal/workflows/do.md +16 -8
- package/rihal/workflows/docs-update.md +2 -2
- package/rihal/workflows/enable-hooks.md +6 -1
- package/rihal/workflows/execute-milestone.md +139 -0
- package/rihal/workflows/execute-regression-gates.md +1 -1
- package/rihal/workflows/execute-sprint.md +54 -2
- package/rihal/workflows/execute-verify-phase-goal.md +31 -4
- package/rihal/workflows/execute-waves.md +33 -5
- package/rihal/workflows/execute.md +40 -6
- package/rihal/workflows/help.md +1 -1
- package/rihal/workflows/import.md +1 -1
- package/rihal/workflows/lens-audit.md +39 -23
- package/rihal/workflows/list-workspaces.md +1 -1
- package/rihal/workflows/map-codebase.md +4 -4
- package/rihal/workflows/new-milestone.md +18 -1
- package/rihal/workflows/new-project-research.md +53 -1
- package/rihal/workflows/new-workspace.md +1 -1
- package/rihal/workflows/plan-milestone.md +105 -0
- package/rihal/workflows/plan-research-validation.md +1 -1
- package/rihal/workflows/plan-spawn-planner.md +1 -1
- package/rihal/workflows/plan.md +31 -3
- package/rihal/workflows/plant-seed.md +6 -0
- package/rihal/workflows/quick.md +11 -5
- package/rihal/workflows/research-phase.md +24 -0
- package/rihal/workflows/scaffold-milestone.md +60 -0
- package/rihal/workflows/scaffold-skill.md +137 -0
- package/rihal/workflows/scan.md +1 -1
- package/rihal/workflows/session-report.md +43 -3
- package/rihal/workflows/verify-work.md +3 -3
- package/server/dashboard.js +154 -5
- package/server/lib/html/client/agents-data.js +27 -0
- package/server/lib/html/client/app.js +15 -0
- package/server/lib/html/client/components/App.js +211 -0
- package/server/lib/html/client/components/OrchPanel.js +293 -0
- package/server/lib/html/client/components/Sidebar.js +73 -0
- package/server/lib/html/client/components/Topbar.js +53 -0
- package/server/lib/html/client/components/XtermPanel.js +220 -0
- package/server/lib/html/client/components/shared.js +330 -0
- package/server/lib/html/client/icons-client.js +85 -0
- package/server/lib/html/client/orchestrator.js +279 -0
- package/server/lib/html/client/preact.js +34 -0
- package/server/lib/html/client/store.js +91 -0
- package/server/lib/html/client/util.js +186 -0
- package/server/lib/html/client/views/AgentsView.js +83 -0
- package/server/lib/html/client/views/DecisionsView.js +102 -0
- package/server/lib/html/client/views/FilesView.js +223 -0
- package/server/lib/html/client/views/KanbanView.js +236 -0
- package/server/lib/html/client/views/MemoryView.js +157 -0
- package/server/lib/html/client/views/MilestonesView.js +136 -0
- package/server/lib/html/client/views/OrchestrationView.js +167 -0
- package/server/lib/html/client/views/OverviewView.js +221 -0
- package/server/lib/html/client/views/PhasesView.js +184 -0
- package/server/lib/html/client/views/RoadmapView.js +238 -0
- package/server/lib/html/client/views/SprintsView.js +178 -0
- package/server/lib/html/client/views/TasksView.js +148 -0
- package/server/lib/html/client.js +42 -1064
- package/server/lib/html/css.js +2266 -466
- package/server/lib/html/icons.js +68 -0
- package/server/lib/html/shell.js +16 -210
- package/server/lib/scanner.js +109 -0
- package/server/orchestrator.js +362 -0
|
@@ -1,1072 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Client
|
|
3
|
-
*
|
|
2
|
+
* Client JS loader.
|
|
3
|
+
*
|
|
4
|
+
* The dashboard's client-side code lives as plain static files under
|
|
5
|
+
* server/lib/html/client/ and is served verbatim at /js/<name>.js by
|
|
6
|
+
* dashboard.js. This module emits:
|
|
7
|
+
* 1. an inline <script> that injects server-scanned state (window.__S__)
|
|
8
|
+
* and the icon map (window.__ICONS__) for the Preact client
|
|
9
|
+
* 2. a module script tag loading /js/app.js (ESM, deferred until DOM ready)
|
|
10
|
+
*
|
|
11
|
+
* Sprint 31.4: legacy modules (client-render.js, client-kanban.js,
|
|
12
|
+
* client-main.js) deleted — the dashboard is 100% Preact.
|
|
4
13
|
*/
|
|
5
|
-
function renderClientJs(state) {
|
|
6
|
-
const clientData = JSON.stringify({
|
|
7
|
-
phases: state.raw?.phases || [],
|
|
8
|
-
milestone: state.raw?.milestone || '',
|
|
9
|
-
currentPhase: state.raw?.current_phase || null,
|
|
10
|
-
currentSprint: state.raw?.current_sprint|| null,
|
|
11
|
-
decisions: state.raw?.decisions || [],
|
|
12
|
-
blockers: state.raw?.blockers || [],
|
|
13
|
-
council_sessions: state.raw?.council_sessions || [],
|
|
14
|
-
last_session: state.raw?.last_session || null,
|
|
15
|
-
chains: state.raw?.chains || [],
|
|
16
|
-
workstreams: state.raw?.workstreams || [],
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
return `<script>
|
|
20
|
-
// ---- Embedded state ----
|
|
21
|
-
window.__S__ = ${clientData};
|
|
22
|
-
const S = window.__S__;
|
|
23
|
-
const _phases = S.phases || [];
|
|
24
|
-
|
|
25
|
-
// ---- Helpers ----
|
|
26
|
-
function chip(s) {
|
|
27
|
-
const c = (s === 'complete' || s === 'completed' || s === 'done') ? 'complete'
|
|
28
|
-
: (s === 'active' || s === 'in_progress') ? 'active'
|
|
29
|
-
: s === 'blocked' ? 'blocked'
|
|
30
|
-
: s === 'planned' ? 'planned'
|
|
31
|
-
: s === 'todo' ? 'todo' : 'other';
|
|
32
|
-
return '<span class="status-chip ' + c + '">● ' + esc(s) + '</span>';
|
|
33
|
-
}
|
|
34
|
-
function tag(t) { return '<span class="tag">' + esc(t) + '</span>'; }
|
|
35
|
-
function esc(s) { return String(s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); }
|
|
36
|
-
function pct(d, t) { return t > 0 ? Math.round(d/t*100) + '%' : '—'; }
|
|
37
|
-
function pctNum(d, t) { return t > 0 ? Math.round(d/t*100) : 0; }
|
|
38
|
-
function dateStr(s) { return s ? String(s).slice(0,10) : null; }
|
|
39
|
-
function humanDate(s) {
|
|
40
|
-
if (!s) return null;
|
|
41
|
-
try { const d = new Date(s); return d.toLocaleDateString('en-US', {month:'short',day:'numeric',year:'numeric'}); }
|
|
42
|
-
catch { return dateStr(s); }
|
|
43
|
-
}
|
|
44
|
-
function allSprints() {
|
|
45
|
-
return _phases.flatMap(p => (p.sprints||[]).map(s => Object.assign({}, s, {phaseId:p.id, phaseName:p.name})));
|
|
46
|
-
}
|
|
47
|
-
function allTasks() {
|
|
48
|
-
return _phases.flatMap(p => (p.sprints||[]).flatMap(s =>
|
|
49
|
-
(s.stories||[]).map(t => Object.assign({}, t, {sprintId:s.id, phaseId:p.id, phaseName:p.name}))
|
|
50
|
-
));
|
|
51
|
-
}
|
|
52
|
-
function attr(label, val) {
|
|
53
|
-
return '<div class="attr-item"><span class="attr-label">' + label + '</span><span class="attr-value">' + (val||'—') + '</span></div>';
|
|
54
|
-
}
|
|
55
|
-
function breadcrumb(label, hash) {
|
|
56
|
-
return '<div class="breadcrumb"><button class="back-btn" onclick="navTo(\\'' + hash + '\\')">← ' + label + '</button></div>';
|
|
57
|
-
}
|
|
58
|
-
function filterInput(listId) {
|
|
59
|
-
return '<div class="filter-bar"><input class="filter-input" type="text" placeholder="Filter…" oninput="filterItems(this,\\'' + listId + '\\')"></div>';
|
|
60
|
-
}
|
|
61
|
-
function progressBar(done, total) {
|
|
62
|
-
const p = pctNum(done, total);
|
|
63
|
-
return '<div class="progress-bar"><div class="progress-bar-fill" style="width:' + p + '%;' +
|
|
64
|
-
(p >= 100 ? 'background:var(--accent-green)' : p > 50 ? 'background:var(--accent-blue)' : 'background:var(--accent-amber)') +
|
|
65
|
-
'"></div></div>';
|
|
66
|
-
}
|
|
67
|
-
function completionRing(done, total) {
|
|
68
|
-
const p = pctNum(done, total);
|
|
69
|
-
const r = 28, c = 2 * Math.PI * r, offset = c - (p / 100) * c;
|
|
70
|
-
return '<div class="completion-ring"><svg width="64" height="64" viewBox="0 0 64 64">' +
|
|
71
|
-
'<circle cx="32" cy="32" r="' + r + '" fill="none" stroke="var(--border)" stroke-width="4"/>' +
|
|
72
|
-
'<circle cx="32" cy="32" r="' + r + '" fill="none" stroke="var(--accent-green)" stroke-width="4" ' +
|
|
73
|
-
'stroke-dasharray="' + c + '" stroke-dashoffset="' + offset + '" stroke-linecap="round"/>' +
|
|
74
|
-
'</svg><span class="ring-text">' + p + '%</span></div>';
|
|
75
|
-
}
|
|
76
|
-
function showToast(msg) {
|
|
77
|
-
const el = document.getElementById('toast');
|
|
78
|
-
if (!el) return;
|
|
79
|
-
el.textContent = msg; el.classList.add('show');
|
|
80
|
-
setTimeout(() => el.classList.remove('show'), 2000);
|
|
81
|
-
}
|
|
82
|
-
function copyCmd(el) {
|
|
83
|
-
const cmd = el.getAttribute('data-cmd');
|
|
84
|
-
if (!cmd) return;
|
|
85
|
-
navigator.clipboard.writeText(cmd).then(() => showToast('Copied: ' + cmd)).catch(() => {
|
|
86
|
-
const ta = document.createElement('textarea'); ta.value = cmd;
|
|
87
|
-
document.body.appendChild(ta); ta.select(); document.execCommand('copy');
|
|
88
|
-
document.body.removeChild(ta); showToast('Copied: ' + cmd);
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
function cmdHint(cmd, desc) {
|
|
92
|
-
return '<div class="cmd-hint-item" data-cmd="' + esc(cmd) + '" onclick="copyCmd(this)">' +
|
|
93
|
-
'<span class="cmd-text">' + esc(cmd) + '</span>' +
|
|
94
|
-
'<span class="cmd-desc">' + esc(desc) + '</span>' +
|
|
95
|
-
'<span class="cmd-copy">📋</span></div>';
|
|
96
|
-
}
|
|
97
|
-
function cmdAccordion(hints) {
|
|
98
|
-
if (!hints.length) return '';
|
|
99
|
-
return '<details class="cmd-hints"><summary>💡 Commands</summary>' +
|
|
100
|
-
'<div class="cmd-hints-list">' + hints.join('') + '</div></details>';
|
|
101
|
-
}
|
|
102
|
-
function sprintHints(s) {
|
|
103
|
-
const stories = Array.isArray(s.stories) ? s.stories : [];
|
|
104
|
-
const st = s.status || 'planned';
|
|
105
|
-
const sid = s.id || '';
|
|
106
|
-
const pid = s.phaseId || '';
|
|
107
|
-
const h = [];
|
|
108
|
-
if (st === 'completed' || st === 'complete' || st === 'done') {
|
|
109
|
-
h.push(cmdHint('/rihal-verify-work', 'Verify UAT for Sprint ' + sid));
|
|
110
|
-
h.push(cmdHint('/rihal-audit', 'Audit completed Sprint ' + sid));
|
|
111
|
-
h.push(cmdHint('/rihal-session-report', 'Generate session report'));
|
|
112
|
-
h.push(cmdHint('/rihal-code-review', 'Review code from Sprint ' + sid));
|
|
113
|
-
} else if (st === 'active' || st === 'in_progress') {
|
|
114
|
-
h.push(cmdHint('/rihal-progress', 'Check Sprint ' + sid + ' progress'));
|
|
115
|
-
h.push(cmdHint('/rihal-sprint-status', 'Status report for Sprint ' + sid));
|
|
116
|
-
h.push(cmdHint('/rihal-pause-work', 'Pause and save context'));
|
|
117
|
-
} else if (st === 'blocked') {
|
|
118
|
-
h.push(cmdHint('/rihal-debug', 'Debug blocker in Sprint ' + sid));
|
|
119
|
-
h.push(cmdHint('/rihal-correct-course', 'Course-correct Sprint ' + sid));
|
|
120
|
-
} else {
|
|
121
|
-
if (!stories.length) {
|
|
122
|
-
h.push(cmdHint('/rihal-sprint-planning', 'Groom Sprint ' + sid + ' — add stories'));
|
|
123
|
-
h.push(cmdHint('/rihal-create-story', 'Create a story for Sprint ' + sid));
|
|
124
|
-
h.push(cmdHint('/rihal-discuss-phase', 'Discuss approach before planning'));
|
|
125
|
-
} else {
|
|
126
|
-
h.push(cmdHint('/rihal-execute', 'Execute Sprint ' + sid));
|
|
127
|
-
h.push(cmdHint('/rihal-discuss-phase', 'Discuss before executing'));
|
|
128
|
-
h.push(cmdHint('/rihal-sprint-planning', 'Refine Sprint ' + sid + ' plan'));
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return h;
|
|
132
|
-
}
|
|
133
|
-
function phaseHints(p) {
|
|
134
|
-
const sps = Array.isArray(p.sprints) ? p.sprints : [];
|
|
135
|
-
const st = p.status || 'planned';
|
|
136
|
-
const pid = p.id || '';
|
|
137
|
-
const h = [];
|
|
138
|
-
if (st === 'completed' || st === 'complete' || st === 'done') {
|
|
139
|
-
h.push(cmdHint('/rihal-validate-phase', 'Validate Phase ' + pid + ' deliverables'));
|
|
140
|
-
h.push(cmdHint('/rihal-audit', 'Audit Phase ' + pid + ' completion'));
|
|
141
|
-
h.push(cmdHint('/rihal-code-review', 'Review Phase ' + pid + ' code'));
|
|
142
|
-
} else if (st === 'active' || st === 'in_progress') {
|
|
143
|
-
h.push(cmdHint('/rihal-progress', 'Check Phase ' + pid + ' progress'));
|
|
144
|
-
h.push(cmdHint('/rihal-sprint-status', 'Current sprint status'));
|
|
145
|
-
h.push(cmdHint('/rihal-code-review', 'Review code in Phase ' + pid));
|
|
146
|
-
} else {
|
|
147
|
-
if (!sps.length) {
|
|
148
|
-
h.push(cmdHint('/rihal-plan', 'Create sprint plan for Phase ' + pid));
|
|
149
|
-
h.push(cmdHint('/rihal-discuss-phase', 'Discuss Phase ' + pid + ' approach'));
|
|
150
|
-
h.push(cmdHint('/rihal-research-phase', 'Research Phase ' + pid + ' before planning'));
|
|
151
|
-
} else {
|
|
152
|
-
h.push(cmdHint('/rihal-execute', 'Start executing Phase ' + pid));
|
|
153
|
-
h.push(cmdHint('/rihal-sprint-planning', 'Plan next sprint in Phase ' + pid));
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
return h;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// ---- Entity cards ----
|
|
160
|
-
function phaseCard(p) {
|
|
161
|
-
const sps = p.sprints || [];
|
|
162
|
-
const stories = sps.flatMap(s => s.stories || []);
|
|
163
|
-
const done = stories.filter(t => t.status === 'done' || t.status === 'completed').length;
|
|
164
|
-
const isCur = String(p.id) === String(S.currentPhase);
|
|
165
|
-
return '<div class="item item-clickable" onclick="navTo(\\'phases/' + p.id + '\\')"' +
|
|
166
|
-
(isCur ? ' style="border-left-color:var(--accent-amber)"' : '') + '>' +
|
|
167
|
-
'<div class="item-title">Phase ' + esc(p.id) + ' — ' + esc(p.name) +
|
|
168
|
-
(isCur ? tag('current') : '') + chip(p.status) + '</div>' +
|
|
169
|
-
'<div class="item-meta">' + tag(sps.length + ' sprint' + (sps.length!==1?'s':'')) +
|
|
170
|
-
tag(done + '/' + stories.length + ' tasks') +
|
|
171
|
-
(stories.length > 0 ? tag(pct(done,stories.length) + ' done') : '') +
|
|
172
|
-
(p.completed_at ? ' <span style="color:var(--text-muted);font-size:var(--text-xs);">Done ' + humanDate(p.completed_at) + '</span>' : '') +
|
|
173
|
-
'</div>' +
|
|
174
|
-
(stories.length > 0 ? '<div style="margin-top:6px;">' + progressBar(done, stories.length) + '</div>' : '') +
|
|
175
|
-
(sps[0]?.goal ? '<div style="color:var(--text-secondary);font-size:var(--text-sm);margin-top:4px;">' + esc(sps[0].goal) + '</div>' : '') +
|
|
176
|
-
'</div>';
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function sprintCard(s) {
|
|
180
|
-
const stories = s.stories || [];
|
|
181
|
-
const done = stories.filter(t => t.status === 'done' || t.status === 'completed').length;
|
|
182
|
-
const isCur = s.id === S.currentSprint;
|
|
183
|
-
const phaseId = s.phaseId || s.id || '';
|
|
184
|
-
return '<div class="item item-clickable' + (isCur ? ' sprint-current' : '') + '" onclick="navTo(\\'sprints/' + s.id + '\\')"' +
|
|
185
|
-
(isCur ? ' style="border-left-color:var(--accent-amber);background:rgba(245,158,11,0.04)"' : '') + '>' +
|
|
186
|
-
'<div class="item-title">Sprint ' + esc(s.id) + ' — ' + esc(s.goal || 'No goal') +
|
|
187
|
-
(isCur ? tag('current') : '') + chip(s.status) + '</div>' +
|
|
188
|
-
'<div class="item-meta">' +
|
|
189
|
-
(s.phaseId ? tag('Phase ' + s.phaseId) : '') +
|
|
190
|
-
tag(done + '/' + stories.length + ' tasks') +
|
|
191
|
-
(s.velocity_target != null ? tag('Target: ' + s.velocity_target + 'pts') : '') +
|
|
192
|
-
(s.velocity_actual != null ? tag('Actual: ' + s.velocity_actual + 'pts') : '') + '</div>' +
|
|
193
|
-
'<div style="margin-top:6px;">' + progressBar(done, stories.length) + '</div>' +
|
|
194
|
-
(stories.length === 0 ? '<div class="empty-action" style="margin-top:var(--space-2);font-size:var(--text-xs);">No tasks — run <code>/rihal-plan ' + esc(phaseId) + '</code> to populate</div>' : '') +
|
|
195
|
-
(s.started_at ? '<div style="color:var(--text-muted);font-size:var(--text-xs);margin-top:4px;">' +
|
|
196
|
-
humanDate(s.started_at) + (s.completed_at ? ' → ' + humanDate(s.completed_at) : ' → ongoing') + '</div>' : '') +
|
|
197
|
-
'</div>';
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function taskCard(t) {
|
|
201
|
-
const done = t.status === 'done' || t.status === 'completed';
|
|
202
|
-
const tid = 'task-' + (t.id || t.title || '').replace(/[^a-zA-Z0-9]/g, '-').slice(0, 40) + '-' + Math.random().toString(36).slice(2, 6);
|
|
203
|
-
// Build detail rows from all available context
|
|
204
|
-
var rows = '';
|
|
205
|
-
if (t.id) rows += '<div class="task-detail-row"><strong>ID:</strong> <code>' + esc(t.id) + '</code></div>';
|
|
206
|
-
if (t.points) rows += '<div class="task-detail-row"><strong>Points:</strong> ' + t.points + '</div>';
|
|
207
|
-
rows += '<div class="task-detail-row"><strong>Status:</strong> ' + chip(t.status || 'unknown') + '</div>';
|
|
208
|
-
if (t.sprintId) rows += '<div class="task-detail-row"><strong>Sprint:</strong> ' + esc(t.sprintId) + '</div>';
|
|
209
|
-
if (t.sprintGoal) rows += '<div class="task-detail-row"><strong>Sprint Goal:</strong> ' + esc(t.sprintGoal) + '</div>';
|
|
210
|
-
if (t.phaseId) rows += '<div class="task-detail-row"><strong>Phase:</strong> P' + esc(t.phaseId) + (t.phaseName ? ' — ' + esc(t.phaseName) : '') + '</div>';
|
|
211
|
-
if (t.acceptance) rows += '<div class="task-detail-row"><strong>Acceptance:</strong> ' + esc(t.acceptance) + '</div>';
|
|
212
|
-
if (t.assignee) rows += '<div class="task-detail-row"><strong>Assignee:</strong> ' + esc(t.assignee) + '</div>';
|
|
213
|
-
// Context-aware commands for this specific task
|
|
214
|
-
var cmds = '';
|
|
215
|
-
if (t.id) {
|
|
216
|
-
var taskCmds = [];
|
|
217
|
-
if (!done) {
|
|
218
|
-
taskCmds.push(cmdHint('/rihal-dev-story ' + t.id, 'Implement this story'));
|
|
219
|
-
taskCmds.push(cmdHint('/rihal-create-story ' + (t.sprintId || ''), 'Add related story'));
|
|
220
|
-
} else {
|
|
221
|
-
taskCmds.push(cmdHint('/rihal-verify-work ' + t.id, 'Verify this story'));
|
|
222
|
-
taskCmds.push(cmdHint('/rihal-code-review ' + t.id, 'Review code for this story'));
|
|
223
|
-
}
|
|
224
|
-
if (t.sprintId) {
|
|
225
|
-
taskCmds.push(cmdHint('/rihal-sprint-status ' + t.sprintId, 'Sprint ' + t.sprintId + ' status'));
|
|
226
|
-
}
|
|
227
|
-
cmds = '<div class="task-detail-cmds">' + taskCmds.join('') + '</div>';
|
|
228
|
-
}
|
|
229
|
-
return '<div class="item item-clickable" data-status="' + (t.status||'') + '" style="' + (done ? 'opacity:.65' : '') + '"' +
|
|
230
|
-
' onclick="toggleTaskDetail(\\'' + tid + '\\')">' +
|
|
231
|
-
'<div class="item-title" style="' + (done ? 'text-decoration:line-through' : '') + '">' +
|
|
232
|
-
(done ? '✓ ' : '') + esc(t.title) + chip(t.status) +
|
|
233
|
-
'<span class="task-expand-icon" id="icon-' + tid + '">▶</span></div>' +
|
|
234
|
-
'<div class="item-meta">' +
|
|
235
|
-
(t.points ? tag(t.points + 'pts') : '') +
|
|
236
|
-
(t.id ? tag(t.id) : '') +
|
|
237
|
-
(t.sprintId ? tag('Sprint ' + t.sprintId) : '') +
|
|
238
|
-
(t.phaseId ? tag('Phase ' + t.phaseId) : '') + '</div>' +
|
|
239
|
-
'<div class="task-detail" id="' + tid + '" style="display:none;">' +
|
|
240
|
-
rows + cmds + '</div>' +
|
|
241
|
-
'</div>';
|
|
242
|
-
}
|
|
243
|
-
function toggleTaskDetail(id) {
|
|
244
|
-
const el = document.getElementById(id);
|
|
245
|
-
const icon = document.getElementById('icon-' + id);
|
|
246
|
-
if (!el) return;
|
|
247
|
-
const open = el.style.display !== 'none';
|
|
248
|
-
el.style.display = open ? 'none' : 'block';
|
|
249
|
-
if (icon) icon.textContent = open ? '▶' : '▼';
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// ---- View renderers ----
|
|
253
|
-
function renderOverview() {
|
|
254
|
-
// #268: current sprint progress bar on overview
|
|
255
|
-
const sprints = allSprints();
|
|
256
|
-
const curSprint = sprints.find(s => s.id === S.currentSprint);
|
|
257
|
-
let sprintProgressHtml = '';
|
|
258
|
-
if (curSprint) {
|
|
259
|
-
const sts = curSprint.stories || [];
|
|
260
|
-
const d = sts.filter(t => t.status === 'done' || t.status === 'completed').length;
|
|
261
|
-
sprintProgressHtml = '<section><h2>⚡ Current Sprint — ' + esc(curSprint.id) + '</h2><div class="body">' +
|
|
262
|
-
'<div style="margin-bottom:8px;font-size:var(--text-sm);color:var(--text-secondary);">' + esc(curSprint.goal || '') + '</div>' +
|
|
263
|
-
'<div style="display:flex;align-items:center;gap:var(--space-3);">' +
|
|
264
|
-
'<div style="flex:1;">' + progressBar(d, sts.length) + '</div>' +
|
|
265
|
-
'<span style="font-size:var(--text-sm);font-weight:600;">' + d + '/' + sts.length + ' (' + pct(d,sts.length) + ')</span>' +
|
|
266
|
-
'</div></div></section>';
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// #267: velocity sparkline
|
|
270
|
-
let velocityHtml = '';
|
|
271
|
-
const completedSprints = sprints.filter(s => s.velocity_actual != null);
|
|
272
|
-
if (completedSprints.length > 1) {
|
|
273
|
-
const vals = completedSprints.map(s => s.velocity_actual);
|
|
274
|
-
const max = Math.max(...vals, 1);
|
|
275
|
-
const w = 200, h = 40, step = w / (vals.length - 1);
|
|
276
|
-
const points = vals.map((v, i) => (i * step) + ',' + (h - (v / max) * h));
|
|
277
|
-
velocityHtml = '<div class="stat"><div class="label">Sprint Velocity</div>' +
|
|
278
|
-
'<svg width="' + w + '" height="' + (h+4) + '" style="margin-top:8px;">' +
|
|
279
|
-
'<polyline points="' + points.join(' ') + '" fill="none" stroke="var(--accent-blue)" stroke-width="2"/>' +
|
|
280
|
-
'</svg><div class="sub">Last ' + vals.length + ' sprints</div></div>';
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// #269: council sessions
|
|
284
|
-
let councilHtml = '';
|
|
285
|
-
if (Array.isArray(S.council_sessions) && S.council_sessions.length) {
|
|
286
|
-
councilHtml = '<section><h2>🏛 Council Sessions</h2><div class="body"><div class="phase-list">' +
|
|
287
|
-
S.council_sessions.slice(-5).reverse().map(cs =>
|
|
288
|
-
'<div class="item"><div class="item-title">' + esc(cs.topic || cs.title || 'Session') + '</div>' +
|
|
289
|
-
'<div class="item-meta">' + (cs.date ? humanDate(cs.date) : '') +
|
|
290
|
-
(cs.participants ? ' · ' + esc(cs.participants.join(', ')) : '') + '</div></div>'
|
|
291
|
-
).join('') + '</div></div></section>';
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// #271: last session
|
|
295
|
-
let lastSessionHtml = '';
|
|
296
|
-
if (S.last_session) {
|
|
297
|
-
const ls = S.last_session;
|
|
298
|
-
lastSessionHtml = '<span style="color:var(--text-muted);font-size:var(--text-xs);margin-left:var(--space-3);">' +
|
|
299
|
-
'Last session: ' + (humanDate(ls.date || ls.timestamp) || '—') + '</span>';
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// #270: chains/workstreams
|
|
303
|
-
let chainsHtml = '';
|
|
304
|
-
const chains = S.chains || [];
|
|
305
|
-
const workstreams = S.workstreams || [];
|
|
306
|
-
if (chains.length || workstreams.length) {
|
|
307
|
-
chainsHtml = '<section><h2>🔗 Chains & Workstreams</h2><div class="body">';
|
|
308
|
-
if (chains.length) {
|
|
309
|
-
chainsHtml += '<div style="margin-bottom:var(--space-4);"><strong>Chains</strong><div class="phase-list" style="margin-top:var(--space-2);">' +
|
|
310
|
-
chains.map(c => '<div class="item"><div class="item-title">' + esc(c.name || c.id || 'Chain') + '</div></div>').join('') + '</div></div>';
|
|
311
|
-
}
|
|
312
|
-
if (workstreams.length) {
|
|
313
|
-
chainsHtml += '<div><strong>Workstreams</strong><div class="phase-list" style="margin-top:var(--space-2);">' +
|
|
314
|
-
workstreams.map(w => '<div class="item"><div class="item-title">' + esc(w.name || w.id || 'Workstream') + ' ' + chip(w.status || 'active') + '</div></div>').join('') + '</div></div>';
|
|
315
|
-
}
|
|
316
|
-
chainsHtml += '</div></section>';
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const el = document.getElementById('view-overview-dynamic');
|
|
320
|
-
// Overview hints
|
|
321
|
-
var oHints = [cmdHint('/rihal-next', 'What should I do next?'), cmdHint('/rihal-status', 'Quick project status'), cmdHint('/rihal-council', 'Ask the team a question')];
|
|
322
|
-
if (curSprint) { oHints = sprintHints(curSprint).concat(oHints); }
|
|
323
|
-
if (el) el.innerHTML = sprintProgressHtml + velocityHtml + councilHtml + chainsHtml + lastSessionHtml + cmdAccordion(oHints);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function renderRoadmap() {
|
|
327
|
-
const ms = S.milestone || 'M1';
|
|
328
|
-
const totalStories = allTasks();
|
|
329
|
-
const doneStories = totalStories.filter(t => t.status === 'done' || t.status === 'completed');
|
|
330
|
-
let h = '<div class="view-title">Roadmap</div>';
|
|
331
|
-
// #273: filter
|
|
332
|
-
h += '<div class="filter-bar"><input class="filter-input" type="text" placeholder="Filter roadmap…" id="roadmap-filter" oninput="filterRoadmap(this.value)"></div>';
|
|
333
|
-
h += '<div class="tree-container" id="roadmap-tree">';
|
|
334
|
-
h += '<div class="tree-node tree-ms"><div class="tree-row tree-header" onclick="toggleNode(this)">';
|
|
335
|
-
h += '<span class="tree-chevron">▼</span><span class="tree-icon">🎯</span>';
|
|
336
|
-
h += '<span class="tree-label">' + esc(ms) + '</span>';
|
|
337
|
-
h += '<span class="tree-badge">' + _phases.length + ' phases · ' + doneStories.length + '/' + totalStories.length + ' tasks</span></div>';
|
|
338
|
-
h += '<div class="tree-children">';
|
|
339
|
-
for (const p of _phases) {
|
|
340
|
-
const sps = p.sprints || [];
|
|
341
|
-
const pStories = sps.flatMap(s => s.stories||[]);
|
|
342
|
-
const pDone = pStories.filter(t => t.status==='done'||t.status==='completed').length;
|
|
343
|
-
// #274: phase nodes navigate to phase detail
|
|
344
|
-
h += '<div class="tree-node" data-filter-text="' + esc(p.name).toLowerCase() + '"><div class="tree-row" onclick="toggleNode(this)">';
|
|
345
|
-
h += '<span class="tree-chevron">▶</span><span class="tree-icon">📋</span>';
|
|
346
|
-
h += '<span class="tree-label" ondblclick="navTo(\\'phases/' + p.id + '\\');event.stopPropagation();">P' + esc(p.id) + ' — ' + esc(p.name) + '</span>' + chip(p.status);
|
|
347
|
-
// #276: inline mini progress bar
|
|
348
|
-
const pp = pctNum(pDone, pStories.length);
|
|
349
|
-
h += '<span style="width:60px;display:inline-block;margin:0 8px;"><div class="progress-bar" style="height:4px;"><div class="progress-bar-fill" style="width:' + pp + '%;height:100%;"></div></div></span>';
|
|
350
|
-
h += '<span class="tree-badge">' + sps.length + ' sprints · ' + pDone + '/' + pStories.length + '</span></div>';
|
|
351
|
-
// #272: start collapsed
|
|
352
|
-
h += '<div class="tree-children" style="display:none">';
|
|
353
|
-
for (const s of sps) {
|
|
354
|
-
const sts = s.stories || [];
|
|
355
|
-
const sDone = sts.filter(t => t.status==='done'||t.status==='completed').length;
|
|
356
|
-
// #275: sprint nodes link to file
|
|
357
|
-
h += '<div class="tree-node"><div class="tree-row" onclick="toggleNode(this)">';
|
|
358
|
-
h += '<span class="tree-chevron">▶</span><span class="tree-icon">⚡</span>';
|
|
359
|
-
h += '<span class="tree-label">Sprint ' + esc(s.id) + ' — ' + esc(s.goal||'No goal') + '</span>' + chip(s.status);
|
|
360
|
-
h += '<span class="tree-badge">' + sDone + '/' + sts.length + '</span></div>';
|
|
361
|
-
h += '<div class="tree-children" style="display:none">';
|
|
362
|
-
for (const t of sts) {
|
|
363
|
-
const td = t.status==='done'||t.status==='completed';
|
|
364
|
-
h += '<div class="tree-node task-leaf"><div class="tree-row">';
|
|
365
|
-
h += '<span class="tree-icon">' + (td?'✓':'○') + '</span>';
|
|
366
|
-
h += '<span class="tree-label" style="' + (td?'opacity:.6;text-decoration:line-through':'') + '">' + esc(t.title) + '</span>';
|
|
367
|
-
h += chip(t.status) + (t.points ? '<span class="tree-badge">' + t.points + 'pts</span>' : '');
|
|
368
|
-
h += '</div></div>';
|
|
369
|
-
}
|
|
370
|
-
if (!sts.length) h += '<div style="color:var(--text-muted);font-size:var(--text-xs);padding:var(--space-2) var(--space-6);">No tasks</div>';
|
|
371
|
-
h += '</div></div>';
|
|
372
|
-
}
|
|
373
|
-
if (!sps.length) h += '<div style="color:var(--text-muted);font-size:var(--text-xs);padding:var(--space-2) var(--space-6);">No sprints</div>';
|
|
374
|
-
h += '</div></div>';
|
|
375
|
-
}
|
|
376
|
-
h += '</div></div></div>';
|
|
377
|
-
// Roadmap hints
|
|
378
|
-
var rmHints = [cmdHint('/rihal-add-phase', 'Add a new phase'), cmdHint('/rihal-milestone-summary', 'View milestone summary'), cmdHint('/rihal-new-milestone', 'Start a new milestone')];
|
|
379
|
-
var allPDone = _phases.length > 0 && _phases.every(ph => ph.status === 'complete' || ph.status === 'completed' || ph.status === 'done');
|
|
380
|
-
if (allPDone) { rmHints.push(cmdHint('/rihal-audit-milestone', 'Audit milestone completion')); rmHints.push(cmdHint('/rihal-complete-milestone', 'Complete and archive milestone')); }
|
|
381
|
-
document.getElementById('view-roadmap').innerHTML = h + cmdAccordion(rmHints);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function filterRoadmap(q) {
|
|
385
|
-
q = q.toLowerCase().trim();
|
|
386
|
-
document.querySelectorAll('#roadmap-tree .tree-node[data-filter-text]').forEach(n => {
|
|
387
|
-
n.style.display = !q || n.dataset.filterText.includes(q) ? '' : 'none';
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function renderMilestones(subId) {
|
|
392
|
-
const el = document.getElementById('view-milestones');
|
|
393
|
-
const ms = S.milestone || 'M1';
|
|
394
|
-
if (subId) {
|
|
395
|
-
const doneP = _phases.filter(p => p.status==='complete'||p.status==='completed').length;
|
|
396
|
-
const total = allTasks(), done = total.filter(t => t.status==='done'||t.status==='completed');
|
|
397
|
-
// #278: velocity history
|
|
398
|
-
const sprints = allSprints().filter(s => s.velocity_actual != null);
|
|
399
|
-
let velocityHtml = '';
|
|
400
|
-
if (sprints.length) {
|
|
401
|
-
velocityHtml = '<div class="view-title" style="margin-top:var(--space-6)">Velocity History</div>';
|
|
402
|
-
const maxV = Math.max(...sprints.map(s => Math.max(s.velocity_actual||0, s.velocity_target||0)), 1);
|
|
403
|
-
velocityHtml += '<div style="max-width:600px;">' + sprints.map(s =>
|
|
404
|
-
'<div class="velocity-bar">' +
|
|
405
|
-
'<div class="velocity-bar-label">S' + esc(s.id) + '</div>' +
|
|
406
|
-
'<div class="velocity-bar-track">' +
|
|
407
|
-
'<div class="velocity-bar-fill" style="width:' + ((s.velocity_actual||0)/maxV*100) + '%;background:var(--accent-blue);"></div>' +
|
|
408
|
-
'</div>' +
|
|
409
|
-
'<div class="velocity-bar-val">' + (s.velocity_actual||0) + '/' + (s.velocity_target||'—') + '</div>' +
|
|
410
|
-
'</div>'
|
|
411
|
-
).join('') + '</div>';
|
|
412
|
-
}
|
|
413
|
-
// #279: phase timeline
|
|
414
|
-
let timelineHtml = '';
|
|
415
|
-
const phasesWithDates = _phases.filter(p => (p.sprints||[]).some(s => s.started_at));
|
|
416
|
-
if (phasesWithDates.length) {
|
|
417
|
-
timelineHtml = '<div class="view-title" style="margin-top:var(--space-6)">Phase Timeline</div>' +
|
|
418
|
-
'<div class="phase-list">' + phasesWithDates.map(p => {
|
|
419
|
-
const sps = p.sprints || [];
|
|
420
|
-
const startDates = sps.map(s => s.started_at).filter(Boolean).sort();
|
|
421
|
-
const endDates = sps.map(s => s.completed_at).filter(Boolean).sort().reverse();
|
|
422
|
-
return '<div class="item"><div class="item-title">P' + esc(p.id) + ' — ' + esc(p.name) + ' ' + chip(p.status) + '</div>' +
|
|
423
|
-
'<div class="item-meta">' + (startDates[0] ? humanDate(startDates[0]) : '?') + ' → ' +
|
|
424
|
-
(endDates[0] ? humanDate(endDates[0]) : 'ongoing') + '</div></div>';
|
|
425
|
-
}).join('') + '</div>';
|
|
426
|
-
}
|
|
427
|
-
// #280: completion ring
|
|
428
|
-
el.innerHTML = breadcrumb('Milestones','milestones') +
|
|
429
|
-
'<div class="entity-header"><div style="display:flex;align-items:center;gap:var(--space-6);"><div>' +
|
|
430
|
-
'<div class="entity-title">🎯 ' + esc(ms) + '</div></div>' +
|
|
431
|
-
completionRing(done.length, total.length) + '</div>' +
|
|
432
|
-
'<div class="attr-grid">' +
|
|
433
|
-
attr('Total Phases', _phases.length) + attr('Completed Phases', doneP) +
|
|
434
|
-
attr('Current Phase', S.currentPhase||'—') + attr('Current Sprint', S.currentSprint||'—') +
|
|
435
|
-
attr('Tasks Done', done.length + '/' + total.length) +
|
|
436
|
-
attr('Progress', pct(done.length, total.length)) + '</div></div>' +
|
|
437
|
-
velocityHtml + timelineHtml +
|
|
438
|
-
'<div class="view-title" style="margin-top:var(--space-6)">Phases under this milestone</div>' +
|
|
439
|
-
'<div class="phase-list">' + _phases.map(phaseCard).join('') + '</div>';
|
|
440
|
-
} else {
|
|
441
|
-
const total = allTasks(), done = total.filter(t => t.status==='done'||t.status==='completed');
|
|
442
|
-
el.innerHTML = '<div class="view-title">Milestones</div>' +
|
|
443
|
-
'<div class="phase-list"><div class="item item-clickable" onclick="navTo(\\'milestones/M1\\')">' +
|
|
444
|
-
'<div style="display:flex;align-items:center;gap:var(--space-4);">' +
|
|
445
|
-
completionRing(done.length, total.length) +
|
|
446
|
-
'<div><div class="item-title">🎯 ' + esc(ms) + '</div>' +
|
|
447
|
-
'<div class="item-meta">' + tag(_phases.length + ' phases') + tag(allSprints().length + ' sprints') +
|
|
448
|
-
tag(done.length + '/' + total.length + ' tasks done') + tag(pct(done.length,total.length) + ' complete') + '</div></div>' +
|
|
449
|
-
'</div></div></div>';
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
function renderPhases(subId) {
|
|
454
|
-
const el = document.getElementById('view-phases');
|
|
455
|
-
if (subId) {
|
|
456
|
-
const p = _phases.find(ph => String(ph.id) === String(subId) || String(ph.number) === String(subId));
|
|
457
|
-
// Fix #319: guard against missing sprints key
|
|
458
|
-
if (!p) { el.innerHTML = breadcrumb('Phases','phases') + '<div class="empty">Phase not found.</div>'; return; }
|
|
459
|
-
const sps = Array.isArray(p.sprints) ? p.sprints : [];
|
|
460
|
-
const stories = sps.flatMap(s => Array.isArray(s.stories) ? s.stories : []);
|
|
461
|
-
const done = stories.filter(t => t.status==='done'||t.status==='completed').length;
|
|
462
|
-
// #284: velocity bars
|
|
463
|
-
let velocityHtml = '';
|
|
464
|
-
const sprintsWithVel = sps.filter(s => s.velocity_actual != null || s.velocity_target != null);
|
|
465
|
-
if (sprintsWithVel.length) {
|
|
466
|
-
const maxV = Math.max(...sprintsWithVel.map(s => Math.max(s.velocity_actual||0, s.velocity_target||0)), 1);
|
|
467
|
-
velocityHtml = '<div class="view-title" style="margin-top:var(--space-6)">Sprint Velocity</div>' +
|
|
468
|
-
'<div style="max-width:600px;">' + sprintsWithVel.map(s =>
|
|
469
|
-
'<div class="velocity-bar">' +
|
|
470
|
-
'<div class="velocity-bar-label">S' + esc(s.id) + '</div>' +
|
|
471
|
-
'<div class="velocity-bar-track">' +
|
|
472
|
-
'<div class="velocity-bar-fill" style="width:' + ((s.velocity_actual||0)/maxV*100) + '%;"></div>' +
|
|
473
|
-
'</div>' +
|
|
474
|
-
'<div class="velocity-bar-val">' + (s.velocity_actual||0) + '/' + (s.velocity_target||'—') + '</div></div>'
|
|
475
|
-
).join('') + '</div>';
|
|
476
|
-
}
|
|
477
|
-
el.innerHTML = breadcrumb('All Phases','phases') +
|
|
478
|
-
'<div class="entity-header"><div class="entity-title">📋 Phase ' + esc(p.id) + ' — ' + esc(p.name) + '</div>' +
|
|
479
|
-
'<div class="attr-grid">' +
|
|
480
|
-
attr('Status', chip(p.status)) + attr('Sprints', sps.length) +
|
|
481
|
-
attr('Tasks Done', done + '/' + stories.length) + attr('Progress', pct(done,stories.length)) +
|
|
482
|
-
// #282: completed_at date
|
|
483
|
-
(p.completed_at ? attr('Completed', humanDate(p.completed_at)) : '') + '</div></div>' +
|
|
484
|
-
'<div style="margin-bottom:var(--space-4);">' + progressBar(done, stories.length) + '</div>' +
|
|
485
|
-
// #283: View plan file button
|
|
486
|
-
'<div style="margin-bottom:var(--space-6);"><button class="back-btn" onclick="viewPlanFile(\\'' + esc(p.id) + '\\')">📄 View plan file →</button></div>' +
|
|
487
|
-
velocityHtml +
|
|
488
|
-
'<div class="view-title" style="margin-top:var(--space-6)">Sprints</div>' +
|
|
489
|
-
'<div class="phase-list">' + (sps.length ? sps.map(s => sprintCard(Object.assign({},s,{phaseId:p.id,phaseName:p.name}))).join('') :
|
|
490
|
-
'<div class="empty">No sprints in this phase yet.<div class="empty-action">Run /rihal-plan to create sprints</div></div>') + '</div>' +
|
|
491
|
-
cmdAccordion(phaseHints(p));
|
|
492
|
-
} else {
|
|
493
|
-
var plHints = [cmdHint('/rihal-add-phase', 'Add a new phase'), cmdHint('/rihal-stats', 'Project statistics'), cmdHint('/rihal-progress', 'Overall progress')];
|
|
494
|
-
var allComplete = _phases.length > 0 && _phases.every(ph => ph.status === 'complete' || ph.status === 'completed' || ph.status === 'done');
|
|
495
|
-
if (allComplete) { plHints.push(cmdHint('/rihal-audit-milestone', 'Audit milestone completion')); plHints.push(cmdHint('/rihal-complete-milestone', 'Complete and archive milestone')); plHints.push(cmdHint('/rihal-ship', 'Create PR and ship')); }
|
|
496
|
-
el.innerHTML = '<div class="view-title">Phases</div>' + filterInput('phases-inner') +
|
|
497
|
-
'<div id="phases-inner" class="phase-list">' +
|
|
498
|
-
(_phases.length ? _phases.map(phaseCard).join('') : '<div class="empty">No phases yet.<div class="empty-action">Run /rihal-new-project to start</div></div>') + '</div>' + cmdAccordion(plHints);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function renderSprints(subId) {
|
|
503
|
-
const el = document.getElementById('view-sprints');
|
|
504
|
-
const sprints = allSprints();
|
|
505
|
-
if (subId) {
|
|
506
|
-
const s = sprints.find(sp => String(sp.id) === String(subId));
|
|
507
|
-
if (!s) { el.innerHTML = breadcrumb('All Sprints','sprints') + '<div class="empty">Sprint not found.</div>'; return; }
|
|
508
|
-
const rawStories = Array.isArray(s.stories) ? s.stories : [];
|
|
509
|
-
const stories = rawStories.map(function(t) { return Object.assign({}, t, {sprintId: s.id, sprintGoal: s.goal || '', phaseId: s.phaseId, phaseName: s.phaseName}); });
|
|
510
|
-
const done = stories.filter(t => t.status==='done'||t.status==='completed').length;
|
|
511
|
-
// #290: acceptance criteria
|
|
512
|
-
let acHtml = '';
|
|
513
|
-
const storiesWithAc = stories.filter(t => t.acceptance);
|
|
514
|
-
if (storiesWithAc.length) {
|
|
515
|
-
acHtml = '<div class="view-title" style="margin-top:var(--space-6)">Acceptance Criteria</div>' +
|
|
516
|
-
'<div class="phase-list">' + storiesWithAc.map(t =>
|
|
517
|
-
'<div class="item"><div class="item-title">' + esc(t.title) + '</div>' +
|
|
518
|
-
'<div style="color:var(--text-secondary);font-size:var(--text-sm);margin-top:4px;">✓ ' + esc(t.acceptance) + '</div></div>'
|
|
519
|
-
).join('') + '</div>';
|
|
520
|
-
}
|
|
521
|
-
// #292: full breadcrumb path
|
|
522
|
-
el.innerHTML = '<div class="breadcrumb"><button class="back-btn" onclick="navTo(\\'sprints\\')">← All Sprints</button> ' +
|
|
523
|
-
(s.phaseId ? '<button class="back-btn" onclick="navTo(\\'phases/' + s.phaseId + '\\')">← Phase ' + esc(s.phaseId) + '</button>' : '') + '</div>' +
|
|
524
|
-
'<div class="entity-header"><div class="entity-title">⚡ Sprint ' + esc(s.id) + '</div>' +
|
|
525
|
-
'<div class="attr-grid">' +
|
|
526
|
-
attr('Goal', esc(s.goal||'—')) + attr('Status', chip(s.status)) +
|
|
527
|
-
attr('Phase', 'P' + s.phaseId + ' — ' + esc(s.phaseName)) +
|
|
528
|
-
attr('Velocity', (s.velocity_actual!=null?s.velocity_actual:'—') + ' / ' + (s.velocity_target!=null?s.velocity_target:'—') + ' pts') +
|
|
529
|
-
attr('Tasks Done', done + '/' + stories.length) + attr('Progress', pct(done,stories.length)) +
|
|
530
|
-
// #293: human-readable dates
|
|
531
|
-
(s.started_at ? attr('Started', humanDate(s.started_at)) : '') +
|
|
532
|
-
(s.completed_at ? attr('Completed', humanDate(s.completed_at)) : '') + '</div></div>' +
|
|
533
|
-
// #289: progress bar
|
|
534
|
-
'<div style="margin-bottom:var(--space-6);">' + progressBar(done, stories.length) + '</div>' +
|
|
535
|
-
'<div class="view-title" style="margin-top:var(--space-6)">Tasks</div>' +
|
|
536
|
-
'<div class="phase-list">' + (stories.length ? stories.map(taskCard).join('') :
|
|
537
|
-
'<div class="empty">No tasks in this sprint yet.<div class="empty-action">Run /rihal-create-story to add tasks</div></div>') + '</div>' +
|
|
538
|
-
acHtml + cmdAccordion(sprintHints(s));
|
|
539
|
-
} else {
|
|
540
|
-
var slHints = [cmdHint('/rihal-sprint-planning', 'Plan a new sprint'), cmdHint('/rihal-stats', 'Project statistics')];
|
|
541
|
-
var curSp = sprints.find(sp => sp.id === S.currentSprint);
|
|
542
|
-
if (curSp) { slHints.push(cmdHint('/rihal-execute', 'Execute current sprint ' + curSp.id)); slHints.push(cmdHint('/rihal-sprint-status', 'Status of Sprint ' + curSp.id)); }
|
|
543
|
-
el.innerHTML = '<div class="view-title">Sprints</div>' + filterInput('sprints-inner') +
|
|
544
|
-
'<div id="sprints-inner" class="phase-list">' +
|
|
545
|
-
(sprints.length ? sprints.map(sprintCard).join('') :
|
|
546
|
-
'<div class="empty">No sprints yet.<div class="empty-action">Run /rihal-plan to create sprints</div></div>') + '</div>' + cmdAccordion(slHints);
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
function renderTasks() {
|
|
551
|
-
const el = document.getElementById('view-tasks');
|
|
552
|
-
const tasks = allTasks();
|
|
553
|
-
// #295: aggregate points
|
|
554
|
-
const totalPts = tasks.reduce((sum, t) => sum + (t.points || 0), 0);
|
|
555
|
-
const donePts = tasks.filter(t => t.status === 'done' || t.status === 'completed').reduce((sum, t) => sum + (t.points || 0), 0);
|
|
556
|
-
// #296: filter by status + #297: sort options
|
|
557
|
-
el.innerHTML = '<div class="view-title">Tasks</div>' +
|
|
558
|
-
'<div class="filter-bar">' +
|
|
559
|
-
'<input class="filter-input" type="text" placeholder="Filter…" oninput="filterItems(this,\\'tasks-inner\\')">' +
|
|
560
|
-
'<select class="filter-select" id="task-status-filter" onchange="filterTasksByStatus()">' +
|
|
561
|
-
'<option value="">All statuses</option><option value="todo">Todo</option>' +
|
|
562
|
-
'<option value="in_progress">In Progress</option><option value="done">Done</option>' +
|
|
563
|
-
'<option value="blocked">Blocked</option></select>' +
|
|
564
|
-
'<select class="filter-select" id="task-sort" onchange="sortTasks()">' +
|
|
565
|
-
'<option value="default">Default order</option><option value="status">By status</option>' +
|
|
566
|
-
'<option value="points-desc">Points ↓</option><option value="points-asc">Points ↑</option></select>' +
|
|
567
|
-
'</div>' +
|
|
568
|
-
(totalPts > 0 ? '<div style="color:var(--text-muted);font-size:var(--text-sm);margin-bottom:var(--space-4);">' +
|
|
569
|
-
donePts + '/' + totalPts + ' points completed</div>' : '') +
|
|
570
|
-
// #294: group by sprint
|
|
571
|
-
'<div id="tasks-inner" class="phase-list">' +
|
|
572
|
-
renderTasksGrouped(tasks) + '</div>';
|
|
573
|
-
// Task hints accordion
|
|
574
|
-
var tHints = [cmdHint('/rihal-create-story', 'Add a new story/task'), cmdHint('/rihal-sprint-planning', 'Plan the next sprint')];
|
|
575
|
-
var allDone = tasks.length > 0 && tasks.every(t => t.status === 'done' || t.status === 'completed');
|
|
576
|
-
var hasBlocked = tasks.some(t => t.status === 'blocked');
|
|
577
|
-
if (allDone) { tHints.push(cmdHint('/rihal-verify-work', 'Verify all tasks pass UAT')); tHints.push(cmdHint('/rihal-audit-uat', 'Audit UAT coverage')); }
|
|
578
|
-
if (hasBlocked) { tHints.push(cmdHint('/rihal-debug', 'Debug blocked tasks')); tHints.push(cmdHint('/rihal-correct-course', 'Course-correct blockers')); }
|
|
579
|
-
el.innerHTML += cmdAccordion(tHints);
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
function renderTasksGrouped(tasks) {
|
|
583
|
-
if (!tasks.length) {
|
|
584
|
-
var phaseHint = S.currentPhase ? ' ' + S.currentPhase : '';
|
|
585
|
-
return '<div class="empty">No tasks yet.' +
|
|
586
|
-
'<div class="empty-action">Run <code>/rihal-plan' + phaseHint + '</code> to generate tasks for this project.</div></div>';
|
|
587
|
-
}
|
|
588
|
-
const groups = {};
|
|
589
|
-
for (const t of tasks) {
|
|
590
|
-
const key = t.sprintId || 'unassigned';
|
|
591
|
-
if (!groups[key]) groups[key] = [];
|
|
592
|
-
groups[key].push(t);
|
|
593
|
-
}
|
|
594
|
-
let h = '';
|
|
595
|
-
for (const [sprintId, items] of Object.entries(groups)) {
|
|
596
|
-
h += '<div style="margin-bottom:var(--space-4);"><div style="font-size:var(--text-sm);font-weight:600;color:var(--text-muted);margin-bottom:var(--space-2);">Sprint ' + esc(sprintId) + '</div>';
|
|
597
|
-
h += items.map(taskCard).join('');
|
|
598
|
-
h += '</div>';
|
|
599
|
-
}
|
|
600
|
-
return h;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
function filterTasksByStatus() {
|
|
604
|
-
const status = document.getElementById('task-status-filter')?.value || '';
|
|
605
|
-
const el = document.getElementById('tasks-inner');
|
|
606
|
-
if (!el) return;
|
|
607
|
-
el.querySelectorAll('.item').forEach(item => {
|
|
608
|
-
if (!status) { item.style.display = ''; return; }
|
|
609
|
-
const s = item.dataset.status || '';
|
|
610
|
-
const match = status === 'done' ? (s === 'done' || s === 'completed') : s === status;
|
|
611
|
-
item.style.display = match ? '' : 'none';
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
function sortTasks() {
|
|
616
|
-
const sort = document.getElementById('task-sort')?.value || 'default';
|
|
617
|
-
const tasks = allTasks();
|
|
618
|
-
if (sort === 'status') tasks.sort((a,b) => (a.status||'').localeCompare(b.status||''));
|
|
619
|
-
else if (sort === 'points-desc') tasks.sort((a,b) => (b.points||0) - (a.points||0));
|
|
620
|
-
else if (sort === 'points-asc') tasks.sort((a,b) => (a.points||0) - (b.points||0));
|
|
621
|
-
const el = document.getElementById('tasks-inner');
|
|
622
|
-
if (el) el.innerHTML = sort === 'default' ? renderTasksGrouped(tasks) : tasks.map(taskCard).join('');
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
// #283: view plan file
|
|
626
|
-
async function viewPlanFile(phaseId) {
|
|
627
|
-
// Try to find the plan file via the file tree
|
|
628
|
-
const padded = String(phaseId).padStart(2, '0');
|
|
629
|
-
const items = document.querySelectorAll('.file-tree-item');
|
|
630
|
-
for (const item of items) {
|
|
631
|
-
const p = item.dataset.path || '';
|
|
632
|
-
if (p.includes('phases') && p.includes(padded) && (p.includes('PLAN') || p.includes('SPRINT'))) {
|
|
633
|
-
item.click();
|
|
634
|
-
return;
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
// Fallback: navigate to files view
|
|
638
|
-
navTo('files');
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
// ---- Tree toggle (with #311 animation) ----
|
|
642
|
-
function toggleNode(row) {
|
|
643
|
-
const children = row.nextElementSibling;
|
|
644
|
-
const chevron = row.querySelector('.tree-chevron');
|
|
645
|
-
if (!children) return;
|
|
646
|
-
const open = children.style.display !== 'none';
|
|
647
|
-
children.style.display = open ? 'none' : 'block';
|
|
648
|
-
if (chevron) chevron.textContent = open ? '▶' : '▼';
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
// #277: collapse/expand all roadmap nodes
|
|
652
|
-
function toggleAllRoadmap(expand) {
|
|
653
|
-
document.querySelectorAll('#roadmap-tree .tree-children').forEach(c => {
|
|
654
|
-
c.style.display = expand ? 'block' : 'none';
|
|
655
|
-
});
|
|
656
|
-
document.querySelectorAll('#roadmap-tree .tree-chevron').forEach(ch => {
|
|
657
|
-
ch.textContent = expand ? '▼' : '▶';
|
|
658
|
-
});
|
|
659
|
-
// Keep root open
|
|
660
|
-
const root = document.querySelector('#roadmap-tree .tree-ms > .tree-row + .tree-children');
|
|
661
|
-
if (root) root.style.display = 'block';
|
|
662
|
-
const rootChev = document.querySelector('#roadmap-tree .tree-ms > .tree-row .tree-chevron');
|
|
663
|
-
if (rootChev) rootChev.textContent = '▼';
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
// ---- Hash router ----
|
|
667
|
-
function navTo(hash) { location.hash = hash; }
|
|
668
|
-
|
|
669
|
-
function route() {
|
|
670
|
-
const raw = location.hash.slice(1) || 'overview';
|
|
671
|
-
const slash = raw.indexOf('/');
|
|
672
|
-
const view = slash === -1 ? raw : raw.slice(0, slash);
|
|
673
|
-
const subId = slash === -1 ? null : raw.slice(slash + 1);
|
|
674
|
-
|
|
675
|
-
// Fix #264: highlight active nav on reload
|
|
676
|
-
document.querySelectorAll('.nav-link[data-view]').forEach(l =>
|
|
677
|
-
l.classList.toggle('active', l.dataset.view === view));
|
|
678
|
-
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
|
|
679
|
-
const el = document.getElementById('view-' + view);
|
|
680
|
-
if (el) {
|
|
681
|
-
el.classList.add('active');
|
|
682
|
-
} else {
|
|
683
|
-
// Fix #263: unknown hash routes show overview instead of blank
|
|
684
|
-
document.getElementById('view-overview')?.classList.add('active');
|
|
685
|
-
document.querySelector('.nav-link[data-view="overview"]')?.classList.add('active');
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
// #310: scroll to top on view switch
|
|
689
|
-
document.querySelector('.content-area')?.scrollTo(0, 0);
|
|
690
|
-
|
|
691
|
-
if (view === 'overview') renderOverview();
|
|
692
|
-
else if (view === 'roadmap') renderRoadmap();
|
|
693
|
-
else if (view === 'milestones') renderMilestones(subId);
|
|
694
|
-
else if (view === 'phases') renderPhases(subId);
|
|
695
|
-
else if (view === 'sprints') renderSprints(subId);
|
|
696
|
-
else if (view === 'tasks') renderTasks();
|
|
697
|
-
else if (view === 'decisions') renderDecisions();
|
|
698
|
-
else if (view === 'memory') renderMemory();
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
function renderMemory() {
|
|
702
|
-
const el = document.getElementById('view-memory-content');
|
|
703
|
-
if (!el) return;
|
|
704
|
-
el.innerHTML = '<div class="view-title">🧠 Memory Bank</div><div class="empty">Loading…</div>';
|
|
705
|
-
fetch('/api/memory').then(r => r.json()).then(m => {
|
|
706
|
-
if (!m.exists) {
|
|
707
|
-
el.innerHTML = '<div class="view-title">🧠 Memory Bank</div>' +
|
|
708
|
-
'<div class="empty"><h3 style="color:var(--rihal-gold);">Not initialised</h3>' +
|
|
709
|
-
'<p>The Memory Bank is rcode\\'s structured project context.</p>' +
|
|
710
|
-
'<div class="empty-action">Run <code>/rcode:memory-init</code> to bootstrap</div></div>';
|
|
711
|
-
return;
|
|
712
|
-
}
|
|
713
|
-
let h = '<div class="view-title">🧠 Memory Bank</div>';
|
|
714
|
-
if (!m.initialised) {
|
|
715
|
-
h += '<div class="empty"><p>Directory exists but INDEX.md is missing — re-run <code>/rcode:memory-init</code></p></div>';
|
|
716
|
-
el.innerHTML = h;
|
|
717
|
-
return;
|
|
718
|
-
}
|
|
719
|
-
const sections = m.sections || {};
|
|
720
|
-
h += '<div class="filter-bar"><span style="color:var(--text-muted);font-size:var(--text-sm);">Last scanned: ' + esc(m.lastScanned) + '</span></div>';
|
|
721
|
-
h += '<div id="memory-sections">';
|
|
722
|
-
for (const [section, files] of Object.entries(sections)) {
|
|
723
|
-
h += '<div style="font-size:var(--text-sm);font-weight:600;color:var(--text-muted);margin:var(--space-4) 0 var(--space-2);">' + esc(section) + '</div>';
|
|
724
|
-
h += '<div class="decision-list">';
|
|
725
|
-
for (const f of files) {
|
|
726
|
-
const status = f.exists ? (f.populated ? '✓' : '○') : '✗';
|
|
727
|
-
const meta = f.exists ? (f.populated ? 'populated' : 'template only') : 'missing';
|
|
728
|
-
h += '<div class="item">' +
|
|
729
|
-
'<div class="item-title">' + status + ' ' + esc(f.name) + '</div>' +
|
|
730
|
-
'<div class="item-meta">' + esc(meta) + ' · ' + (f.bytes || 0) + ' bytes</div>' +
|
|
731
|
-
'</div>';
|
|
732
|
-
}
|
|
733
|
-
h += '</div>';
|
|
734
|
-
}
|
|
735
|
-
function listGroup(label, items) {
|
|
736
|
-
if (!items || !items.length) return '';
|
|
737
|
-
let g = '<div style="font-size:var(--text-sm);font-weight:600;color:var(--text-muted);margin:var(--space-4) 0 var(--space-2);">' + esc(label) + ' (' + items.length + ')</div>';
|
|
738
|
-
g += '<div class="decision-list">';
|
|
739
|
-
for (const f of items) {
|
|
740
|
-
g += '<div class="item">' +
|
|
741
|
-
'<div class="item-title">' + esc(f.name) + '</div></div>';
|
|
742
|
-
}
|
|
743
|
-
g += '</div>';
|
|
744
|
-
return g;
|
|
745
|
-
}
|
|
746
|
-
h += listGroup('Distillates', m.distillates);
|
|
747
|
-
h += listGroup('Change Records', m.changeRecords);
|
|
748
|
-
h += listGroup('Milestone Archive', m.archive);
|
|
749
|
-
h += listGroup('Post-mortems', m.postMortems);
|
|
750
|
-
h += '</div>';
|
|
751
|
-
h += cmdAccordion([
|
|
752
|
-
cmdHint('/rcode:memory-init', 'Bootstrap the Memory Bank'),
|
|
753
|
-
cmdHint('/rcode:memory-update', 'Append a decision, issue, or stakeholder entry'),
|
|
754
|
-
cmdHint('/rcode:memory-distill', 'Regenerate fast-load distillates'),
|
|
755
|
-
cmdHint('/rcode:memory-audit', 'Find stale entries and gaps')
|
|
756
|
-
]);
|
|
757
|
-
el.innerHTML = h;
|
|
758
|
-
}).catch(err => {
|
|
759
|
-
el.innerHTML = '<div class="view-title">🧠 Memory Bank</div><div class="empty">Failed to load /api/memory: ' + esc(String(err)) + '</div>';
|
|
760
|
-
});
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
function renderDecisions() {
|
|
764
|
-
const el = document.getElementById('view-decisions');
|
|
765
|
-
if (!el) return;
|
|
766
|
-
const decisions = S.decisions || [];
|
|
767
|
-
if (!decisions.length) {
|
|
768
|
-
el.innerHTML = '<div class="view-title">Decisions (ADRs)</div>' +
|
|
769
|
-
'<div class="empty">No decisions recorded yet.<div class="empty-action">Decisions made during /rihal-council appear here</div></div>';
|
|
770
|
-
return;
|
|
771
|
-
}
|
|
772
|
-
// #307: group by phase
|
|
773
|
-
const grouped = {};
|
|
774
|
-
for (const d of decisions) {
|
|
775
|
-
const phase = (typeof d === 'object' ? d.phase : null) || 'General';
|
|
776
|
-
if (!grouped[phase]) grouped[phase] = [];
|
|
777
|
-
grouped[phase].push(d);
|
|
778
|
-
}
|
|
779
|
-
let h = '<div class="view-title">Decisions (ADRs)</div>' +
|
|
780
|
-
'<div class="filter-bar"><input class="filter-input" type="text" placeholder="Filter…" oninput="filterItems(this,\\'decisions-inner\\')"></div>' +
|
|
781
|
-
'<div id="decisions-inner">';
|
|
782
|
-
for (const [phase, decs] of Object.entries(grouped)) {
|
|
783
|
-
h += '<div style="font-size:var(--text-sm);font-weight:600;color:var(--text-muted);margin:var(--space-4) 0 var(--space-2);">' + esc(phase) + '</div>';
|
|
784
|
-
h += '<div class="decision-list">';
|
|
785
|
-
for (const d of decs) {
|
|
786
|
-
const title = typeof d === 'string' ? d : (d.title || d.summary || d.decision || JSON.stringify(d).slice(0, 80));
|
|
787
|
-
const filterText = String(title).toLowerCase();
|
|
788
|
-
// #306: date and phase context
|
|
789
|
-
const dateInfo = (typeof d === 'object' && d.date) ? '<span style="color:var(--text-muted);font-size:var(--text-xs);margin-left:8px;">' + humanDate(d.date) + '</span>' : '';
|
|
790
|
-
const phaseInfo = (typeof d === 'object' && d.phase) ? tag('Phase ' + d.phase) : '';
|
|
791
|
-
h += '<div class="item" data-filter-text="' + esc(filterText) + '">' +
|
|
792
|
-
'<div class="item-title">' + esc(title) + dateInfo + '</div>' +
|
|
793
|
-
'<div class="item-meta">' + phaseInfo + '</div>' +
|
|
794
|
-
// #308: rationale
|
|
795
|
-
(typeof d === 'object' && d.rationale ? '<div style="color:var(--text-secondary);font-size:var(--text-sm);margin-top:4px;">' + esc(d.rationale) + '</div>' : '') +
|
|
796
|
-
'</div>';
|
|
797
|
-
}
|
|
798
|
-
h += '</div>';
|
|
799
|
-
}
|
|
800
|
-
h += '</div>';
|
|
801
|
-
el.innerHTML = h + cmdAccordion([
|
|
802
|
-
cmdHint('/rihal-council', 'Convene the council for a new decision'),
|
|
803
|
-
cmdHint('/rihal-discuss [agent] \"topic\"', 'Discuss with a specific expert'),
|
|
804
|
-
cmdHint('/rihal-decisions', 'View decision log')
|
|
805
|
-
]);
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
window.addEventListener('hashchange', route);
|
|
809
|
-
document.querySelectorAll('.nav-link[data-view]').forEach(l =>
|
|
810
|
-
l.addEventListener('click', () => navTo(l.dataset.view)));
|
|
811
|
-
|
|
812
|
-
// ---- Inline filter ----
|
|
813
|
-
function filterItems(input, listId) {
|
|
814
|
-
const q = input.value.toLowerCase().trim();
|
|
815
|
-
const el = document.getElementById(listId);
|
|
816
|
-
if (!el) return;
|
|
817
|
-
el.querySelectorAll('.item').forEach(item => {
|
|
818
|
-
item.style.display = !q || item.textContent.toLowerCase().includes(q) ? '' : 'none';
|
|
819
|
-
});
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
// ---- Shared file-list fetch (single request for all consumers) ----
|
|
823
|
-
const _filesPromise = fetch('/api/files').then(function(r) { return r.json(); }).catch(function() { return []; });
|
|
824
|
-
|
|
825
|
-
// Inline file list inside Files view
|
|
826
|
-
(async function() {
|
|
827
|
-
let groups = [];
|
|
828
|
-
try { groups = await _filesPromise; } catch { return; }
|
|
829
|
-
const el = document.getElementById('file-list-inline');
|
|
830
|
-
if (!el) return;
|
|
831
|
-
let h = '<div class="filter-bar"><input class="filter-input" type="text" placeholder="Search files…" oninput="filterInlineFiles(this.value)"></div>';
|
|
832
|
-
h += '<div id="inline-file-items" class="phase-list">';
|
|
833
|
-
|
|
834
|
-
function renderFileItem(f, extraFilterText) {
|
|
835
|
-
var filterText = esc(f.label + ' ' + f.path + (extraFilterText ? ' ' + extraFilterText : '')).toLowerCase();
|
|
836
|
-
return '<div class="item item-clickable inline-file-entry" data-path="' + esc(f.path) + '" data-filter-text="' + filterText + '" onclick="loadInlineFile(this)" style="padding:var(--space-2) var(--space-3);font-family:\\'SF Mono\\',Monaco,Consolas,monospace;font-size:var(--text-xs);">' + esc(f.label) + '</div>';
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
groups.forEach(function(g) {
|
|
840
|
-
h += '<div class="inline-file-group" style="margin-bottom:var(--space-3);">';
|
|
841
|
-
h += '<div style="font-size:var(--text-xs);font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.07em;padding:var(--space-1) 0;">' + esc(g.group) + '</div>';
|
|
842
|
-
if (g.subGroups) {
|
|
843
|
-
// Render expandable sub-groups (e.g. per-phase)
|
|
844
|
-
g.subGroups.forEach(function(sg) {
|
|
845
|
-
h += '<details class="inline-subgroup" open style="margin-left:var(--space-2);margin-bottom:var(--space-1);">';
|
|
846
|
-
h += '<summary style="font-size:var(--text-xs);font-weight:500;color:var(--text-secondary);cursor:pointer;padding:var(--space-1) 0;user-select:none;">' + esc(sg.subGroup) + ' <span style="color:var(--text-muted);font-weight:400;">(' + sg.files.length + ')</span></summary>';
|
|
847
|
-
sg.files.forEach(function(f) {
|
|
848
|
-
h += renderFileItem(f, sg.subGroup);
|
|
849
|
-
});
|
|
850
|
-
h += '</details>';
|
|
851
|
-
});
|
|
852
|
-
} else if (g.files) {
|
|
853
|
-
g.files.forEach(function(f) {
|
|
854
|
-
h += renderFileItem(f, '');
|
|
855
|
-
});
|
|
856
|
-
}
|
|
857
|
-
h += '</div>';
|
|
858
|
-
});
|
|
859
|
-
h += '</div>';
|
|
860
|
-
el.innerHTML = h;
|
|
861
|
-
})();
|
|
862
|
-
function filterInlineFiles(q) {
|
|
863
|
-
q = q.toLowerCase().trim();
|
|
864
|
-
document.querySelectorAll('#inline-file-items .inline-file-entry').forEach(function(item) {
|
|
865
|
-
item.style.display = !q || (item.dataset.filterText || '').includes(q) ? '' : 'none';
|
|
866
|
-
});
|
|
867
|
-
}
|
|
868
|
-
async function loadInlineFile(el) {
|
|
869
|
-
var fv = document.getElementById('file-view');
|
|
870
|
-
if (!fv) return;
|
|
871
|
-
fv.innerHTML = '<div class="skeleton"></div><div class="skeleton" style="height:200px;"></div>';
|
|
872
|
-
// Scroll file content into view immediately
|
|
873
|
-
fv.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
874
|
-
document.querySelectorAll('.inline-file-entry').forEach(function(e) { e.style.borderLeftColor = ''; });
|
|
875
|
-
el.style.borderLeftColor = 'var(--accent-blue)';
|
|
876
|
-
// Also sync sidebar selection
|
|
877
|
-
document.querySelectorAll('.file-tree-item').forEach(function(e) {
|
|
878
|
-
e.classList.toggle('selected', e.dataset.path === el.dataset.path);
|
|
879
|
-
});
|
|
880
|
-
try {
|
|
881
|
-
var resp = await fetch('/api/file?path=' + encodeURIComponent(el.dataset.path));
|
|
882
|
-
if (!resp.ok) { fv.innerHTML = '<div style="color:var(--accent-red);padding:16px;">Failed to load file.</div>'; return; }
|
|
883
|
-
var text = await resp.text();
|
|
884
|
-
fv.innerHTML = '<div class="file-path-header"><span>' + esc(el.dataset.path) + '</span>' +
|
|
885
|
-
'<button class="copy-btn" onclick="navigator.clipboard.writeText(\\'' + el.dataset.path.replace(/'/g, "\\\\'") + '\\');showToast(\\'Path copied!\\')">📋 Copy</button></div>' +
|
|
886
|
-
'<div class="md-render">' + renderMd(text) + '</div>';
|
|
887
|
-
} catch { fv.innerHTML = '<div style="color:var(--accent-red);padding:16px;">Network error.</div>'; }
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
// ---- Markdown + frontmatter ----
|
|
891
|
-
function stripFrontmatter(md) {
|
|
892
|
-
if (!md.startsWith('---')) return md;
|
|
893
|
-
var end = md.indexOf('\\n---', 3);
|
|
894
|
-
return end === -1 ? md : md.slice(end + 4).trimStart();
|
|
895
|
-
}
|
|
896
|
-
function renderMd(md) {
|
|
897
|
-
var clean = stripFrontmatter(md);
|
|
898
|
-
return (typeof marked !== 'undefined') ? marked.parse(clean) : '<pre>' + clean.replace(/</g,'<') + '</pre>';
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
// ---- Open file from phase card ----
|
|
902
|
-
async function openFile(filePath) {
|
|
903
|
-
navTo('files');
|
|
904
|
-
document.querySelectorAll('.file-tree-item').forEach(function(el) {
|
|
905
|
-
el.classList.toggle('selected', el.dataset.path === filePath);
|
|
906
|
-
});
|
|
907
|
-
var fv = document.getElementById('file-view');
|
|
908
|
-
if (!fv) return;
|
|
909
|
-
fv.innerHTML = '<div class="skeleton"></div><div class="skeleton" style="height:200px;"></div>';
|
|
910
|
-
try {
|
|
911
|
-
var resp = await fetch('/api/file?path=' + encodeURIComponent(filePath));
|
|
912
|
-
if (!resp.ok) { fv.innerHTML = '<div style="color:var(--accent-red);padding:var(--space-8);">Failed.</div>'; return; }
|
|
913
|
-
var text = await resp.text();
|
|
914
|
-
fv.innerHTML = '<div class="file-path-header"><span>' + esc(filePath) + '</span></div>' +
|
|
915
|
-
'<div class="md-render">' + renderMd(text) + '</div>';
|
|
916
|
-
} catch { fv.innerHTML = '<div style="color:var(--accent-red);padding:var(--space-8);">Network error.</div>'; }
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
// ---- Refresh ----
|
|
920
|
-
var _lastScanned = ${JSON.stringify(state.lastScanned)};
|
|
921
|
-
var _scanTime = Date.now();
|
|
922
|
-
function renderUpdatedAgo() {
|
|
923
|
-
var s = Math.floor((Date.now() - _scanTime) / 1000);
|
|
924
|
-
var el = document.getElementById('updated-ago');
|
|
925
|
-
if (el) el.textContent = s < 5 ? 'just now' : s < 60 ? s + 's ago' : Math.floor(s/60) + 'm ago';
|
|
926
|
-
}
|
|
927
|
-
setInterval(renderUpdatedAgo, 1000);
|
|
928
|
-
|
|
929
|
-
// #262: hot-swap without full page reload
|
|
930
|
-
async function fetchAndRerender() {
|
|
931
|
-
var btn = document.getElementById('refresh-btn');
|
|
932
|
-
if (btn) btn.textContent = '↺ …';
|
|
933
|
-
try {
|
|
934
|
-
var r = await fetch('/api/state');
|
|
935
|
-
var newState = await r.json();
|
|
936
|
-
_lastScanned = newState.lastScanned;
|
|
937
|
-
_scanTime = Date.now();
|
|
938
|
-
renderUpdatedAgo();
|
|
939
|
-
// Update embedded data
|
|
940
|
-
if (newState.raw) {
|
|
941
|
-
S.phases = newState.raw.phases || [];
|
|
942
|
-
S.milestone = newState.raw.milestone || '';
|
|
943
|
-
S.currentPhase = newState.raw.current_phase || null;
|
|
944
|
-
S.currentSprint = newState.raw.current_sprint || null;
|
|
945
|
-
S.decisions = newState.raw.decisions || [];
|
|
946
|
-
S.blockers = newState.raw.blockers || [];
|
|
947
|
-
S.council_sessions = newState.raw.council_sessions || [];
|
|
948
|
-
S.last_session = newState.raw.last_session || null;
|
|
949
|
-
_phases.length = 0;
|
|
950
|
-
_phases.push(...S.phases);
|
|
951
|
-
}
|
|
952
|
-
// #261: re-render active view
|
|
953
|
-
route();
|
|
954
|
-
} catch {}
|
|
955
|
-
if (btn) btn.textContent = '↺ Refresh';
|
|
956
|
-
}
|
|
957
|
-
setInterval(async function() {
|
|
958
|
-
try {
|
|
959
|
-
var r = await fetch('/api/state');
|
|
960
|
-
var s = await r.json();
|
|
961
|
-
if (s.lastScanned !== _lastScanned) fetchAndRerender();
|
|
962
|
-
} catch {}
|
|
963
|
-
}, 30000);
|
|
964
|
-
function manualRefresh() { fetchAndRerender(); }
|
|
965
|
-
|
|
966
|
-
// ---- Blocker banner ----
|
|
967
|
-
(function() {
|
|
968
|
-
// #317: allow re-show via custom event
|
|
969
|
-
if (sessionStorage.getItem('blockers-dismissed') === '1') {
|
|
970
|
-
var b = document.getElementById('blocker-banner');
|
|
971
|
-
if (b) b.style.display = 'none';
|
|
972
|
-
}
|
|
973
|
-
})();
|
|
974
|
-
function dismissBlockers() {
|
|
975
|
-
sessionStorage.setItem('blockers-dismissed','1');
|
|
976
|
-
var b = document.getElementById('blocker-banner');
|
|
977
|
-
if (b) b.style.display = 'none';
|
|
978
|
-
}
|
|
979
|
-
function showBlockers() {
|
|
980
|
-
sessionStorage.removeItem('blockers-dismissed');
|
|
981
|
-
var b = document.getElementById('blocker-banner');
|
|
982
|
-
if (b) b.style.display = '';
|
|
983
|
-
}
|
|
984
14
|
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
})
|
|
1004
|
-
|
|
1005
|
-
//
|
|
1006
|
-
|
|
1007
|
-
var data = JSON.stringify(S, null, 2);
|
|
1008
|
-
var blob = new Blob([data], {type: 'application/json'});
|
|
1009
|
-
var url = URL.createObjectURL(blob);
|
|
1010
|
-
var a = document.createElement('a');
|
|
1011
|
-
a.href = url; a.download = 'majlis-snapshot-' + new Date().toISOString().slice(0,10) + '.json';
|
|
1012
|
-
a.click(); URL.revokeObjectURL(url);
|
|
1013
|
-
showToast('Snapshot exported!');
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
// #312: copy URL
|
|
1017
|
-
function copyUrl() {
|
|
1018
|
-
navigator.clipboard.writeText(location.href);
|
|
1019
|
-
showToast('URL copied!');
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
// #313: dark/light mode
|
|
1023
|
-
function toggleTheme() {
|
|
1024
|
-
var current = document.documentElement.getAttribute('data-theme');
|
|
1025
|
-
var next = current === 'light' ? 'dark' : 'light';
|
|
1026
|
-
document.documentElement.setAttribute('data-theme', next === 'dark' ? '' : next);
|
|
1027
|
-
localStorage.setItem('majlis-theme', next);
|
|
1028
|
-
var btn = document.getElementById('theme-btn');
|
|
1029
|
-
if (btn) btn.textContent = next === 'light' ? '🌙' : '☀️';
|
|
1030
|
-
}
|
|
1031
|
-
(function() {
|
|
1032
|
-
var saved = localStorage.getItem('majlis-theme');
|
|
1033
|
-
if (saved === 'light') {
|
|
1034
|
-
document.documentElement.setAttribute('data-theme', 'light');
|
|
1035
|
-
var btn = document.getElementById('theme-btn');
|
|
1036
|
-
if (btn) btn.textContent = '🌙';
|
|
1037
|
-
}
|
|
1038
|
-
})();
|
|
1039
|
-
|
|
1040
|
-
// #324: dynamic title
|
|
1041
|
-
var _origTitle = document.title;
|
|
1042
|
-
function updateTitle() {
|
|
1043
|
-
var view = (location.hash.slice(1) || 'overview').split('/')[0];
|
|
1044
|
-
var viewNames = {overview:'Overview',roadmap:'Roadmap',milestones:'Milestones',phases:'Phases',sprints:'Sprints',tasks:'Tasks',files:'Files',agents:'Agents',decisions:'Decisions'};
|
|
1045
|
-
document.title = (viewNames[view] || 'Overview') + ' — Majlis';
|
|
1046
|
-
}
|
|
1047
|
-
window.addEventListener('hashchange', updateTitle);
|
|
1048
|
-
|
|
1049
|
-
// Sidebar toggle (hamburger menu)
|
|
1050
|
-
function toggleSidebar() {
|
|
1051
|
-
var sidebar = document.querySelector('.sidebar');
|
|
1052
|
-
var backdrop = document.getElementById('sidebar-backdrop');
|
|
1053
|
-
if (!sidebar) return;
|
|
1054
|
-
var open = sidebar.classList.toggle('sidebar-open');
|
|
1055
|
-
if (backdrop) backdrop.classList.toggle('active', open);
|
|
1056
|
-
document.body.classList.toggle('sidebar-visible', open);
|
|
1057
|
-
}
|
|
1058
|
-
function closeSidebar() {
|
|
1059
|
-
var sidebar = document.querySelector('.sidebar');
|
|
1060
|
-
var backdrop = document.getElementById('sidebar-backdrop');
|
|
1061
|
-
if (sidebar) sidebar.classList.remove('sidebar-open');
|
|
1062
|
-
if (backdrop) backdrop.classList.remove('active');
|
|
1063
|
-
document.body.classList.remove('sidebar-visible');
|
|
15
|
+
const { ICONS } = require('./icons');
|
|
16
|
+
|
|
17
|
+
// Fields the client needs from the scanned state. Kept in sync with
|
|
18
|
+
// store.js initial state and the view components that read it.
|
|
19
|
+
function clientState(state) {
|
|
20
|
+
return JSON.stringify({
|
|
21
|
+
phases: state.phaseTree || state.raw?.phases || [],
|
|
22
|
+
milestone: state.raw?.milestone || '',
|
|
23
|
+
currentPhase: state.raw?.current_phase || null,
|
|
24
|
+
currentSprint: state.raw?.current_sprint || null,
|
|
25
|
+
decisions: state.raw?.decisions || [],
|
|
26
|
+
blockers: state.raw?.blockers || [],
|
|
27
|
+
council_sessions: state.raw?.council_sessions || [],
|
|
28
|
+
last_session: state.raw?.last_session || null,
|
|
29
|
+
chains: state.raw?.chains || [],
|
|
30
|
+
workstreams: state.raw?.workstreams || [],
|
|
31
|
+
pendingHandoff: state.pendingHandoff || null,
|
|
32
|
+
memoryBank: state.memoryBank || null,
|
|
33
|
+
})
|
|
34
|
+
// Prevent a stray "</script>" inside any string from closing the inline
|
|
35
|
+
// <script> early. Escaping "<" keeps the JSON valid and inert.
|
|
36
|
+
.replace(/</g, '\\u003c');
|
|
1064
37
|
}
|
|
1065
38
|
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
39
|
+
function renderClientJs(state) {
|
|
40
|
+
// Emit __ICONS__ so the Preact client can use the same icon set as the
|
|
41
|
+
// server without duplicating the map in a way that would require a build step.
|
|
42
|
+
const iconsJson = JSON.stringify(ICONS).replace(/</g, '\\u003c');
|
|
43
|
+
return [
|
|
44
|
+
`<script>window.__S__ = ${clientState(state)}; window.__ICONS__ = ${iconsJson};</script>`,
|
|
45
|
+
// Preact entry — type=module defers until HTML is parsed.
|
|
46
|
+
`<script type="module" src="/js/app.js"></script>`,
|
|
47
|
+
].join('\n');
|
|
1070
48
|
}
|
|
1071
49
|
|
|
1072
50
|
module.exports = { renderClientJs };
|