codecartographer-pi 0.6.1 → 0.9.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/.codecarto/GUIDE.md +2 -0
- package/README.md +275 -219
- package/assets/logo.svg +42 -0
- package/dist/core/dashboard.d.ts +42 -0
- package/dist/core/dashboard.js +637 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/library.d.ts +157 -0
- package/dist/core/library.js +675 -0
- package/dist/core/orchestrator-config.d.ts +38 -0
- package/dist/core/orchestrator-config.js +86 -19
- package/dist/core/pipeline.js +15 -3
- package/dist/core/prompts.d.ts +12 -1
- package/dist/core/prompts.js +15 -6
- package/dist/core/usage.js +15 -1
- package/dist/core/utils.d.ts +21 -0
- package/dist/core/utils.js +44 -1
- package/dist/core/workspace.d.ts +1 -0
- package/dist/core/workspace.js +13 -1
- package/dist/extensions/codecarto/auto-runner.d.ts +96 -0
- package/dist/extensions/codecarto/auto-runner.js +403 -0
- package/dist/extensions/codecarto/dashboard-flags.d.ts +6 -0
- package/dist/extensions/codecarto/dashboard-flags.js +17 -0
- package/dist/extensions/codecarto/dashboard-narrator.d.ts +8 -0
- package/dist/extensions/codecarto/dashboard-narrator.js +182 -0
- package/dist/extensions/codecarto/dashboard-writer.d.ts +1 -0
- package/dist/extensions/codecarto/dashboard-writer.js +148 -0
- package/dist/extensions/codecarto/index.js +98 -208
- package/dist/extensions/codecarto/next-flags.d.ts +4 -0
- package/dist/extensions/codecarto/next-flags.js +19 -6
- package/dist/mcp-server/server.d.ts +21 -0
- package/dist/mcp-server/server.js +305 -3
- package/package.json +3 -2
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
// Pure-ish HTML dashboard renderer for a CodeCartographer workspace. The
|
|
2
|
+
// writer gathers disk state and calls renderDashboard() to produce a
|
|
3
|
+
// self-contained, file://-friendly report. The report includes a small embedded
|
|
4
|
+
// script for local-only filtering and export; it has no external assets.
|
|
5
|
+
import { formatMillis, formatTokenCount } from "./utils.js";
|
|
6
|
+
import { computePerPhaseTotals, computeTotals } from "./usage.js";
|
|
7
|
+
export const DASHBOARD_RELATIVE_PATH = "dashboard.html";
|
|
8
|
+
export const NARRATION_CACHE_RELATIVE_PATH = ".dashboard-narration.local.md";
|
|
9
|
+
const TIMELINE_VISIBLE_COUNT = 10;
|
|
10
|
+
export function renderDashboard(inputs) {
|
|
11
|
+
const projectName = inputs.status.project_name || "CodeCartographer";
|
|
12
|
+
const sections = [
|
|
13
|
+
renderSidebar(inputs),
|
|
14
|
+
`<div class="cc-main">`,
|
|
15
|
+
renderHeader(inputs),
|
|
16
|
+
renderStalenessWarning(inputs),
|
|
17
|
+
inputs.narration ? renderNarration(inputs.narration, completedPhaseCount(inputs.status)) : "",
|
|
18
|
+
renderKeyResults(inputs),
|
|
19
|
+
renderProgressBar(inputs.pipeline, inputs.status),
|
|
20
|
+
renderPhaseCards(inputs),
|
|
21
|
+
renderUsagePanel(inputs),
|
|
22
|
+
renderActivityTimeline(inputs.usage.runs),
|
|
23
|
+
renderOpenQuestionsRollup(inputs.status),
|
|
24
|
+
renderCloseoutsList(inputs),
|
|
25
|
+
renderFooter(inputs),
|
|
26
|
+
`</div>`,
|
|
27
|
+
].filter(Boolean).join("\n");
|
|
28
|
+
return [
|
|
29
|
+
"<!DOCTYPE html>",
|
|
30
|
+
`<html lang="en">`,
|
|
31
|
+
"<head>",
|
|
32
|
+
`<meta charset="utf-8">`,
|
|
33
|
+
`<meta name="viewport" content="width=device-width, initial-scale=1">`,
|
|
34
|
+
`<title>${escapeHtml(projectName)} — CodeCartographer dashboard</title>`,
|
|
35
|
+
renderStyles(),
|
|
36
|
+
"</head>",
|
|
37
|
+
"<body>",
|
|
38
|
+
`<button class="cc-menu-button" type="button" data-sidebar-toggle aria-label="Toggle navigation">☰</button>`,
|
|
39
|
+
`<main class="cc-dashboard">`,
|
|
40
|
+
sections,
|
|
41
|
+
"</main>",
|
|
42
|
+
renderExportData(inputs),
|
|
43
|
+
renderScripts(),
|
|
44
|
+
"</body>",
|
|
45
|
+
"</html>",
|
|
46
|
+
].join("\n");
|
|
47
|
+
}
|
|
48
|
+
// ----------------------------------------------------------------------------
|
|
49
|
+
// Sections
|
|
50
|
+
// ----------------------------------------------------------------------------
|
|
51
|
+
function renderSidebar(inputs) {
|
|
52
|
+
const phaseLinks = inputs.pipeline.phase_order.map((phaseId) => {
|
|
53
|
+
const state = phaseRenderState(inputs.status, phaseId);
|
|
54
|
+
return `<a class="cc-nav-link cc-state-${state}" href="#${phaseAnchor(phaseId)}" data-status="${escapeAttr(state)}"><span>${escapeHtml(phaseId)}</span><b>${escapeHtml(state)}</b></a>`;
|
|
55
|
+
}).join("\n");
|
|
56
|
+
return [
|
|
57
|
+
`<aside class="cc-sidebar" data-sidebar>`,
|
|
58
|
+
`<div class="cc-brand">`,
|
|
59
|
+
`<span class="cc-brand-mark">◇</span>`,
|
|
60
|
+
`<div><strong>CodeCartographer</strong><span>${escapeHtml(inputs.status.project_name || "workspace")}</span></div>`,
|
|
61
|
+
`</div>`,
|
|
62
|
+
`<label class="cc-search-label">Search dashboard</label>`,
|
|
63
|
+
`<input class="cc-search" type="search" placeholder="phase, output, question…" data-search>`,
|
|
64
|
+
`<div class="cc-filter-row" aria-label="Status filters">`,
|
|
65
|
+
`<button type="button" class="cc-filter is-active" data-filter="all">All</button>`,
|
|
66
|
+
`<button type="button" class="cc-filter" data-filter="complete">Complete</button>`,
|
|
67
|
+
`<button type="button" class="cc-filter" data-filter="current">Current</button>`,
|
|
68
|
+
`<button type="button" class="cc-filter" data-filter="pending">Pending</button>`,
|
|
69
|
+
`</div>`,
|
|
70
|
+
`<nav class="cc-nav" aria-label="Dashboard sections">`,
|
|
71
|
+
`<a class="cc-nav-section" href="#summary">Summary</a>`,
|
|
72
|
+
`<a class="cc-nav-section" href="#key-results">Key results</a>`,
|
|
73
|
+
`<a class="cc-nav-section" href="#phases">Phases</a>`,
|
|
74
|
+
phaseLinks,
|
|
75
|
+
`<a class="cc-nav-section" href="#usage">Usage</a>`,
|
|
76
|
+
`<a class="cc-nav-section" href="#closeouts">Closeouts</a>`,
|
|
77
|
+
`</nav>`,
|
|
78
|
+
`<button type="button" class="cc-export" data-export>Export dashboard JSON</button>`,
|
|
79
|
+
`<p class="cc-sidebar-foot">Self-contained local report. No network calls.</p>`,
|
|
80
|
+
`</aside>`,
|
|
81
|
+
].join("\n");
|
|
82
|
+
}
|
|
83
|
+
function renderHeader(inputs) {
|
|
84
|
+
const { status, pipeline, packageVersion, generatedAt } = inputs;
|
|
85
|
+
const projectName = status.project_name || "(unnamed project)";
|
|
86
|
+
const pipelineLabel = pipeline.workflow_name || status.pipeline;
|
|
87
|
+
const currentPhase = status.current_phase || "—";
|
|
88
|
+
const totals = computeTotals(inputs.usage);
|
|
89
|
+
const completed = completedPhaseCount(status);
|
|
90
|
+
const total = pipeline.phase_order.length;
|
|
91
|
+
const nextActions = status.next_actions?.length
|
|
92
|
+
? `<ul class="cc-next-actions">${status.next_actions.map((a) => `<li>${escapeHtml(a)}</li>`).join("")}</ul>`
|
|
93
|
+
: `<p class="cc-muted">No next actions recorded.</p>`;
|
|
94
|
+
return [
|
|
95
|
+
`<header class="cc-header" id="summary" data-section data-search-text="${escapeAttr([projectName, pipelineLabel, currentPhase, pipeline.workflow_goal ?? ""].join(" "))}">`,
|
|
96
|
+
`<div class="cc-eyebrow">CodeCartographer dashboard</div>`,
|
|
97
|
+
`<h1>${escapeHtml(projectName)}</h1>`,
|
|
98
|
+
pipeline.workflow_goal ? `<p class="cc-goal">${escapeHtml(pipeline.workflow_goal)}</p>` : "",
|
|
99
|
+
`<div class="cc-stat-grid">`,
|
|
100
|
+
renderStat("Pipeline", pipelineLabel),
|
|
101
|
+
renderStat("Current phase", currentPhase),
|
|
102
|
+
renderStat("Progress", `${completed}/${total} phases`),
|
|
103
|
+
renderStat("Recorded tokens", formatTokenCount(totals.tokens.input + totals.tokens.output)),
|
|
104
|
+
renderStat("Tool uses", String(totals.tool_uses)),
|
|
105
|
+
renderStat("Package", `v${packageVersion}`),
|
|
106
|
+
`</div>`,
|
|
107
|
+
`<details class="cc-meta-details">`,
|
|
108
|
+
`<summary>Run metadata and next actions</summary>`,
|
|
109
|
+
`<dl class="cc-header-meta">`,
|
|
110
|
+
`<dt>Status last updated</dt><dd>${escapeHtml(status.last_updated || "never")}</dd>`,
|
|
111
|
+
`<dt>Dashboard generated</dt><dd>${escapeHtml(generatedAt)}</dd>`,
|
|
112
|
+
`<dt>Status file</dt><dd><code>${escapeHtml(status.pipeline)}</code></dd>`,
|
|
113
|
+
`</dl>`,
|
|
114
|
+
`<h3>Next actions</h3>`,
|
|
115
|
+
nextActions,
|
|
116
|
+
`</details>`,
|
|
117
|
+
`</header>`,
|
|
118
|
+
].filter(Boolean).join("\n");
|
|
119
|
+
}
|
|
120
|
+
function renderStat(label, value) {
|
|
121
|
+
return `<div class="cc-stat"><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong></div>`;
|
|
122
|
+
}
|
|
123
|
+
function renderStalenessWarning(inputs) {
|
|
124
|
+
if (!inputs.status.last_updated || inputs.status.last_updated <= inputs.generatedAt)
|
|
125
|
+
return "";
|
|
126
|
+
return `<section class="cc-warning" data-section><strong>Dashboard may be stale.</strong> Status was updated at ${escapeHtml(inputs.status.last_updated)} after this dashboard was generated at ${escapeHtml(inputs.generatedAt)}. Regenerate with <code>/codecarto-dashboard</code>.</section>`;
|
|
127
|
+
}
|
|
128
|
+
function renderNarration(narration, currentCompletedCount) {
|
|
129
|
+
const runsSince = Math.max(0, currentCompletedCount - narration.phaseCountAtGeneration);
|
|
130
|
+
const staleness = runsSince === 0 ? "current" : `${runsSince} run${runsSince === 1 ? "" : "s"} since`;
|
|
131
|
+
return [
|
|
132
|
+
`<section class="cc-card cc-narration" aria-label="Executive summary" data-section data-search-text="${escapeAttr(narration.content)}">`,
|
|
133
|
+
`<div class="cc-section-head"><h2>Executive summary</h2><span>${escapeHtml(staleness)}</span></div>`,
|
|
134
|
+
`<p class="cc-narration-meta">Narrated ${escapeHtml(narration.generatedAt)}.</p>`,
|
|
135
|
+
`<pre>${escapeHtml(narration.content)}</pre>`,
|
|
136
|
+
`</section>`,
|
|
137
|
+
].join("\n");
|
|
138
|
+
}
|
|
139
|
+
function renderKeyResults(inputs) {
|
|
140
|
+
const rows = [];
|
|
141
|
+
for (const phaseId of inputs.pipeline.phase_order) {
|
|
142
|
+
const phase = getPhase(inputs.pipeline, phaseId);
|
|
143
|
+
if (!phase?.primary_output)
|
|
144
|
+
continue;
|
|
145
|
+
const outputs = inputs.outputsPresent.get(phaseId);
|
|
146
|
+
const closeout = closeoutForPhase(inputs.closeouts, phaseId);
|
|
147
|
+
const primary = outputs?.primary;
|
|
148
|
+
if (primary?.exists || phaseRenderState(inputs.status, phaseId) === "complete") {
|
|
149
|
+
rows.push(renderArtifactRow({ phaseId, label: "Primary result", path: phase.primary_output, exists: primary?.exists ?? false, closeout }));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (rows.length === 0)
|
|
153
|
+
return "";
|
|
154
|
+
return [
|
|
155
|
+
`<section class="cc-card" id="key-results" aria-label="Key results" data-section data-search-text="key results final artifacts outputs">`,
|
|
156
|
+
`<div class="cc-section-head"><h2>Key results</h2><span>${rows.length} artifacts</span></div>`,
|
|
157
|
+
`<div class="cc-artifact-list">${rows.join("\n")}</div>`,
|
|
158
|
+
`</section>`,
|
|
159
|
+
].join("\n");
|
|
160
|
+
}
|
|
161
|
+
function renderArtifactRow(input) {
|
|
162
|
+
const result = input.exists
|
|
163
|
+
? renderSafeLink(input.path, input.path)
|
|
164
|
+
: `<span class="cc-missing-path">${escapeHtml(input.path)}</span>`;
|
|
165
|
+
const closeout = input.closeout
|
|
166
|
+
? renderSafeLink(`closeouts/${input.closeout.fileName}`, "closeout", "cc-pill")
|
|
167
|
+
: `<span class="cc-pill cc-pill-muted">no closeout</span>`;
|
|
168
|
+
return `<article class="cc-artifact-row" data-search-text="${escapeAttr(`${input.phaseId} ${input.path} ${input.label}`)}"><div><a class="cc-phase-jump" href="#${phaseAnchor(input.phaseId)}">${escapeHtml(input.phaseId)}</a><span>${escapeHtml(input.label)}</span></div><div>${result}</div><div>${input.exists ? `<span class="cc-pill cc-pill-ok">available</span>` : `<span class="cc-pill cc-pill-bad">missing</span>`}${closeout}</div></article>`;
|
|
169
|
+
}
|
|
170
|
+
function renderProgressBar(pipeline, status) {
|
|
171
|
+
const items = pipeline.phase_order.map((phaseId) => {
|
|
172
|
+
const state = phaseRenderState(status, phaseId);
|
|
173
|
+
const purpose = getPhase(pipeline, phaseId)?.purpose ?? "";
|
|
174
|
+
return [
|
|
175
|
+
`<li class="cc-progress-item cc-state-${state}" title="${escapeAttr(purpose)}">`,
|
|
176
|
+
`<a href="#${phaseAnchor(phaseId)}">`,
|
|
177
|
+
`<span class="cc-progress-id">${escapeHtml(phaseId)}</span>`,
|
|
178
|
+
`<span class="cc-progress-badge">${escapeHtml(state)}</span>`,
|
|
179
|
+
`</a>`,
|
|
180
|
+
`</li>`,
|
|
181
|
+
].join("");
|
|
182
|
+
}).join("\n");
|
|
183
|
+
return [
|
|
184
|
+
`<section class="cc-progress" aria-label="Pipeline progress" data-section data-search-text="pipeline progress">`,
|
|
185
|
+
`<h2>Pipeline progress</h2>`,
|
|
186
|
+
`<ol class="cc-progress-list">`,
|
|
187
|
+
items,
|
|
188
|
+
`</ol>`,
|
|
189
|
+
`</section>`,
|
|
190
|
+
].join("\n");
|
|
191
|
+
}
|
|
192
|
+
function renderPhaseCards(inputs) {
|
|
193
|
+
const { status, pipeline, usage, outputsPresent, closeouts } = inputs;
|
|
194
|
+
const perPhaseLastRun = lastRunPerPhase(usage.runs);
|
|
195
|
+
const cards = pipeline.phase_order.map((phaseId) => {
|
|
196
|
+
const phaseDef = getPhase(pipeline, phaseId);
|
|
197
|
+
const phaseState = status.phases[phaseId];
|
|
198
|
+
const renderState = phaseRenderState(status, phaseId);
|
|
199
|
+
const outputs = outputsPresent.get(phaseId);
|
|
200
|
+
const lastRun = perPhaseLastRun.get(phaseId);
|
|
201
|
+
const closeout = closeoutForPhase(closeouts, phaseId);
|
|
202
|
+
const open = renderState === "running" || renderState === "current";
|
|
203
|
+
const detailsAttr = open ? " open" : "";
|
|
204
|
+
const searchText = [phaseId, phaseDef?.purpose, phaseDef?.primary_output, ...(phaseDef?.secondary_outputs ?? []).map((s) => s.path), ...(phaseState?.owner_notes ?? []), ...(phaseState?.open_questions ?? []).map((q) => q.description)].filter(Boolean).join(" ");
|
|
205
|
+
return [
|
|
206
|
+
`<details class="cc-phase-card cc-state-${renderState}" id="${phaseAnchor(phaseId)}" data-section data-phase data-status="${escapeAttr(renderState)}" data-search-text="${escapeAttr(searchText)}"${detailsAttr}>`,
|
|
207
|
+
`<summary>`,
|
|
208
|
+
`<span class="cc-phase-id">${escapeHtml(phaseId)}</span>`,
|
|
209
|
+
`<span class="cc-phase-badge">${escapeHtml(renderState)}</span>`,
|
|
210
|
+
`</summary>`,
|
|
211
|
+
renderPhasePurpose(phaseDef),
|
|
212
|
+
renderPhaseOverview(phaseDef),
|
|
213
|
+
renderPhaseOutputs(phaseDef, outputs),
|
|
214
|
+
renderPhaseCloseout(closeout),
|
|
215
|
+
renderPhaseOpenQuestions(phaseState),
|
|
216
|
+
renderPhaseCarryForward(phaseState),
|
|
217
|
+
renderPhaseOwnerNotes(phaseState),
|
|
218
|
+
renderPhaseLastRun(lastRun),
|
|
219
|
+
`</details>`,
|
|
220
|
+
].filter(Boolean).join("\n");
|
|
221
|
+
}).join("\n");
|
|
222
|
+
return [
|
|
223
|
+
`<section class="cc-phases" id="phases" aria-label="Per-phase status">`,
|
|
224
|
+
`<h2>Phases</h2>`,
|
|
225
|
+
cards,
|
|
226
|
+
`</section>`,
|
|
227
|
+
].join("\n");
|
|
228
|
+
}
|
|
229
|
+
function renderPhasePurpose(phase) {
|
|
230
|
+
if (!phase?.purpose)
|
|
231
|
+
return "";
|
|
232
|
+
return `<p class="cc-phase-purpose">${escapeHtml(phase.purpose)}</p>`;
|
|
233
|
+
}
|
|
234
|
+
function renderPhaseOverview(phase) {
|
|
235
|
+
if (!phase)
|
|
236
|
+
return "";
|
|
237
|
+
const bits = [];
|
|
238
|
+
if ((phase.depends_on ?? []).length > 0)
|
|
239
|
+
bits.push(`<div><strong>Depends on</strong>${phase.depends_on.map((d) => `<a class="cc-pill" href="#${phaseAnchor(d)}">${escapeHtml(d)}</a>`).join("")}</div>`);
|
|
240
|
+
if ((phase.required_reads ?? []).length > 0)
|
|
241
|
+
bits.push(`<div><strong>Required reads</strong>${phase.required_reads.map((r) => `<code>${escapeHtml(r)}</code>`).join(" ")}</div>`);
|
|
242
|
+
if (bits.length === 0)
|
|
243
|
+
return "";
|
|
244
|
+
return `<div class="cc-phase-section cc-phase-overview">${bits.join("")}</div>`;
|
|
245
|
+
}
|
|
246
|
+
function renderPhaseOutputs(phase, outputs) {
|
|
247
|
+
if (!phase)
|
|
248
|
+
return "";
|
|
249
|
+
const lines = [];
|
|
250
|
+
if (phase.primary_output) {
|
|
251
|
+
const present = outputs?.primary?.exists ?? false;
|
|
252
|
+
const link = renderSafeLink(phase.primary_output, phase.primary_output);
|
|
253
|
+
lines.push(present && link
|
|
254
|
+
? `<li>${link} <span class="cc-pill cc-pill-ok">primary</span></li>`
|
|
255
|
+
: `<li><span class="cc-pill cc-pill-bad">primary missing</span> <span class="cc-missing-path">${escapeHtml(phase.primary_output)}</span></li>`);
|
|
256
|
+
}
|
|
257
|
+
for (const sec of phase.secondary_outputs ?? []) {
|
|
258
|
+
const present = outputs?.secondary.find((s) => s.path === sec.path)?.exists ?? false;
|
|
259
|
+
const link = renderSafeLink(sec.path, sec.path);
|
|
260
|
+
lines.push(present && link
|
|
261
|
+
? `<li>${link} <span class="cc-pill">secondary</span></li>`
|
|
262
|
+
: `<li><span class="cc-pill cc-pill-bad">secondary missing</span> <span class="cc-missing-path">${escapeHtml(sec.path)}</span></li>`);
|
|
263
|
+
}
|
|
264
|
+
if (lines.length === 0)
|
|
265
|
+
return "";
|
|
266
|
+
return `<div class="cc-phase-section"><h3>Outputs</h3><ul class="cc-output-list">${lines.join("")}</ul></div>`;
|
|
267
|
+
}
|
|
268
|
+
function renderPhaseCloseout(closeout) {
|
|
269
|
+
if (!closeout)
|
|
270
|
+
return "";
|
|
271
|
+
const link = renderSafeLink(`closeouts/${closeout.fileName}`, closeout.fileName);
|
|
272
|
+
return `<div class="cc-phase-section"><h3>Closeout</h3><p>${link ?? escapeHtml(closeout.fileName)}</p>${closeout.summary ? `<p class="cc-closeout-summary">${escapeHtml(closeout.summary)}</p>` : ""}</div>`;
|
|
273
|
+
}
|
|
274
|
+
function renderPhaseOpenQuestions(phaseState) {
|
|
275
|
+
const items = phaseState?.open_questions ?? [];
|
|
276
|
+
if (items.length === 0)
|
|
277
|
+
return "";
|
|
278
|
+
return [`<div class="cc-phase-section">`, `<h3>Open questions (${items.length})</h3>`, `<ul class="cc-question-list">`, items.map(renderOpenQuestion).join(""), `</ul>`, `</div>`].join("");
|
|
279
|
+
}
|
|
280
|
+
function renderOpenQuestion(q) {
|
|
281
|
+
const kind = q.kind ? `<span class="cc-pill">${escapeHtml(String(q.kind))}</span> ` : "";
|
|
282
|
+
const desc = escapeHtml(q.description ?? "(no description)");
|
|
283
|
+
const reason = q.deferred_reason ? `<div class="cc-question-reason">${escapeHtml(q.deferred_reason)}</div>` : "";
|
|
284
|
+
return `<li>${kind}${desc}${reason}</li>`;
|
|
285
|
+
}
|
|
286
|
+
function renderPhaseCarryForward(phaseState) {
|
|
287
|
+
const items = phaseState?.carry_forward ?? [];
|
|
288
|
+
if (items.length === 0)
|
|
289
|
+
return "";
|
|
290
|
+
return [`<div class="cc-phase-section">`, `<h3>Carry-forward (${items.length})</h3>`, `<ul class="cc-carry-list">`, items.map(renderCarryForward).join(""), `</ul>`, `</div>`].join("");
|
|
291
|
+
}
|
|
292
|
+
function renderCarryForward(c) {
|
|
293
|
+
const target = c.target_phase ? `<a class="cc-pill cc-pill-target" href="#${phaseAnchor(c.target_phase)}">→ ${escapeHtml(c.target_phase)}</a> ` : "";
|
|
294
|
+
const kind = c.kind ? `<span class="cc-pill">${escapeHtml(String(c.kind))}</span> ` : "";
|
|
295
|
+
const desc = escapeHtml(c.description ?? "(no description)");
|
|
296
|
+
const reason = c.deferred_reason ? `<div class="cc-question-reason">${escapeHtml(c.deferred_reason)}</div>` : "";
|
|
297
|
+
return `<li>${target}${kind}${desc}${reason}</li>`;
|
|
298
|
+
}
|
|
299
|
+
function renderPhaseOwnerNotes(phaseState) {
|
|
300
|
+
const notes = phaseState?.owner_notes ?? [];
|
|
301
|
+
if (notes.length === 0)
|
|
302
|
+
return "";
|
|
303
|
+
return [`<div class="cc-phase-section">`, `<h3>Owner notes</h3>`, `<ul class="cc-notes-list">`, notes.map((n) => `<li>${escapeHtml(n)}</li>`).join(""), `</ul>`, `</div>`].join("");
|
|
304
|
+
}
|
|
305
|
+
function renderPhaseLastRun(run) {
|
|
306
|
+
if (!run)
|
|
307
|
+
return `<div class="cc-phase-section"><h3>Last run</h3><p class="cc-muted">No usage record for this phase.</p></div>`;
|
|
308
|
+
const tokensTotal = (run.tokens?.input ?? 0) + (run.tokens?.output ?? 0);
|
|
309
|
+
const sessionLink = run.session_file ? renderSafeLink(run.session_file, "transcript") : undefined;
|
|
310
|
+
const session = sessionLink ? `<dt>Session</dt><dd>${sessionLink}</dd>` : "";
|
|
311
|
+
return [`<div class="cc-phase-section">`, `<h3>Last run</h3>`, `<dl class="cc-run-meta">`, `<dt>Timestamp</dt><dd>${escapeHtml(run.timestamp)}</dd>`, `<dt>Status</dt><dd>${escapeHtml(run.status)}</dd>`, `<dt>Turns</dt><dd>${run.turn_count}</dd>`, `<dt>Tool uses</dt><dd>${run.tool_uses}</dd>`, `<dt>Tokens</dt><dd>${escapeHtml(formatTokenCount(tokensTotal))}</dd>`, `<dt>Duration</dt><dd>${escapeHtml(formatMillis(run.duration_ms))}</dd>`, session, `</dl>`, `</div>`].join("");
|
|
312
|
+
}
|
|
313
|
+
function renderUsagePanel(inputs) {
|
|
314
|
+
const { usage, pipeline, status } = inputs;
|
|
315
|
+
const totals = computeTotals(usage);
|
|
316
|
+
const perPhase = computePerPhaseTotals(usage);
|
|
317
|
+
const rows = pipeline.phase_order.map((phaseId) => {
|
|
318
|
+
const t = perPhase.get(phaseId);
|
|
319
|
+
if (!t) {
|
|
320
|
+
const complete = status.phases[phaseId]?.status === "complete";
|
|
321
|
+
return `<tr class="${complete ? "cc-usage-missing" : ""}"><td><a href="#${phaseAnchor(phaseId)}">${escapeHtml(phaseId)}</a></td><td>0</td><td>—</td><td>—</td><td>${complete ? "usage not recorded" : "not run"}</td></tr>`;
|
|
322
|
+
}
|
|
323
|
+
const tokensTotal = t.tokens.input + t.tokens.output;
|
|
324
|
+
return `<tr><td><a href="#${phaseAnchor(phaseId)}">${escapeHtml(phaseId)}</a></td><td>${t.runs}</td><td>${escapeHtml(formatTokenCount(tokensTotal))}</td><td>${t.tool_uses}</td><td>${escapeHtml(formatMillis(t.duration_ms))}</td></tr>`;
|
|
325
|
+
}).join("");
|
|
326
|
+
return [
|
|
327
|
+
`<section class="cc-card cc-usage" id="usage" aria-label="Token usage" data-section data-search-text="usage tokens tool duration">`,
|
|
328
|
+
`<div class="cc-section-head"><h2>Usage</h2><span>${totals.runs} runs</span></div>`,
|
|
329
|
+
usage.runs.length === 0 ? `<p class="cc-empty">No phase runs recorded yet.</p>` : `<dl class="cc-usage-totals">${renderUsageTotalsList(totals)}</dl>`,
|
|
330
|
+
`<table class="cc-usage-table">`,
|
|
331
|
+
`<thead><tr><th>Phase</th><th>Runs</th><th>Tokens</th><th>Tools</th><th>Duration</th></tr></thead>`,
|
|
332
|
+
`<tbody>${rows}</tbody>`,
|
|
333
|
+
`</table>`,
|
|
334
|
+
`</section>`,
|
|
335
|
+
].join("\n");
|
|
336
|
+
}
|
|
337
|
+
function renderUsageTotalsList(totals) {
|
|
338
|
+
const tokensTotal = totals.tokens.input + totals.tokens.output;
|
|
339
|
+
return [`<dt>Total runs</dt><dd>${totals.runs}</dd>`, `<dt>Tokens (in / out / cache)</dt><dd>${escapeHtml(formatTokenCount(totals.tokens.input))} / ${escapeHtml(formatTokenCount(totals.tokens.output))} / ${escapeHtml(formatTokenCount(totals.tokens.cache_write))}</dd>`, `<dt>Total tokens</dt><dd>${escapeHtml(formatTokenCount(tokensTotal))}</dd>`, `<dt>Tool uses</dt><dd>${totals.tool_uses}</dd>`, `<dt>Total duration</dt><dd>${escapeHtml(formatMillis(totals.duration_ms))}</dd>`].join("");
|
|
340
|
+
}
|
|
341
|
+
function renderActivityTimeline(runs) {
|
|
342
|
+
if (runs.length === 0)
|
|
343
|
+
return "";
|
|
344
|
+
const sorted = [...runs].sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1));
|
|
345
|
+
const visible = sorted.slice(0, TIMELINE_VISIBLE_COUNT);
|
|
346
|
+
const overflow = sorted.slice(TIMELINE_VISIBLE_COUNT);
|
|
347
|
+
const rowOf = (run) => {
|
|
348
|
+
const tokensTotal = (run.tokens?.input ?? 0) + (run.tokens?.output ?? 0);
|
|
349
|
+
const sessionCell = run.session_file ? (renderSafeLink(run.session_file, "session") ?? "—") : "—";
|
|
350
|
+
return `<tr><td>${escapeHtml(run.timestamp)}</td><td><a href="#${phaseAnchor(run.phase)}">${escapeHtml(run.phase)}</a></td><td>${escapeHtml(run.status)}</td><td>${run.turn_count}</td><td>${run.tool_uses}</td><td>${escapeHtml(formatTokenCount(tokensTotal))}</td><td>${escapeHtml(formatMillis(run.duration_ms))}</td><td>${sessionCell}</td></tr>`;
|
|
351
|
+
};
|
|
352
|
+
const overflowBlock = overflow.length === 0 ? "" : [`<details class="cc-timeline-older">`, `<summary>Older runs (${overflow.length})</summary>`, `<table class="cc-timeline-table">`, `<tbody>${overflow.map(rowOf).join("")}</tbody>`, `</table>`, `</details>`].join("\n");
|
|
353
|
+
return [`<section class="cc-card cc-timeline" aria-label="Activity timeline" data-section data-search-text="activity timeline sessions">`, `<div class="cc-section-head"><h2>Activity timeline</h2><span>newest first</span></div>`, `<table class="cc-timeline-table">`, `<thead><tr><th>When</th><th>Phase</th><th>Status</th><th>⟳</th><th>Tools</th><th>Tokens</th><th>Duration</th><th>Session</th></tr></thead>`, `<tbody>${visible.map(rowOf).join("")}</tbody>`, `</table>`, overflowBlock, `</section>`].join("\n");
|
|
354
|
+
}
|
|
355
|
+
function renderOpenQuestionsRollup(status) {
|
|
356
|
+
const buckets = [];
|
|
357
|
+
const seen = new Set();
|
|
358
|
+
let total = 0;
|
|
359
|
+
for (const [phaseId, phaseState] of Object.entries(status.phases)) {
|
|
360
|
+
const questions = [];
|
|
361
|
+
for (const q of phaseState.open_questions ?? []) {
|
|
362
|
+
const key = `${q.kind ?? ""}|${q.description ?? ""}|${q.deferred_reason ?? ""}`;
|
|
363
|
+
if (seen.has(key))
|
|
364
|
+
continue;
|
|
365
|
+
seen.add(key);
|
|
366
|
+
questions.push(renderOpenQuestion(q));
|
|
367
|
+
}
|
|
368
|
+
if (questions.length === 0)
|
|
369
|
+
continue;
|
|
370
|
+
total += questions.length;
|
|
371
|
+
buckets.push(`<div class="cc-question-bucket"><h3>${escapeHtml(phaseId)} (${questions.length})</h3><a class="cc-pill cc-pill-target" href="#${phaseAnchor(phaseId)}">jump to phase</a><ul class="cc-question-list">${questions.join("")}</ul></div>`);
|
|
372
|
+
}
|
|
373
|
+
if (total === 0)
|
|
374
|
+
return "";
|
|
375
|
+
return [`<section class="cc-card cc-rollup" aria-label="Open questions roll-up" data-section data-search-text="open questions">`, `<div class="cc-section-head"><h2>Open questions</h2><span>${total} unique</span></div>`, buckets.join("\n"), `</section>`].join("\n");
|
|
376
|
+
}
|
|
377
|
+
function renderCloseoutsList(inputs) {
|
|
378
|
+
const closeouts = inputs.closeouts;
|
|
379
|
+
if (closeouts.length === 0)
|
|
380
|
+
return [`<section class="cc-card cc-closeouts" id="closeouts" aria-label="Closeouts" data-section>`, `<h2>Closeouts</h2>`, `<p class="cc-empty">No closeouts yet.</p>`, `</section>`].join("\n");
|
|
381
|
+
const sorted = [...closeouts].sort((a, b) => (a.date < b.date ? 1 : -1));
|
|
382
|
+
const rows = sorted.map((c) => {
|
|
383
|
+
const phase = getPhase(inputs.pipeline, c.phaseOrModule);
|
|
384
|
+
const outputs = inputs.outputsPresent.get(c.phaseOrModule);
|
|
385
|
+
const outputLinks = renderCloseoutOutputLinks(phase, outputs);
|
|
386
|
+
const closeoutLink = renderSafeLink(`closeouts/${c.fileName}`, "summary");
|
|
387
|
+
const summary = c.summary ? `<p class="cc-closeout-summary">${escapeHtml(c.summary)}</p>` : "";
|
|
388
|
+
return `<article class="cc-closeout-row" data-search-text="${escapeAttr(`${c.date} ${c.phaseOrModule} ${c.fileName} ${c.summary ?? ""}`)}"><div><span class="cc-closeout-date">${escapeHtml(c.date)}</span><a class="cc-phase-jump" href="#${phaseAnchor(c.phaseOrModule)}">${escapeHtml(c.phaseOrModule)}</a></div><div>${closeoutLink ?? "summary"}<code>${escapeHtml(c.fileName)}</code>${summary}</div><div class="cc-closeout-results">${outputLinks}</div></article>`;
|
|
389
|
+
}).join("\n");
|
|
390
|
+
return [`<section class="cc-card cc-closeouts" id="closeouts" aria-label="Closeouts" data-section data-search-text="closeouts summaries results">`, `<div class="cc-section-head"><h2>Closeouts</h2><span>${closeouts.length} summaries</span></div>`, `<div class="cc-closeouts-list">${rows}</div>`, `</section>`].join("\n");
|
|
391
|
+
}
|
|
392
|
+
function renderCloseoutOutputLinks(phase, outputs) {
|
|
393
|
+
if (!phase)
|
|
394
|
+
return `<span class="cc-pill cc-pill-muted">non-phase closeout</span>`;
|
|
395
|
+
const links = [];
|
|
396
|
+
if (phase.primary_output) {
|
|
397
|
+
const exists = outputs?.primary?.exists ?? false;
|
|
398
|
+
links.push(exists ? (renderSafeLink(phase.primary_output, "primary result", "cc-pill cc-pill-ok") ?? `<span class="cc-pill cc-pill-bad">primary unsafe</span>`) : `<span class="cc-pill cc-pill-bad">primary missing</span>`);
|
|
399
|
+
}
|
|
400
|
+
for (const sec of phase.secondary_outputs ?? []) {
|
|
401
|
+
const exists = outputs?.secondary.find((s) => s.path === sec.path)?.exists ?? false;
|
|
402
|
+
if (exists)
|
|
403
|
+
links.push(renderSafeLink(sec.path, "secondary", "cc-pill") ?? `<span class="cc-pill cc-pill-bad">secondary unsafe</span>`);
|
|
404
|
+
}
|
|
405
|
+
return links.length ? links.join("") : `<span class="cc-pill cc-pill-muted">no outputs</span>`;
|
|
406
|
+
}
|
|
407
|
+
function renderFooter(inputs) {
|
|
408
|
+
return [`<footer class="cc-footer">`, `<p>Generated by codecartographer-pi v${escapeHtml(inputs.packageVersion)} at ${escapeHtml(inputs.generatedAt)}.</p>`, `<p class="cc-footer-hint">Regenerate via <code>/codecarto-dashboard</code> · narrate via <code>/codecarto-dashboard --narrate</code>.</p>`, `</footer>`].join("\n");
|
|
409
|
+
}
|
|
410
|
+
// ----------------------------------------------------------------------------
|
|
411
|
+
// Data export and JS
|
|
412
|
+
// ----------------------------------------------------------------------------
|
|
413
|
+
function renderExportData(inputs) {
|
|
414
|
+
const phases = inputs.pipeline.phase_order.map((phaseId) => {
|
|
415
|
+
const phase = getPhase(inputs.pipeline, phaseId);
|
|
416
|
+
const outputs = inputs.outputsPresent.get(phaseId);
|
|
417
|
+
return {
|
|
418
|
+
id: phaseId,
|
|
419
|
+
status: phaseRenderState(inputs.status, phaseId),
|
|
420
|
+
purpose: phase?.purpose ?? "",
|
|
421
|
+
primary_output: phase?.primary_output ?? null,
|
|
422
|
+
primary_output_exists: outputs?.primary?.exists ?? false,
|
|
423
|
+
secondary_outputs: outputs?.secondary ?? [],
|
|
424
|
+
};
|
|
425
|
+
});
|
|
426
|
+
const data = { project: inputs.status.project_name, generatedAt: inputs.generatedAt, packageVersion: inputs.packageVersion, phases, usage: inputs.usage, closeouts: inputs.closeouts };
|
|
427
|
+
return `<script id="cc-dashboard-data" type="application/json">${escapeJsonForScript(data)}</script>`;
|
|
428
|
+
}
|
|
429
|
+
function renderScripts() {
|
|
430
|
+
return `<script>
|
|
431
|
+
(() => {
|
|
432
|
+
const q = document.querySelector('[data-search]');
|
|
433
|
+
const filters = [...document.querySelectorAll('[data-filter]')];
|
|
434
|
+
const sections = [...document.querySelectorAll('[data-section]')];
|
|
435
|
+
let active = 'all';
|
|
436
|
+
function textFor(el){ return ((el.dataset.searchText || '') + ' ' + el.textContent).toLowerCase(); }
|
|
437
|
+
function apply(){
|
|
438
|
+
const terms = (q?.value || '').toLowerCase().trim().split(/\\s+/).filter(Boolean);
|
|
439
|
+
for (const el of sections) {
|
|
440
|
+
const status = el.dataset.status;
|
|
441
|
+
const statusOk = active === 'all' || !status || status === active;
|
|
442
|
+
const searchOk = terms.every(t => textFor(el).includes(t));
|
|
443
|
+
el.hidden = !(statusOk && searchOk);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
q?.addEventListener('input', apply);
|
|
447
|
+
for (const btn of filters) btn.addEventListener('click', () => {
|
|
448
|
+
active = btn.dataset.filter || 'all';
|
|
449
|
+
filters.forEach(b => b.classList.toggle('is-active', b === btn));
|
|
450
|
+
apply();
|
|
451
|
+
});
|
|
452
|
+
document.querySelector('[data-export]')?.addEventListener('click', () => {
|
|
453
|
+
const raw = document.getElementById('cc-dashboard-data')?.textContent || '{}';
|
|
454
|
+
const blob = new Blob([JSON.stringify(JSON.parse(raw), null, 2) + '\\n'], { type: 'application/json' });
|
|
455
|
+
const a = document.createElement('a');
|
|
456
|
+
a.href = URL.createObjectURL(blob);
|
|
457
|
+
a.download = 'codecartographer-dashboard.json';
|
|
458
|
+
a.click();
|
|
459
|
+
URL.revokeObjectURL(a.href);
|
|
460
|
+
});
|
|
461
|
+
document.querySelector('[data-sidebar-toggle]')?.addEventListener('click', () => document.body.classList.toggle('cc-sidebar-open'));
|
|
462
|
+
window.addEventListener('keydown', (event) => {
|
|
463
|
+
if (event.key === 'Escape') { if (q) q.value = ''; active = 'all'; filters.forEach(b => b.classList.toggle('is-active', b.dataset.filter === 'all')); apply(); document.body.classList.remove('cc-sidebar-open'); }
|
|
464
|
+
});
|
|
465
|
+
})();
|
|
466
|
+
</script>`;
|
|
467
|
+
}
|
|
468
|
+
// ----------------------------------------------------------------------------
|
|
469
|
+
// Helpers
|
|
470
|
+
// ----------------------------------------------------------------------------
|
|
471
|
+
function phaseRenderState(status, phaseId) {
|
|
472
|
+
const phaseStatus = status.phases[phaseId]?.status;
|
|
473
|
+
if (phaseStatus === "complete")
|
|
474
|
+
return "complete";
|
|
475
|
+
if (phaseStatus === "running")
|
|
476
|
+
return "running";
|
|
477
|
+
if (status.current_phase === phaseId)
|
|
478
|
+
return "current";
|
|
479
|
+
return "pending";
|
|
480
|
+
}
|
|
481
|
+
function completedPhaseCount(status) {
|
|
482
|
+
return Object.values(status.phases).filter((p) => p.status === "complete").length;
|
|
483
|
+
}
|
|
484
|
+
function lastRunPerPhase(runs) {
|
|
485
|
+
const out = new Map();
|
|
486
|
+
for (const r of runs) {
|
|
487
|
+
const existing = out.get(r.phase);
|
|
488
|
+
if (!existing || existing.timestamp < r.timestamp)
|
|
489
|
+
out.set(r.phase, r);
|
|
490
|
+
}
|
|
491
|
+
return out;
|
|
492
|
+
}
|
|
493
|
+
function getPhase(pipeline, phaseId) {
|
|
494
|
+
return pipeline.phases.find((p) => p.id === phaseId);
|
|
495
|
+
}
|
|
496
|
+
function closeoutForPhase(closeouts, phaseId) {
|
|
497
|
+
return [...closeouts].filter((c) => c.phaseOrModule === phaseId).sort((a, b) => (a.date < b.date ? 1 : -1))[0];
|
|
498
|
+
}
|
|
499
|
+
function phaseAnchor(phaseId) {
|
|
500
|
+
return `phase-${phaseId.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
|
|
501
|
+
}
|
|
502
|
+
function renderSafeLink(path, label, className) {
|
|
503
|
+
const href = safeRelativeHref(path);
|
|
504
|
+
if (!href)
|
|
505
|
+
return undefined;
|
|
506
|
+
const classAttr = className ? ` class="${escapeAttr(className)}"` : "";
|
|
507
|
+
return `<a${classAttr} href="${escapeAttr(href)}">${escapeHtml(label)}</a>`;
|
|
508
|
+
}
|
|
509
|
+
function safeRelativeHref(path) {
|
|
510
|
+
if (!path || path.startsWith("/") || path.startsWith("\\") || /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(path))
|
|
511
|
+
return undefined;
|
|
512
|
+
const parts = path.split("/");
|
|
513
|
+
if (parts.some((seg) => !seg || seg === "." || seg === ".."))
|
|
514
|
+
return undefined;
|
|
515
|
+
return parts.map((seg) => encodeURIComponent(seg)).join("/");
|
|
516
|
+
}
|
|
517
|
+
function escapeJsonForScript(value) {
|
|
518
|
+
return JSON.stringify(value)
|
|
519
|
+
.replace(/&/g, "\\u0026")
|
|
520
|
+
.replace(/</g, "\\u003c")
|
|
521
|
+
.replace(/>/g, "\\u003e")
|
|
522
|
+
.replace(/\u2028/g, "\\u2028")
|
|
523
|
+
.replace(/\u2029/g, "\\u2029");
|
|
524
|
+
}
|
|
525
|
+
export function escapeHtml(input) {
|
|
526
|
+
return input.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\"/g, """).replace(/'/g, "'");
|
|
527
|
+
}
|
|
528
|
+
function escapeAttr(input) {
|
|
529
|
+
return escapeHtml(input);
|
|
530
|
+
}
|
|
531
|
+
// ----------------------------------------------------------------------------
|
|
532
|
+
// Styles
|
|
533
|
+
// ----------------------------------------------------------------------------
|
|
534
|
+
function renderStyles() {
|
|
535
|
+
return `<style>
|
|
536
|
+
:root {
|
|
537
|
+
--sidebar-width: 300px; --s-1: 4px; --s-2: 8px; --s-3: 16px; --s-4: 24px; --s-5: 40px;
|
|
538
|
+
--fg: #1f2328; --fg-dim: #667085; --bg: #f6f4ee; --bg-card: #fffdf7; --bg-soft: #ece7da;
|
|
539
|
+
--border: #ddd5c4; --accent: #b8432f; --accent-2: #2c5862; --accent-3: #63a77d;
|
|
540
|
+
--status-pending: #8a8f98; --status-current: #2c5862; --status-running: #b7791f; --status-complete: #2f855a; --status-error: #c53030;
|
|
541
|
+
--code-bg: #eee8da; --shadow: 0 14px 45px rgba(22, 18, 12, 0.10);
|
|
542
|
+
}
|
|
543
|
+
@media (prefers-color-scheme: dark) {
|
|
544
|
+
:root {
|
|
545
|
+
--fg: #f3ead7; --fg-dim: #a79f90; --bg: #101211; --bg-card: #181b19; --bg-soft: #20251f;
|
|
546
|
+
--border: #30362f; --accent: #f1a84f; --accent-2: #71c4cf; --accent-3: #63e6a4;
|
|
547
|
+
--status-pending: #7d8790; --status-current: #71c4cf; --status-running: #f1a84f; --status-complete: #63e6a4; --status-error: #ff7768;
|
|
548
|
+
--code-bg: #242820; --shadow: 0 14px 45px rgba(0, 0, 0, 0.28);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
* { box-sizing: border-box; }
|
|
552
|
+
html { scroll-behavior: smooth; }
|
|
553
|
+
body { margin: 0; background: radial-gradient(circle at top left, color-mix(in srgb, var(--accent-2) 15%, transparent), transparent 32rem), var(--bg); color: var(--fg); font: 14px/1.55 ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
554
|
+
a { color: var(--accent-2); text-decoration: none; }
|
|
555
|
+
a:hover { color: var(--accent); text-decoration: underline; }
|
|
556
|
+
code { background: var(--code-bg); padding: 2px 5px; border-radius: 5px; font: 12px/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
557
|
+
[hidden] { display: none !important; }
|
|
558
|
+
.cc-dashboard { display: grid; grid-template-columns: var(--sidebar-width) minmax(0, 1fr); min-height: 100vh; }
|
|
559
|
+
.cc-sidebar { position: sticky; top: 0; height: 100vh; overflow: auto; padding: var(--s-3); border-right: 1px solid var(--border); background: color-mix(in srgb, var(--bg-card) 92%, transparent); backdrop-filter: blur(12px); }
|
|
560
|
+
.cc-main { max-width: 1180px; width: 100%; padding: var(--s-4); }
|
|
561
|
+
.cc-brand { display: flex; gap: var(--s-2); align-items: center; margin-bottom: var(--s-3); }
|
|
562
|
+
.cc-brand-mark { display: grid; place-items: center; width: 34px; height: 34px; border-radius: 10px; background: linear-gradient(135deg, var(--accent), var(--accent-2)); color: white; font-weight: 800; }
|
|
563
|
+
.cc-brand div { display: grid; line-height: 1.2; } .cc-brand span:last-child { color: var(--fg-dim); font-size: 12px; }
|
|
564
|
+
.cc-search-label { display: block; color: var(--fg-dim); font-size: 12px; margin-bottom: var(--s-1); }
|
|
565
|
+
.cc-search { width: 100%; padding: 10px 11px; border: 1px solid var(--border); border-radius: 10px; background: var(--bg); color: var(--fg); }
|
|
566
|
+
.cc-filter-row { display: flex; flex-wrap: wrap; gap: var(--s-1); margin: var(--s-2) 0 var(--s-3); }
|
|
567
|
+
.cc-filter, .cc-export { border: 1px solid var(--border); background: var(--bg-soft); color: var(--fg); border-radius: 999px; padding: 6px 9px; cursor: pointer; font: inherit; font-size: 12px; }
|
|
568
|
+
.cc-filter.is-active, .cc-export:hover { border-color: var(--accent); color: var(--accent); }
|
|
569
|
+
.cc-export { width: 100%; border-radius: 10px; margin-top: var(--s-3); }
|
|
570
|
+
.cc-nav { display: grid; gap: 3px; }
|
|
571
|
+
.cc-nav-link, .cc-nav-section { display: flex; justify-content: space-between; gap: var(--s-2); padding: 7px 8px; border-radius: 8px; color: var(--fg); }
|
|
572
|
+
.cc-nav-link:hover, .cc-nav-section:hover { background: var(--bg-soft); text-decoration: none; }
|
|
573
|
+
.cc-nav-link b { color: var(--fg-dim); font-size: 11px; text-transform: uppercase; }
|
|
574
|
+
.cc-nav-section { color: var(--fg-dim); font-weight: 700; margin-top: var(--s-2); }
|
|
575
|
+
.cc-sidebar-foot { color: var(--fg-dim); font-size: 12px; }
|
|
576
|
+
.cc-menu-button { display: none; position: fixed; top: 12px; right: 12px; z-index: 20; border: 1px solid var(--border); border-radius: 10px; padding: 8px 10px; background: var(--bg-card); color: var(--fg); }
|
|
577
|
+
.cc-header, .cc-card, .cc-phase-card, .cc-warning { background: var(--bg-card); border: 1px solid var(--border); border-radius: 18px; box-shadow: var(--shadow); }
|
|
578
|
+
.cc-header { padding: var(--s-4); margin-bottom: var(--s-4); }
|
|
579
|
+
.cc-eyebrow { color: var(--accent); text-transform: uppercase; letter-spacing: .14em; font-size: 12px; font-weight: 800; }
|
|
580
|
+
h1 { font-size: clamp(30px, 5vw, 56px); line-height: 1; margin: var(--s-2) 0; }
|
|
581
|
+
h2 { margin: 0; font-size: 18px; } h3 { margin: var(--s-2) 0 var(--s-1); color: var(--fg-dim); text-transform: uppercase; letter-spacing: .06em; font-size: 12px; }
|
|
582
|
+
.cc-goal { max-width: 70ch; color: var(--fg-dim); font-size: 16px; }
|
|
583
|
+
.cc-stat-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s-2); margin-top: var(--s-3); }
|
|
584
|
+
.cc-stat { padding: var(--s-2); border: 1px solid var(--border); border-radius: 12px; background: var(--bg); }
|
|
585
|
+
.cc-stat span { display: block; color: var(--fg-dim); font-size: 12px; } .cc-stat strong { display: block; font-size: 18px; }
|
|
586
|
+
.cc-meta-details { margin-top: var(--s-3); } .cc-header-meta, .cc-run-meta, .cc-usage-totals { display: grid; grid-template-columns: max-content 1fr; gap: var(--s-1) var(--s-3); margin: var(--s-2) 0; } dt { color: var(--fg-dim); } dd { margin: 0; }
|
|
587
|
+
.cc-card, .cc-warning { padding: var(--s-3); margin-bottom: var(--s-4); }
|
|
588
|
+
.cc-warning { border-color: var(--status-running); background: color-mix(in srgb, var(--status-running) 9%, var(--bg-card)); }
|
|
589
|
+
.cc-section-head { display: flex; justify-content: space-between; gap: var(--s-2); align-items: baseline; padding-bottom: var(--s-2); margin-bottom: var(--s-2); border-bottom: 1px solid var(--border); }
|
|
590
|
+
.cc-section-head span, .cc-muted, .cc-empty { color: var(--fg-dim); }
|
|
591
|
+
.cc-narration pre { white-space: pre-wrap; word-wrap: break-word; margin: 0; font-family: inherit; }
|
|
592
|
+
.cc-artifact-list, .cc-closeouts-list { display: grid; gap: var(--s-2); }
|
|
593
|
+
.cc-artifact-row, .cc-closeout-row { display: grid; grid-template-columns: minmax(170px, .8fr) minmax(0, 1.5fr) minmax(180px, .8fr); gap: var(--s-2); align-items: start; padding: var(--s-2); border: 1px solid var(--border); border-radius: 12px; background: var(--bg); }
|
|
594
|
+
.cc-artifact-row > div:first-child, .cc-closeout-row > div:first-child { display: grid; gap: 2px; }
|
|
595
|
+
.cc-closeout-row code { display: block; margin-top: 3px; width: fit-content; }
|
|
596
|
+
.cc-closeout-summary { color: var(--fg-dim); margin: var(--s-1) 0 0; }
|
|
597
|
+
.cc-closeout-results { display: flex; flex-wrap: wrap; gap: var(--s-1); }
|
|
598
|
+
.cc-progress-list { list-style: none; padding: 0; margin: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(135px, 1fr)); gap: var(--s-2); }
|
|
599
|
+
.cc-progress-item a { display: grid; gap: 2px; padding: var(--s-2); border: 1px solid var(--border); border-radius: 12px; background: var(--bg-card); color: var(--fg); }
|
|
600
|
+
.cc-progress-item a:hover { text-decoration: none; border-color: var(--accent); }
|
|
601
|
+
.cc-progress-id, .cc-phase-id { font-weight: 800; } .cc-progress-badge, .cc-phase-badge { text-transform: uppercase; letter-spacing: .06em; font-size: 11px; color: var(--fg-dim); }
|
|
602
|
+
.cc-state-complete .cc-progress-badge, .cc-state-complete .cc-phase-badge { color: var(--status-complete); } .cc-state-current .cc-progress-badge, .cc-state-current .cc-phase-badge { color: var(--status-current); } .cc-state-running .cc-progress-badge, .cc-state-running .cc-phase-badge { color: var(--status-running); } .cc-state-pending .cc-progress-badge, .cc-state-pending .cc-phase-badge { color: var(--status-pending); }
|
|
603
|
+
.cc-phase-card { margin-bottom: var(--s-2); scroll-margin-top: var(--s-3); overflow: hidden; }
|
|
604
|
+
.cc-phase-card summary { padding: var(--s-3); cursor: pointer; display: flex; justify-content: space-between; gap: var(--s-3); align-items: baseline; }
|
|
605
|
+
.cc-phase-card[open] summary { border-bottom: 1px solid var(--border); }
|
|
606
|
+
.cc-phase-purpose { padding: var(--s-2) var(--s-3) 0; color: var(--fg-dim); }
|
|
607
|
+
.cc-phase-section { padding: var(--s-2) var(--s-3); }
|
|
608
|
+
.cc-phase-section ul { margin: 0; padding-left: var(--s-3); }
|
|
609
|
+
.cc-phase-overview { display: grid; gap: var(--s-1); color: var(--fg-dim); }
|
|
610
|
+
.cc-output-list li, .cc-question-list li, .cc-carry-list li, .cc-notes-list li { margin: var(--s-1) 0; }
|
|
611
|
+
.cc-question-reason { color: var(--fg-dim); font-size: 13px; margin-left: var(--s-2); }
|
|
612
|
+
.cc-pill { display: inline-block; padding: 2px 7px; border-radius: 999px; font-size: 11px; background: var(--code-bg); color: var(--fg-dim); border: 1px solid var(--border); margin: 0 3px 3px 0; }
|
|
613
|
+
.cc-pill-ok { background: color-mix(in srgb, var(--status-complete) 14%, transparent); color: var(--status-complete); border-color: var(--status-complete); } .cc-pill-bad { background: color-mix(in srgb, var(--status-error) 14%, transparent); color: var(--status-error); border-color: var(--status-error); } .cc-pill-target { color: var(--accent-2); border-color: var(--accent-2); } .cc-pill-muted, .cc-missing-path { color: var(--fg-dim); }
|
|
614
|
+
.cc-phase-jump { font-weight: 800; }
|
|
615
|
+
.cc-usage-table, .cc-timeline-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
616
|
+
.cc-usage-table th, .cc-usage-table td, .cc-timeline-table th, .cc-timeline-table td { padding: var(--s-1) var(--s-2); text-align: left; border-bottom: 1px solid var(--border); font-variant-numeric: tabular-nums; }
|
|
617
|
+
.cc-usage-table th, .cc-timeline-table th { color: var(--fg-dim); font-weight: 700; text-transform: uppercase; font-size: 11px; letter-spacing: .04em; }
|
|
618
|
+
.cc-usage-missing td:last-child { color: var(--status-running); }
|
|
619
|
+
.cc-timeline-older { margin-top: var(--s-2); }
|
|
620
|
+
.cc-footer { margin-top: var(--s-5); padding-top: var(--s-3); border-top: 1px solid var(--border); color: var(--fg-dim); font-size: 13px; }
|
|
621
|
+
@media (max-width: 900px) {
|
|
622
|
+
.cc-dashboard { display: block; }
|
|
623
|
+
.cc-sidebar { position: fixed; inset: 0 auto 0 0; width: min(86vw, 340px); transform: translateX(-105%); transition: transform .18s ease; z-index: 10; }
|
|
624
|
+
body.cc-sidebar-open .cc-sidebar { transform: translateX(0); }
|
|
625
|
+
.cc-menu-button { display: block; }
|
|
626
|
+
.cc-main { padding: var(--s-3); padding-top: var(--s-5); }
|
|
627
|
+
.cc-stat-grid, .cc-artifact-row, .cc-closeout-row { grid-template-columns: 1fr; }
|
|
628
|
+
.cc-header-meta, .cc-run-meta, .cc-usage-totals { grid-template-columns: 1fr; }
|
|
629
|
+
table { display: block; overflow-x: auto; white-space: nowrap; }
|
|
630
|
+
}
|
|
631
|
+
@media print {
|
|
632
|
+
body { background: #fff; color: #111; }
|
|
633
|
+
.cc-sidebar, .cc-menu-button, script { display: none !important; }
|
|
634
|
+
.cc-dashboard { display: block; } .cc-main { max-width: none; padding: 0; } .cc-card, .cc-header, .cc-phase-card { box-shadow: none; break-inside: avoid; }
|
|
635
|
+
}
|
|
636
|
+
</style>`;
|
|
637
|
+
}
|