@azure-id/orc 0.55.0 → 0.56.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.
@@ -1,447 +1,453 @@
1
- "use strict";
2
- /* panels/overview.js — orc ui client
3
- FINDING_ROUTE (a caution routes to the panel that can CLEAR it), the
4
- attention list, the stat tiles.
5
-
6
- Loaded by app.html in the order its numeric prefix names. Classic
7
- script, no import/export: an ES module import carries no query string,
8
- and every static request here needs the per-launch session token. */
9
-
10
-
11
- /* ================================================================ OVERVIEW == */
12
-
13
- /* WHERE A PROBLEM IS ACTUALLY FIXED (v0.43.6).
14
- `orc doctor` reports every problem in one list, and the panel used to send
15
- the whole list to Maintenance with a single "Open Maintenance" button. That
16
- is right for the install-footprint findings — version skew, orphans, missing
17
- files, unwired hooks — because `orc update` / `doctor --fix` really is where
18
- those are repaired. It was WRONG for the ones whose fix lives on another
19
- panel, and `diy-stale` was the one people hit: the flow is recompiled with
20
- `orc diy compile`, which is a button on FLOW. Sending them to Maintenance
21
- pointed at a page with no control for the thing it was complaining about.
22
-
23
- So the routing is a table keyed on the finding id the CLI already emits.
24
- `null` means there is nothing to press anywhere — a dangling trace pointer
25
- clears itself on the next run, and offering a button for it would be a lie. */
26
- const FINDING_ROUTE = {
27
- "diy-stale": { panel: "flow", cta: "overview.item.diyStale.cta" },
28
- "trace-pointer-dangling": { panel: null },
29
- // v0.46.0. Correct by the rule, not by default: `export-stale` is cleared by
30
- // `orc export`, which IS a CLI write Maintenance can run — so Maintenance is
31
- // genuinely the panel that can clear it, not merely the fallback.
32
- "export-stale": { panel: "maintenance", cta: "overview.item.exportStale.cta" },
33
- "pact-broken": { panel: "pact", cta: "overview.item.pactBroken.cta" },
34
- // v0.48.1. Same rule again: `orc doc audit` and `orc doc ship` are both on
35
- // DOCS, so that is where a drifted document has to send you.
36
- "doc-drifted": { panel: "docs", cta: "overview.item.docDrifted.cta" },
37
- "boundary-refuse": { panel: "boundary", cta: "overview.item.boundaryRefuse.cta" },
38
- // v0.49.1. Same rule a third time. `orc wiki sync` is a BUTTON on Knowledge
39
- // and it is free, and `orc wiki plan` (which ranks the refresh) is the card
40
- // directly above it — so Knowledge is genuinely the panel that can clear
41
- // both, not merely the fallback.
42
- "wiki-unregistered": { panel: "knowledge", cta: "overview.item.wikiUnregistered.cta" },
43
- "wiki-debt": { panel: "knowledge", cta: "overview.item.wikiDebt.cta" },
44
- // v0.50.0. `orc doctor` collapses the whole `orc extra` subsystem into ONE
45
- // finding carrying the count and the ids — a doctor that recited eleven ids
46
- // for a subsystem most repos never arm is a doctor people learn to scroll
47
- // past. It routes to Extra because that is where every one of them is
48
- // rendered beside the connection it is about, and where the connection test
49
- // that clears most of them lives.
50
- "extra-findings": { panel: "extra", cta: "overview.item.extraFindings.cta" },
51
- };
52
- const DEFAULT_FINDING_ROUTE = { panel: "maintenance", cta: "overview.item.doctor.cta" };
53
- const findingRoute = (id) => FINDING_ROUTE[id] || DEFAULT_FINDING_ROUTE;
54
-
55
- PANELS.overview = function (host) {
56
- head(host, t("overview.title"), t("overview.sub"));
57
- section(
58
- host,
59
- () => read("/api/overview").then((r) => r.data),
60
- (d) => {
61
- const out = frag();
62
- const doctor = d.doctor || {};
63
- const w = d.wiki || {};
64
- const p = d.patterns || {};
65
- // Rows since v0.49.2: `{ slug, updated_ms, lane }`. Everything that only
66
- // wants a count still reads `.length`.
67
- const waiting = d.waiting || [];
68
- const findings = doctor.findings || [];
69
-
70
- /* --- the tiles ------------------------------------------------------ */
71
- const stats = el("div", "grid grid-3");
72
- stats.append(
73
- statTile(
74
- t("overview.tile.install"),
75
- doctor.ok ? t("overview.tile.installHealthy") : tn(findings.length, "overview.tile.installIssues"),
76
- doctor.installed_version
77
- ? t("overview.tile.installNote", { payload: doctor.installed_version, cli: doctor.package_version })
78
- : "",
79
- doctor.ok ? "ok" : "warn",
80
- doctor.ok ? null : "maintenance"
81
- )
82
- );
83
- stats.append(
84
- statTile(
85
- t("overview.tile.wiki"),
86
- // TIER and STATE are CLI vocabulary (FRESH / AGING / STALE /
87
- // unregistered) — the words the docs and every other lane use. They
88
- // are shown as-is in both languages; only the sentence under them is
89
- // translated.
90
- w.state === "registered" ? w.tier || t("overview.tile.wikiUnknown") : String(w.state || "none").toUpperCase(),
91
- w.state === "registered"
92
- ? t("overview.tile.wikiNote", { docs: w.docs, scan: w.last_scan || "?" })
93
- : t("overview.tile.wikiNone"),
94
- w.tier === "FRESH" ? "ok" : w.tier === "AGING" ? "warn" : w.state === "none" ? "" : "bad",
95
- "knowledge"
96
- )
97
- );
98
- stats.append(
99
- statTile(
100
- t("overview.tile.waiting"),
101
- String(waiting.length),
102
- t("overview.tile.waitingNote", { n: d.runs_total || 0 }),
103
- waiting.length ? "warn" : "ok",
104
- "runs"
105
- )
106
- );
107
- const langs = (p.patterns || []).map((x) => x.lang);
108
- stats.append(
109
- statTile(
110
- t("overview.tile.patterns"),
111
- String(langs.length),
112
- langs.length ? langs.join(", ") : t("overview.tile.patternsNone"),
113
- langs.length ? "ok" : "",
114
- "knowledge"
115
- )
116
- );
117
-
118
- /* v0.46.0 three chips repeating the CLI's OWN state words. A chip with
119
- nothing to say still renders its good state: an absent chip and a
120
- healthy one must never look the same. */
121
- const pc = d.pact;
122
- const bc = d.boundary;
123
- const wd = d.wiki_debt;
124
- stats.append(
125
- statTile(
126
- t("overview.tile.pact"),
127
- pc && pc.ok
128
- ? (pc.counts.BROKEN ? pc.counts.BROKEN + " BROKEN" : pc.counts.DRIFTED ? pc.counts.DRIFTED + " DRIFTED" : pc.counts.HOLDING + " HOLDING")
129
- : t("overview.tile.none"),
130
- pc && pc.ok ? t("overview.tile.pactNote", { n: pc.entries, u: pc.counts.UNCHECKABLE }) : t("overview.tile.pactNone"),
131
- pc && pc.ok ? (pc.counts.BROKEN ? "bad" : pc.counts.DRIFTED ? "warn" : "ok") : "",
132
- pc && pc.ok ? "pact" : null
133
- )
134
- );
135
- stats.append(
136
- statTile(
137
- t("overview.tile.boundary"),
138
- bc && bc.cards && bc.cards.length
139
- ? (bc.counts.REFUSE ? bc.counts.REFUSE + " REFUSE" : bc.counts.ESCALATE ? bc.counts.ESCALATE + " ESCALATE" : bc.counts.EXECUTE + " EXECUTE")
140
- : t("overview.tile.none"),
141
- bc && bc.cards && bc.cards.length ? t("overview.tile.boundaryNote", { n: bc.cards.length, stale: bc.stale }) : t("overview.tile.boundaryNone"),
142
- bc && bc.cards && bc.cards.length ? (bc.counts.REFUSE ? "bad" : bc.counts.ESCALATE ? "warn" : "ok") : "",
143
- bc && bc.cards && bc.cards.length ? "boundary" : null
144
- )
145
- );
146
- stats.append(
147
- statTile(
148
- t("overview.tile.debt"),
149
- wd && wd.ok && wd.pending ? String(wd.pending) : "0",
150
- wd && wd.ok && wd.pending
151
- ? t("overview.tile.debtNote", { tier: wd.tier, tok: kTokUi((wd.tokens || {}).input + (wd.tokens || {}).cache_write + (wd.tokens || {}).cache_read + (wd.tokens || {}).output) })
152
- : t("overview.tile.debtNone"),
153
- wd && wd.ok && wd.pending ? "warn" : "ok",
154
- "knowledge"
155
- )
156
- );
157
-
158
- // The version check crosses the network, so it must never hold up the
159
- // tiles beside it: the tile renders in its pending state and fills itself
160
- // in when the answer lands.
161
- const vt = statTile(t("overview.tile.version"), t("common.checking"), t("overview.tile.versionChecking"), "");
162
- vt.classList.add("stat-pending");
163
- stats.append(vt);
164
- versionInfo()
165
- .then((v) => {
166
- const s = versionState(v);
167
- vt.classList.remove("stat-pending");
168
- vt.replaceChildren();
169
- vt.append(el("div", "stat-label", t("overview.tile.version")));
170
- const val = el("div", "stat-value" + (s.kind ? " stat-" + s.kind : ""));
171
- val.append(document.createTextNode(v && v.version ? "v" + v.version : "?"), chip(s.label, s.kind));
172
- vt.append(val, el("div", "stat-note", s.note));
173
- })
174
- .catch(() => {
175
- vt.classList.remove("stat-pending");
176
- vt.replaceChildren(
177
- el("div", "stat-label", t("overview.tile.version")),
178
- el("div", "stat-value", "?"),
179
- el("div", "stat-note", t("overview.tile.versionFailed"))
180
- );
181
- });
182
- out.append(stats);
183
-
184
- /* --- worth doing ----------------------------------------------------
185
- One list, in severity order, of everything that wants a decision — and
186
- every row carries the panel where the fix actually is. This is the
187
- panel's answer to "what now", and it is the only place the wiki tier
188
- turns into advice instead of a colour. */
189
- out.append(attentionCard(d, findings));
190
-
191
- /* --- waiting runs --------------------------------------------------- */
192
- if (waiting.length) {
193
- const c = card(t("overview.waiting.title"));
194
- c.append(el("div", "note", t("overview.waiting.note")));
195
- const list = el("div", "run-list");
196
- for (const r of waiting) {
197
- // The CARD CONTRACT (v0.49.2). `.run-card` is a four-column grid —
198
- // caret, chip, mid, age — and this row used to append three children
199
- // with no caret, so the chip landed in the 16px caret column and
200
- // printed over an 88px-wide slug. One column short is not a rounding
201
- // error; it is the whole card. This row navigates rather than expands,
202
- // so it declares `.no-caret` and fills the three columns that variant
203
- // has, in order.
204
- const b = el("button", "run-card no-caret");
205
- b.type = "button";
206
- b.append(chip("waiting", "warn"));
207
- const mid = el("div", "run-mid");
208
- mid.append(el("div", "run-slug", r.slug));
209
- mid.append(el("div", "run-where", r.lane ? r.lane + " · " + t("overview.waiting.where") : t("overview.waiting.where")));
210
- b.append(mid, el("div", "run-age", r.updated_ms ? t("overview.waiting.since", { age: relAge(r.updated_ms) }) : ""));
211
- b.addEventListener("click", () => {
212
- location.hash = "#/runs?slug=" + encodeURIComponent(r.slug);
213
- });
214
- list.append(b);
215
- // A CAUTION ROUTES TO THE PANEL THAT CAN CLEAR IT and here the panel
216
- // that can clear it is this one. A waiting run is what blocks the
217
- // upgrade preview, so the way out sits beside the thing complaining.
218
- const done = el("button", "btn btn-sm btn-ghost", t("runs.close.button"));
219
- done.type = "button";
220
- done.addEventListener("click", (ev) => {
221
- ev.stopPropagation();
222
- confirmRunClose(r.slug, () => route());
223
- });
224
- const act = el("div", "row-actions run-card-actions");
225
- act.append(done);
226
- list.append(act);
227
- }
228
- c.append(list);
229
- out.append(c);
230
- }
231
-
232
- /* --- v0.54.0: foreign dispatches that never reported back ------------
233
- ONE LINE, and only when there is something to say. Money was spent and
234
- work is half-done, and nothing will look at it again unless somebody is
235
- told. It ROUTES to the panel that can clear it (Extra ▸ Recovery) — it
236
- never resumes anything, because continuing a third party's unfinished
237
- write without asking is the same class of act as routing off Claude
238
- without saying so. */
239
- const ej = d.extra_journal;
240
- if (ej && ej.orphans) {
241
- const c = card(t("overview.extraOrphan.title"));
242
- c.append(el("div", "note", t("overview.extraOrphan.note")));
243
- // `.no-caret` this row NAVIGATES rather than expands, and every
244
- // `.run-card` variant declares its own column count (v0.49.2). A card
245
- // one column short is not a rounding error; it is the whole card.
246
- const b = el("button", "run-card no-caret");
247
- b.type = "button";
248
- b.append(chip(tn(ej.orphans, "overview.extraOrphan.chip"), "bad"));
249
- const mid = el("div", "run-mid");
250
- mid.append(el("div", "run-slug", t("overview.extraOrphan.slug")));
251
- mid.append(el("div", "run-where", t("overview.extraOrphan.where")));
252
- b.append(mid, el("div", "run-age", ""));
253
- b.addEventListener("click", () => {
254
- location.hash = "#/extra";
255
- });
256
- c.append(b);
257
- out.append(c);
258
- }
259
-
260
- /* --- the raw doctor list -------------------------------------------
261
- Kept below the actionable card, because it is the EVIDENCE: the exact
262
- message the CLI printed, unedited and untranslated, so what you read
263
- here is what you would read in a terminal. */
264
- if (!doctor.ok && findings.length) {
265
- const c = card(t("overview.doctor.title"));
266
- for (const f of findings) {
267
- const row = el("div", "finding");
268
- row.append(chip(f.fixable ? t("overview.doctor.fixable") : t("overview.doctor.manual"), f.fixable ? "info" : "warn"));
269
- const detail = el("div");
270
- detail.append(el("div", null, f.message));
271
- const r = findingRoute(f.id);
272
- if (r.panel) {
273
- const go = el("button", "btn btn-ghost btn-sm", t(r.cta));
274
- go.type = "button";
275
- go.addEventListener("click", () => (location.hash = "#/" + r.panel));
276
- detail.append(go);
277
- } else {
278
- detail.append(el("div", "note", t("overview.attention.nothingToDo")));
279
- }
280
- row.append(detail);
281
- c.append(row);
282
- }
283
- out.append(c);
284
- }
285
-
286
- /* --- where things are ----------------------------------------------- */
287
- const paths = card(t("overview.paths.title"));
288
- const where = d.where || {};
289
- paths.append(
290
- kvList(
291
- [
292
- [t("overview.paths.project"), where.project_root],
293
- [t("overview.paths.config"), where.config],
294
- [t("overview.paths.skills"), where.skills],
295
- [t("overview.paths.runs"), where.run_dir],
296
- [t("overview.paths.traces"), where.log_dir],
297
- ],
298
- true
299
- )
300
- );
301
- out.append(paths);
302
-
303
- const know = card(t("overview.know.title"));
304
- know.append(
305
- kvList([
306
- [t("overview.know.patterns"), langs.join(", ") || t("common.none")],
307
- [t("overview.know.tags"), w.crosslink_tags === undefined ? "" : String(w.crosslink_tags)],
308
- // `state` and `reason` are the CLI's — shown verbatim.
309
- [t("overview.know.diy"), d.diy ? `${d.diy.state} — ${d.diy.reason}` : ""],
310
- ])
311
- );
312
- out.append(know);
313
-
314
- return out;
315
- }
316
- );
317
- };
318
-
319
- // The actionable list. Each entry is {kind, title, body, panel, cta} and knows
320
- // where its fix lives — never a blanket "go to Maintenance".
321
- function attentionCard(d, findings) {
322
- const w = d.wiki || {};
323
- const p = d.patterns || {};
324
- const items = [];
325
-
326
- // 1. Install findings, routed per id.
327
- for (const f of findings) {
328
- const r = findingRoute(f.id);
329
- if (f.id === "diy-stale") {
330
- items.push({
331
- kind: "warn",
332
- title: t("overview.item.diyStale.title"),
333
- body: t("overview.item.diyStale.body"),
334
- evidence: d.diy && d.diy.reason,
335
- panel: "flow",
336
- cta: t("overview.item.diyStale.cta"),
337
- });
338
- } else if (r.panel) {
339
- items.push({
340
- kind: f.fixable ? "warn" : "bad",
341
- // A doctor finding is already a sentence written for a human — using it
342
- // as the title beats paraphrasing it into something less exact.
343
- title: f.message,
344
- panel: r.panel,
345
- cta: t(r.cta),
346
- });
347
- }
348
- }
349
-
350
- // 2. The wiki. THIS is the recommendation the panel was missing: an AGING
351
- // wiki is not an error, it is the moment a refresh is still cheap.
352
- if (!w.state || w.state === "none") {
353
- items.push({ kind: "info", title: t("overview.item.wikiNone.title"), body: t("overview.item.wikiNone.body"), panel: "knowledge", cta: t("overview.item.wikiNone.cta") });
354
- } else if (w.state !== "registered") {
355
- items.push({ kind: "warn", title: t("overview.item.wikiUnregistered.title"), body: t("overview.item.wikiUnregistered.body"), panel: "knowledge", cta: t("overview.item.wikiUnregistered.cta") });
356
- } else if (w.tier === "AGING") {
357
- items.push({
358
- kind: "warn",
359
- title: t("overview.item.wikiAging.title"),
360
- body: t("overview.item.wikiAging.body"),
361
- evidence: (w.reasons || [])[0],
362
- panel: "knowledge",
363
- cta: t("overview.item.wikiAging.cta"),
364
- });
365
- } else if (w.tier && w.tier !== "FRESH") {
366
- items.push({
367
- kind: "bad",
368
- title: t("overview.item.wikiStale.title"),
369
- body: t("overview.item.wikiStale.body"),
370
- evidence: (w.reasons || [])[0],
371
- panel: "knowledge",
372
- cta: t("overview.item.wikiStale.cta"),
373
- });
374
- }
375
-
376
- // 3. No cached pattern — not a fault, but the cheapest quality win there is.
377
- if (!(p.patterns || []).length)
378
- items.push({ kind: "info", title: t("overview.item.patterns.title"), body: t("overview.item.patterns.body"), panel: "knowledge", cta: t("overview.item.patterns.cta") });
379
-
380
- // 4. Runs left waiting.
381
- if ((d.waiting || []).length)
382
- items.push({
383
- kind: "warn",
384
- title: tn((d.waiting || []).length, "overview.item.waiting.title"),
385
- body: t("overview.item.waiting.body"),
386
- panel: "runs",
387
- cta: t("overview.item.waiting.cta"),
388
- });
389
-
390
- const c = card(t("overview.attention.title"), chip(String(items.length), items.length ? "warn" : "ok"));
391
- c.id = "attention-card";
392
-
393
- if (!items.length) {
394
- const ok = el("div", "all-clear");
395
- ok.append(el("div", "all-clear-mark", "✓"));
396
- const txt = el("div");
397
- txt.append(el("div", "all-clear-title", t("overview.attention.allClear")));
398
- txt.append(el("div", "note", t("overview.attention.allClearHint")));
399
- ok.append(txt);
400
- c.append(ok);
401
- }
402
-
403
- const list = el("div", "todo-list");
404
- const order = { bad: 0, warn: 1, info: 2 };
405
- items.sort((a, b) => (order[a.kind] || 3) - (order[b.kind] || 3));
406
- for (const it of items) {
407
- const row = el("button", "todo todo-" + it.kind);
408
- row.type = "button";
409
- row.append(el("span", "todo-mark"));
410
- const mid = el("div", "todo-body");
411
- mid.append(el("div", "todo-title", it.title));
412
- if (it.body) mid.append(el("div", "todo-text", it.body));
413
- // The CLI's own words for WHY, kept verbatim under our explanation.
414
- if (it.evidence) mid.append(el("div", "todo-evidence", it.evidence));
415
- row.append(mid, el("span", "todo-cta", it.cta));
416
- row.addEventListener("click", () => (location.hash = "#/" + it.panel));
417
- list.append(row);
418
- }
419
- if (items.length) c.append(list);
420
-
421
- // The update offer is appended asynchronously so a slow network check never
422
- // delays the list it arrives as one more row when the answer does.
423
- versionInfo()
424
- .then((v) => {
425
- if (!v || !v.update_available) return;
426
- const row = el("button", "todo todo-info todo-late");
427
- row.type = "button";
428
- row.append(el("span", "todo-mark"));
429
- const mid = el("div", "todo-body");
430
- mid.append(el("div", "todo-title", t("overview.item.update.title", { latest: v.latest })));
431
- mid.append(el("div", "todo-text", t("overview.item.update.body", { version: v.version })));
432
- row.append(mid, el("span", "todo-cta", t("overview.item.update.cta")));
433
- row.addEventListener("click", () => (location.hash = "#/maintenance"));
434
- if (!list.isConnected) c.append(list);
435
- list.append(row);
436
- const count = c.querySelector(".card-head .chip");
437
- if (count) {
438
- count.textContent = String(list.children.length);
439
- count.className = "chip chip-warn";
440
- }
441
- const clear = c.querySelector(".all-clear");
442
- if (clear) clear.remove();
443
- })
444
- .catch(() => {});
445
-
446
- return c;
447
- }
1
+ "use strict";
2
+ /* panels/overview.js — orc ui client
3
+ FINDING_ROUTE (a caution routes to the panel that can CLEAR it), the
4
+ attention list, the stat tiles.
5
+
6
+ Loaded by app.html in the order its numeric prefix names. Classic
7
+ script, no import/export: an ES module import carries no query string,
8
+ and every static request here needs the per-launch session token. */
9
+
10
+
11
+ /* ================================================================ OVERVIEW == */
12
+
13
+ /* WHERE A PROBLEM IS ACTUALLY FIXED (v0.43.6).
14
+ `orc doctor` reports every problem in one list, and the panel used to send
15
+ the whole list to Maintenance with a single "Open Maintenance" button. That
16
+ is right for the install-footprint findings — version skew, orphans, missing
17
+ files, unwired hooks — because `orc update` / `doctor --fix` really is where
18
+ those are repaired. It was WRONG for the ones whose fix lives on another
19
+ panel, and `diy-stale` was the one people hit: the flow is recompiled with
20
+ `orc diy compile`, which is a button on FLOW. Sending them to Maintenance
21
+ pointed at a page with no control for the thing it was complaining about.
22
+
23
+ So the routing is a table keyed on the finding id the CLI already emits.
24
+ `null` means there is nothing to press anywhere — a dangling trace pointer
25
+ clears itself on the next run, and offering a button for it would be a lie. */
26
+ const FINDING_ROUTE = {
27
+ "diy-stale": { panel: "flow", cta: "overview.item.diyStale.cta" },
28
+ "trace-pointer-dangling": { panel: null },
29
+ // v0.46.0. Correct by the rule, not by default: `export-stale` is cleared by
30
+ // `orc export`, which IS a CLI write Maintenance can run — so Maintenance is
31
+ // genuinely the panel that can clear it, not merely the fallback.
32
+ "export-stale": { panel: "maintenance", cta: "overview.item.exportStale.cta" },
33
+ "pact-broken": { panel: "pact", cta: "overview.item.pactBroken.cta" },
34
+ // v0.48.1. Same rule again: `orc doc audit` and `orc doc ship` are both on
35
+ // DOCS, so that is where a drifted document has to send you.
36
+ "doc-drifted": { panel: "docs", cta: "overview.item.docDrifted.cta" },
37
+ "boundary-refuse": { panel: "boundary", cta: "overview.item.boundaryRefuse.cta" },
38
+ // v0.49.1. Same rule a third time. `orc wiki sync` is a BUTTON on Knowledge
39
+ // and it is free, and `orc wiki plan` (which ranks the refresh) is the card
40
+ // directly above it — so Knowledge is genuinely the panel that can clear
41
+ // both, not merely the fallback.
42
+ "wiki-unregistered": { panel: "knowledge", cta: "overview.item.wikiUnregistered.cta" },
43
+ "wiki-debt": { panel: "knowledge", cta: "overview.item.wikiDebt.cta" },
44
+ // v0.50.0. `orc doctor` collapses the whole `orc extra` subsystem into ONE
45
+ // finding carrying the count and the ids — a doctor that recited eleven ids
46
+ // for a subsystem most repos never arm is a doctor people learn to scroll
47
+ // past. It routes to Extra because that is where every one of them is
48
+ // rendered beside the connection it is about, and where the connection test
49
+ // that clears most of them lives.
50
+ "extra-findings": { panel: "extra", cta: "overview.item.extraFindings.cta" },
51
+ // v0.56.0. The rule again, and this time the finding is about the TOOL rather
52
+ // than the project: a legacy global `orc` package owns the `orc` command and
53
+ // blocks every install source. `orc upgrade` is the thing that clears it (it
54
+ // evicts the old package first), and `upgrade` is an action row on
55
+ // MAINTENANCE so that is genuinely the panel that can clear it.
56
+ "legacy-global-package": { panel: "maintenance", cta: "overview.item.legacyGlobalPackage.cta" },
57
+ };
58
+ const DEFAULT_FINDING_ROUTE = { panel: "maintenance", cta: "overview.item.doctor.cta" };
59
+ const findingRoute = (id) => FINDING_ROUTE[id] || DEFAULT_FINDING_ROUTE;
60
+
61
+ PANELS.overview = function (host) {
62
+ head(host, t("overview.title"), t("overview.sub"));
63
+ section(
64
+ host,
65
+ () => read("/api/overview").then((r) => r.data),
66
+ (d) => {
67
+ const out = frag();
68
+ const doctor = d.doctor || {};
69
+ const w = d.wiki || {};
70
+ const p = d.patterns || {};
71
+ // Rows since v0.49.2: `{ slug, updated_ms, lane }`. Everything that only
72
+ // wants a count still reads `.length`.
73
+ const waiting = d.waiting || [];
74
+ const findings = doctor.findings || [];
75
+
76
+ /* --- the tiles ------------------------------------------------------ */
77
+ const stats = el("div", "grid grid-3");
78
+ stats.append(
79
+ statTile(
80
+ t("overview.tile.install"),
81
+ doctor.ok ? t("overview.tile.installHealthy") : tn(findings.length, "overview.tile.installIssues"),
82
+ doctor.installed_version
83
+ ? t("overview.tile.installNote", { payload: doctor.installed_version, cli: doctor.package_version })
84
+ : "",
85
+ doctor.ok ? "ok" : "warn",
86
+ doctor.ok ? null : "maintenance"
87
+ )
88
+ );
89
+ stats.append(
90
+ statTile(
91
+ t("overview.tile.wiki"),
92
+ // TIER and STATE are CLI vocabulary (FRESH / AGING / STALE /
93
+ // unregistered) — the words the docs and every other lane use. They
94
+ // are shown as-is in both languages; only the sentence under them is
95
+ // translated.
96
+ w.state === "registered" ? w.tier || t("overview.tile.wikiUnknown") : String(w.state || "none").toUpperCase(),
97
+ w.state === "registered"
98
+ ? t("overview.tile.wikiNote", { docs: w.docs, scan: w.last_scan || "?" })
99
+ : t("overview.tile.wikiNone"),
100
+ w.tier === "FRESH" ? "ok" : w.tier === "AGING" ? "warn" : w.state === "none" ? "" : "bad",
101
+ "knowledge"
102
+ )
103
+ );
104
+ stats.append(
105
+ statTile(
106
+ t("overview.tile.waiting"),
107
+ String(waiting.length),
108
+ t("overview.tile.waitingNote", { n: d.runs_total || 0 }),
109
+ waiting.length ? "warn" : "ok",
110
+ "runs"
111
+ )
112
+ );
113
+ const langs = (p.patterns || []).map((x) => x.lang);
114
+ stats.append(
115
+ statTile(
116
+ t("overview.tile.patterns"),
117
+ String(langs.length),
118
+ langs.length ? langs.join(", ") : t("overview.tile.patternsNone"),
119
+ langs.length ? "ok" : "",
120
+ "knowledge"
121
+ )
122
+ );
123
+
124
+ /* v0.46.0 — three chips repeating the CLI's OWN state words. A chip with
125
+ nothing to say still renders its good state: an absent chip and a
126
+ healthy one must never look the same. */
127
+ const pc = d.pact;
128
+ const bc = d.boundary;
129
+ const wd = d.wiki_debt;
130
+ stats.append(
131
+ statTile(
132
+ t("overview.tile.pact"),
133
+ pc && pc.ok
134
+ ? (pc.counts.BROKEN ? pc.counts.BROKEN + " BROKEN" : pc.counts.DRIFTED ? pc.counts.DRIFTED + " DRIFTED" : pc.counts.HOLDING + " HOLDING")
135
+ : t("overview.tile.none"),
136
+ pc && pc.ok ? t("overview.tile.pactNote", { n: pc.entries, u: pc.counts.UNCHECKABLE }) : t("overview.tile.pactNone"),
137
+ pc && pc.ok ? (pc.counts.BROKEN ? "bad" : pc.counts.DRIFTED ? "warn" : "ok") : "",
138
+ pc && pc.ok ? "pact" : null
139
+ )
140
+ );
141
+ stats.append(
142
+ statTile(
143
+ t("overview.tile.boundary"),
144
+ bc && bc.cards && bc.cards.length
145
+ ? (bc.counts.REFUSE ? bc.counts.REFUSE + " REFUSE" : bc.counts.ESCALATE ? bc.counts.ESCALATE + " ESCALATE" : bc.counts.EXECUTE + " EXECUTE")
146
+ : t("overview.tile.none"),
147
+ bc && bc.cards && bc.cards.length ? t("overview.tile.boundaryNote", { n: bc.cards.length, stale: bc.stale }) : t("overview.tile.boundaryNone"),
148
+ bc && bc.cards && bc.cards.length ? (bc.counts.REFUSE ? "bad" : bc.counts.ESCALATE ? "warn" : "ok") : "",
149
+ bc && bc.cards && bc.cards.length ? "boundary" : null
150
+ )
151
+ );
152
+ stats.append(
153
+ statTile(
154
+ t("overview.tile.debt"),
155
+ wd && wd.ok && wd.pending ? String(wd.pending) : "0",
156
+ wd && wd.ok && wd.pending
157
+ ? t("overview.tile.debtNote", { tier: wd.tier, tok: kTokUi((wd.tokens || {}).input + (wd.tokens || {}).cache_write + (wd.tokens || {}).cache_read + (wd.tokens || {}).output) })
158
+ : t("overview.tile.debtNone"),
159
+ wd && wd.ok && wd.pending ? "warn" : "ok",
160
+ "knowledge"
161
+ )
162
+ );
163
+
164
+ // The version check crosses the network, so it must never hold up the
165
+ // tiles beside it: the tile renders in its pending state and fills itself
166
+ // in when the answer lands.
167
+ const vt = statTile(t("overview.tile.version"), t("common.checking"), t("overview.tile.versionChecking"), "");
168
+ vt.classList.add("stat-pending");
169
+ stats.append(vt);
170
+ versionInfo()
171
+ .then((v) => {
172
+ const s = versionState(v);
173
+ vt.classList.remove("stat-pending");
174
+ vt.replaceChildren();
175
+ vt.append(el("div", "stat-label", t("overview.tile.version")));
176
+ const val = el("div", "stat-value" + (s.kind ? " stat-" + s.kind : ""));
177
+ val.append(document.createTextNode(v && v.version ? "v" + v.version : "?"), chip(s.label, s.kind));
178
+ vt.append(val, el("div", "stat-note", s.note));
179
+ })
180
+ .catch(() => {
181
+ vt.classList.remove("stat-pending");
182
+ vt.replaceChildren(
183
+ el("div", "stat-label", t("overview.tile.version")),
184
+ el("div", "stat-value", "?"),
185
+ el("div", "stat-note", t("overview.tile.versionFailed"))
186
+ );
187
+ });
188
+ out.append(stats);
189
+
190
+ /* --- worth doing ----------------------------------------------------
191
+ One list, in severity order, of everything that wants a decision — and
192
+ every row carries the panel where the fix actually is. This is the
193
+ panel's answer to "what now", and it is the only place the wiki tier
194
+ turns into advice instead of a colour. */
195
+ out.append(attentionCard(d, findings));
196
+
197
+ /* --- waiting runs --------------------------------------------------- */
198
+ if (waiting.length) {
199
+ const c = card(t("overview.waiting.title"));
200
+ c.append(el("div", "note", t("overview.waiting.note")));
201
+ const list = el("div", "run-list");
202
+ for (const r of waiting) {
203
+ // The CARD CONTRACT (v0.49.2). `.run-card` is a four-column grid —
204
+ // caret, chip, mid, age — and this row used to append three children
205
+ // with no caret, so the chip landed in the 16px caret column and
206
+ // printed over an 88px-wide slug. One column short is not a rounding
207
+ // error; it is the whole card. This row navigates rather than expands,
208
+ // so it declares `.no-caret` and fills the three columns that variant
209
+ // has, in order.
210
+ const b = el("button", "run-card no-caret");
211
+ b.type = "button";
212
+ b.append(chip("waiting", "warn"));
213
+ const mid = el("div", "run-mid");
214
+ mid.append(el("div", "run-slug", r.slug));
215
+ mid.append(el("div", "run-where", r.lane ? r.lane + " · " + t("overview.waiting.where") : t("overview.waiting.where")));
216
+ b.append(mid, el("div", "run-age", r.updated_ms ? t("overview.waiting.since", { age: relAge(r.updated_ms) }) : ""));
217
+ b.addEventListener("click", () => {
218
+ location.hash = "#/runs?slug=" + encodeURIComponent(r.slug);
219
+ });
220
+ list.append(b);
221
+ // A CAUTION ROUTES TO THE PANEL THAT CAN CLEAR IT — and here the panel
222
+ // that can clear it is this one. A waiting run is what blocks the
223
+ // upgrade preview, so the way out sits beside the thing complaining.
224
+ const done = el("button", "btn btn-sm btn-ghost", t("runs.close.button"));
225
+ done.type = "button";
226
+ done.addEventListener("click", (ev) => {
227
+ ev.stopPropagation();
228
+ confirmRunClose(r.slug, () => route());
229
+ });
230
+ const act = el("div", "row-actions run-card-actions");
231
+ act.append(done);
232
+ list.append(act);
233
+ }
234
+ c.append(list);
235
+ out.append(c);
236
+ }
237
+
238
+ /* --- v0.54.0: foreign dispatches that never reported back ------------
239
+ ONE LINE, and only when there is something to say. Money was spent and
240
+ work is half-done, and nothing will look at it again unless somebody is
241
+ told. It ROUTES to the panel that can clear it (Extra ▸ Recovery) — it
242
+ never resumes anything, because continuing a third party's unfinished
243
+ write without asking is the same class of act as routing off Claude
244
+ without saying so. */
245
+ const ej = d.extra_journal;
246
+ if (ej && ej.orphans) {
247
+ const c = card(t("overview.extraOrphan.title"));
248
+ c.append(el("div", "note", t("overview.extraOrphan.note")));
249
+ // `.no-caret` this row NAVIGATES rather than expands, and every
250
+ // `.run-card` variant declares its own column count (v0.49.2). A card
251
+ // one column short is not a rounding error; it is the whole card.
252
+ const b = el("button", "run-card no-caret");
253
+ b.type = "button";
254
+ b.append(chip(tn(ej.orphans, "overview.extraOrphan.chip"), "bad"));
255
+ const mid = el("div", "run-mid");
256
+ mid.append(el("div", "run-slug", t("overview.extraOrphan.slug")));
257
+ mid.append(el("div", "run-where", t("overview.extraOrphan.where")));
258
+ b.append(mid, el("div", "run-age", ""));
259
+ b.addEventListener("click", () => {
260
+ location.hash = "#/extra";
261
+ });
262
+ c.append(b);
263
+ out.append(c);
264
+ }
265
+
266
+ /* --- the raw doctor list -------------------------------------------
267
+ Kept below the actionable card, because it is the EVIDENCE: the exact
268
+ message the CLI printed, unedited and untranslated, so what you read
269
+ here is what you would read in a terminal. */
270
+ if (!doctor.ok && findings.length) {
271
+ const c = card(t("overview.doctor.title"));
272
+ for (const f of findings) {
273
+ const row = el("div", "finding");
274
+ row.append(chip(f.fixable ? t("overview.doctor.fixable") : t("overview.doctor.manual"), f.fixable ? "info" : "warn"));
275
+ const detail = el("div");
276
+ detail.append(el("div", null, f.message));
277
+ const r = findingRoute(f.id);
278
+ if (r.panel) {
279
+ const go = el("button", "btn btn-ghost btn-sm", t(r.cta));
280
+ go.type = "button";
281
+ go.addEventListener("click", () => (location.hash = "#/" + r.panel));
282
+ detail.append(go);
283
+ } else {
284
+ detail.append(el("div", "note", t("overview.attention.nothingToDo")));
285
+ }
286
+ row.append(detail);
287
+ c.append(row);
288
+ }
289
+ out.append(c);
290
+ }
291
+
292
+ /* --- where things are ----------------------------------------------- */
293
+ const paths = card(t("overview.paths.title"));
294
+ const where = d.where || {};
295
+ paths.append(
296
+ kvList(
297
+ [
298
+ [t("overview.paths.project"), where.project_root],
299
+ [t("overview.paths.config"), where.config],
300
+ [t("overview.paths.skills"), where.skills],
301
+ [t("overview.paths.runs"), where.run_dir],
302
+ [t("overview.paths.traces"), where.log_dir],
303
+ ],
304
+ true
305
+ )
306
+ );
307
+ out.append(paths);
308
+
309
+ const know = card(t("overview.know.title"));
310
+ know.append(
311
+ kvList([
312
+ [t("overview.know.patterns"), langs.join(", ") || t("common.none")],
313
+ [t("overview.know.tags"), w.crosslink_tags === undefined ? "" : String(w.crosslink_tags)],
314
+ // `state` and `reason` are the CLI's — shown verbatim.
315
+ [t("overview.know.diy"), d.diy ? `${d.diy.state} — ${d.diy.reason}` : ""],
316
+ ])
317
+ );
318
+ out.append(know);
319
+
320
+ return out;
321
+ }
322
+ );
323
+ };
324
+
325
+ // The actionable list. Each entry is {kind, title, body, panel, cta} and knows
326
+ // where its fix lives never a blanket "go to Maintenance".
327
+ function attentionCard(d, findings) {
328
+ const w = d.wiki || {};
329
+ const p = d.patterns || {};
330
+ const items = [];
331
+
332
+ // 1. Install findings, routed per id.
333
+ for (const f of findings) {
334
+ const r = findingRoute(f.id);
335
+ if (f.id === "diy-stale") {
336
+ items.push({
337
+ kind: "warn",
338
+ title: t("overview.item.diyStale.title"),
339
+ body: t("overview.item.diyStale.body"),
340
+ evidence: d.diy && d.diy.reason,
341
+ panel: "flow",
342
+ cta: t("overview.item.diyStale.cta"),
343
+ });
344
+ } else if (r.panel) {
345
+ items.push({
346
+ kind: f.fixable ? "warn" : "bad",
347
+ // A doctor finding is already a sentence written for a human — using it
348
+ // as the title beats paraphrasing it into something less exact.
349
+ title: f.message,
350
+ panel: r.panel,
351
+ cta: t(r.cta),
352
+ });
353
+ }
354
+ }
355
+
356
+ // 2. The wiki. THIS is the recommendation the panel was missing: an AGING
357
+ // wiki is not an error, it is the moment a refresh is still cheap.
358
+ if (!w.state || w.state === "none") {
359
+ items.push({ kind: "info", title: t("overview.item.wikiNone.title"), body: t("overview.item.wikiNone.body"), panel: "knowledge", cta: t("overview.item.wikiNone.cta") });
360
+ } else if (w.state !== "registered") {
361
+ items.push({ kind: "warn", title: t("overview.item.wikiUnregistered.title"), body: t("overview.item.wikiUnregistered.body"), panel: "knowledge", cta: t("overview.item.wikiUnregistered.cta") });
362
+ } else if (w.tier === "AGING") {
363
+ items.push({
364
+ kind: "warn",
365
+ title: t("overview.item.wikiAging.title"),
366
+ body: t("overview.item.wikiAging.body"),
367
+ evidence: (w.reasons || [])[0],
368
+ panel: "knowledge",
369
+ cta: t("overview.item.wikiAging.cta"),
370
+ });
371
+ } else if (w.tier && w.tier !== "FRESH") {
372
+ items.push({
373
+ kind: "bad",
374
+ title: t("overview.item.wikiStale.title"),
375
+ body: t("overview.item.wikiStale.body"),
376
+ evidence: (w.reasons || [])[0],
377
+ panel: "knowledge",
378
+ cta: t("overview.item.wikiStale.cta"),
379
+ });
380
+ }
381
+
382
+ // 3. No cached pattern — not a fault, but the cheapest quality win there is.
383
+ if (!(p.patterns || []).length)
384
+ items.push({ kind: "info", title: t("overview.item.patterns.title"), body: t("overview.item.patterns.body"), panel: "knowledge", cta: t("overview.item.patterns.cta") });
385
+
386
+ // 4. Runs left waiting.
387
+ if ((d.waiting || []).length)
388
+ items.push({
389
+ kind: "warn",
390
+ title: tn((d.waiting || []).length, "overview.item.waiting.title"),
391
+ body: t("overview.item.waiting.body"),
392
+ panel: "runs",
393
+ cta: t("overview.item.waiting.cta"),
394
+ });
395
+
396
+ const c = card(t("overview.attention.title"), chip(String(items.length), items.length ? "warn" : "ok"));
397
+ c.id = "attention-card";
398
+
399
+ if (!items.length) {
400
+ const ok = el("div", "all-clear");
401
+ ok.append(el("div", "all-clear-mark", "✓"));
402
+ const txt = el("div");
403
+ txt.append(el("div", "all-clear-title", t("overview.attention.allClear")));
404
+ txt.append(el("div", "note", t("overview.attention.allClearHint")));
405
+ ok.append(txt);
406
+ c.append(ok);
407
+ }
408
+
409
+ const list = el("div", "todo-list");
410
+ const order = { bad: 0, warn: 1, info: 2 };
411
+ items.sort((a, b) => (order[a.kind] || 3) - (order[b.kind] || 3));
412
+ for (const it of items) {
413
+ const row = el("button", "todo todo-" + it.kind);
414
+ row.type = "button";
415
+ row.append(el("span", "todo-mark"));
416
+ const mid = el("div", "todo-body");
417
+ mid.append(el("div", "todo-title", it.title));
418
+ if (it.body) mid.append(el("div", "todo-text", it.body));
419
+ // The CLI's own words for WHY, kept verbatim under our explanation.
420
+ if (it.evidence) mid.append(el("div", "todo-evidence", it.evidence));
421
+ row.append(mid, el("span", "todo-cta", it.cta));
422
+ row.addEventListener("click", () => (location.hash = "#/" + it.panel));
423
+ list.append(row);
424
+ }
425
+ if (items.length) c.append(list);
426
+
427
+ // The update offer is appended asynchronously so a slow network check never
428
+ // delays the list — it arrives as one more row when the answer does.
429
+ versionInfo()
430
+ .then((v) => {
431
+ if (!v || !v.update_available) return;
432
+ const row = el("button", "todo todo-info todo-late");
433
+ row.type = "button";
434
+ row.append(el("span", "todo-mark"));
435
+ const mid = el("div", "todo-body");
436
+ mid.append(el("div", "todo-title", t("overview.item.update.title", { latest: v.latest })));
437
+ mid.append(el("div", "todo-text", t("overview.item.update.body", { version: v.version })));
438
+ row.append(mid, el("span", "todo-cta", t("overview.item.update.cta")));
439
+ row.addEventListener("click", () => (location.hash = "#/maintenance"));
440
+ if (!list.isConnected) c.append(list);
441
+ list.append(row);
442
+ const count = c.querySelector(".card-head .chip");
443
+ if (count) {
444
+ count.textContent = String(list.children.length);
445
+ count.className = "chip chip-warn";
446
+ }
447
+ const clear = c.querySelector(".all-clear");
448
+ if (clear) clear.remove();
449
+ })
450
+ .catch(() => {});
451
+
452
+ return c;
453
+ }