instar 1.3.815 → 1.3.817
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/dashboard/glance.js +688 -7
- package/dashboard/index.html +164 -649
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +12 -4
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/WriteDomainRegistry.d.ts.map +1 -1
- package/dist/core/WriteDomainRegistry.js +13 -0
- package/dist/core/WriteDomainRegistry.js.map +1 -1
- package/dist/scaffold/templates.js +1 -1
- package/dist/server/CapabilityIndex.d.ts.map +1 -1
- package/dist/server/CapabilityIndex.js +1 -0
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +68 -57
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +63 -63
- package/src/scaffold/templates.ts +1 -1
- package/upgrades/1.3.816.md +23 -0
- package/upgrades/1.3.817.md +22 -0
- package/upgrades/eli16/plain-enrollment-cancel.md +9 -0
- package/upgrades/side-effects/external-operation-unknown-verbs.md +85 -0
- package/upgrades/side-effects/plain-enrollment-cancel.md +79 -0
package/dashboard/glance.js
CHANGED
|
@@ -41,22 +41,27 @@ export const GLANCE_MAX_TOKEN_LEN = 40; // a longer token is a glued-word budget
|
|
|
41
41
|
// (same discipline as the F3 purpose-line exempt list). The completeness test
|
|
42
42
|
// asserts adopted ∪ grandfathered == every TAB_REGISTRY id, so a NEW tab in NEITHER
|
|
43
43
|
// set fails the build; the monotonicity test asserts the grandfather size ≤ ceiling.
|
|
44
|
-
export const GLANCE_ADOPTED_TABS = [
|
|
44
|
+
export const GLANCE_ADOPTED_TABS = [
|
|
45
|
+
'commitments', 'blockers', // Phases 1–2
|
|
46
|
+
'machines', 'systems', 'spend', 'routing-map', // Phase 3 (the jargon belt); 'systems' is the Health tab
|
|
47
|
+
];
|
|
45
48
|
|
|
46
49
|
export const GLANCE_GRANDFATHERED = [
|
|
47
|
-
'insights', 'sessions', 'files', 'dropzone', 'jobs', 'features',
|
|
50
|
+
'insights', 'sessions', 'files', 'dropzone', 'jobs', 'features',
|
|
48
51
|
'integrated-being', 'pr-pipeline', 'projects', 'initiatives', 'tokens',
|
|
49
|
-
'resources', 'llm-activity', '
|
|
50
|
-
'process-health', 'subscriptions', 'preferences-learning', '
|
|
52
|
+
'resources', 'llm-activity', 'threadline', 'evidence',
|
|
53
|
+
'process-health', 'subscriptions', 'preferences-learning', 'mandates',
|
|
51
54
|
'secrets',
|
|
52
|
-
]; //
|
|
55
|
+
]; // Phase 3 removed machines / systems (Health) / spend / routing-map — they now
|
|
56
|
+
// build through this component. 'blockers' left in Phase 2.
|
|
53
57
|
|
|
54
58
|
// The committed ceiling on grandfathered-tab count. Only ever LOWER this (each
|
|
55
59
|
// lowering marks a tab retrofitted onto the floor). Never raise it without an
|
|
56
60
|
// operator-signed justification — raising it is how a NEW tab would silently ship
|
|
57
61
|
// below the floor, the exact regression the ratchet exists to prevent.
|
|
58
|
-
// Lowered 25 → 24 (Phase 2): Blockers retrofitted
|
|
59
|
-
|
|
62
|
+
// Lowered 25 → 24 (Phase 2): Blockers retrofitted.
|
|
63
|
+
// Lowered 24 → 20 (Phase 3): Machines, Health (systems), Spend, Routing Map retrofitted.
|
|
64
|
+
export const GLANCE_GRANDFATHERED_CEILING = 20;
|
|
60
65
|
|
|
61
66
|
// ── F10 — insider-vocab detection ────────────────────────────────────────────
|
|
62
67
|
// A readability floor, NOT a secret-redaction boundary (secret handling stays at
|
|
@@ -684,3 +689,679 @@ export function blockersGlanceSpec(doc, entries, opts = {}) {
|
|
|
684
689
|
}));
|
|
685
690
|
return { headline: base.headline, tiles, population: base.population };
|
|
686
691
|
}
|
|
692
|
+
|
|
693
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
694
|
+
// PHASE 3 — the jargon belt: Machines, Health, Spend, Routing Map (topic 29836).
|
|
695
|
+
// Each of the four tabs was ≥200 words of insider vocabulary on its front page
|
|
696
|
+
// (guards lines, "paid door / metered", "lane / nature / door"). Rebuilt on the
|
|
697
|
+
// SAME three-layer template: a component-authored, jargon-free headline + ≤5 tiles
|
|
698
|
+
// (Layer 1); each tile drills into the rows behind that number in plain words
|
|
699
|
+
// (Layer 2); each row opens the full record where the IDs/config/raw detail live
|
|
700
|
+
// (Layer 3). Nothing is lost — the insider fields move one or two clicks down.
|
|
701
|
+
// Every builder maps its counts to ONE population so the headline stays honest, and
|
|
702
|
+
// every dynamic value is displayed through the shared sanitizer + textContent.
|
|
703
|
+
// ═════════════════════════════════════════════════════════════════════════════
|
|
704
|
+
|
|
705
|
+
// ── Shared plain-language helpers ────────────────────────────────────────────
|
|
706
|
+
// Acronyms that should stay upper-cased when a token is humanized for display.
|
|
707
|
+
const KNOWN_ACRONYMS = {
|
|
708
|
+
gpt: 'GPT', llm: 'LLM', cli: 'CLI', ai: 'AI', cpu: 'CPU', ram: 'RAM',
|
|
709
|
+
api: 'API', url: 'URL', id: 'ID', ok: 'OK',
|
|
710
|
+
};
|
|
711
|
+
function titleCaseWord(w) {
|
|
712
|
+
const lw = String(w).toLowerCase();
|
|
713
|
+
if (KNOWN_ACRONYMS[lw]) return KNOWN_ACRONYMS[lw];
|
|
714
|
+
return lw.charAt(0).toUpperCase() + lw.slice(1);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Turn an identifier-ish token into a plain, sentence-cased phrase: split
|
|
719
|
+
* camelCase + separators, drop empties, cap only the first word.
|
|
720
|
+
* 'zombieCleanup' → 'Zombie cleanup'; 'session_watchdog' → 'Session watchdog'.
|
|
721
|
+
*/
|
|
722
|
+
export function humanizeToken(token) {
|
|
723
|
+
const words = String(token == null ? '' : token)
|
|
724
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
725
|
+
.split(/[\s\-_/.]+/)
|
|
726
|
+
.filter(Boolean);
|
|
727
|
+
if (words.length === 0) return '';
|
|
728
|
+
return words
|
|
729
|
+
.map((w, i) => (i === 0 ? titleCaseWord(w) : (KNOWN_ACRONYMS[w.toLowerCase()] || w.toLowerCase())))
|
|
730
|
+
.join(' ');
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* A plain, jargon-safe display name for a model id — drops every version/date
|
|
735
|
+
* segment (any part containing a digit) and title-cases the words that remain.
|
|
736
|
+
* 'claude-opus-4-8-20260115' → 'Claude Opus'; 'gpt-5.5' → 'GPT'. Returns '' when
|
|
737
|
+
* nothing plain survives OR the result would still trip the F10 jargon check — the
|
|
738
|
+
* caller substitutes a plain phrase, so a raw model id can NEVER leak to Layer 1.
|
|
739
|
+
*/
|
|
740
|
+
export function friendlyModel(modelId) {
|
|
741
|
+
// Drop any version/date segment (a part with a digit) AND single-letter fragments
|
|
742
|
+
// (a lone 'm' from 'm_<hex>' is meaningless as a name) — so a hex/opaque id yields
|
|
743
|
+
// '' and the caller substitutes a plain phrase.
|
|
744
|
+
const words = String(modelId == null ? '' : modelId)
|
|
745
|
+
.split(/[\s\-_./]+/)
|
|
746
|
+
.filter((w) => w && w.length >= 2 && !/\d/.test(w));
|
|
747
|
+
const name = words.map(titleCaseWord).join(' ').trim();
|
|
748
|
+
if (!name) return '';
|
|
749
|
+
return findInsiderVocab(name).length === 0 ? name : '';
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/** The plain model phrase for a routing/spend position, with a door-class fallback. */
|
|
753
|
+
function modelPhrase(pos) {
|
|
754
|
+
if (!pos) return '';
|
|
755
|
+
return friendlyModel(pos.modelId) || (pos.doorClass === 'metered' ? 'a paid model' : 'the built-in model');
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/** Plain words for a routing/spend door class. */
|
|
759
|
+
function doorClassWord(doorClass) {
|
|
760
|
+
return doorClass === 'metered' ? 'pay-per-use' : 'built-in (no per-use charge)';
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function fmtUsd(n, dp = 2) {
|
|
764
|
+
const v = Number(n);
|
|
765
|
+
return '$' + (Number.isFinite(v) ? v : 0).toFixed(dp);
|
|
766
|
+
}
|
|
767
|
+
function fmtCount(n) {
|
|
768
|
+
const v = Number(n);
|
|
769
|
+
return (Number.isFinite(v) ? v : 0).toLocaleString('en-US');
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/** Build a Layer-3 record node from an ordered [key, value] list (all XSS-safe). */
|
|
773
|
+
function recordFields(doc, rows) {
|
|
774
|
+
const wrap = el(doc, 'div', 'glance-record-fields');
|
|
775
|
+
for (const [k, v] of rows) {
|
|
776
|
+
if (v == null || v === '') continue;
|
|
777
|
+
const row = el(doc, 'div', 'glance-record-row');
|
|
778
|
+
row.appendChild(el(doc, 'span', 'glance-record-key', String(k)));
|
|
779
|
+
row.appendChild(el(doc, 'span', 'glance-record-val', String(v)));
|
|
780
|
+
wrap.appendChild(row);
|
|
781
|
+
}
|
|
782
|
+
return wrap;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Shared tile→list→record wiring: map buildX's { rows, ... } tiles into glance
|
|
787
|
+
* tiles whose onActivate renders each row as a plain Layer-2 line that opens a
|
|
788
|
+
* Layer-3 record. `rowText(item)` → the plain summary; `recordNode(doc, item)` →
|
|
789
|
+
* the full record. Mirrors commitmentsGlanceSpec/blockersGlanceSpec exactly.
|
|
790
|
+
*/
|
|
791
|
+
function wireTiles(doc, baseTiles, rowText, recordNode) {
|
|
792
|
+
return baseTiles.map((t) => ({
|
|
793
|
+
key: t.key,
|
|
794
|
+
label: t.label,
|
|
795
|
+
value: t.value,
|
|
796
|
+
tone: t.tone,
|
|
797
|
+
onActivate: ({ doc: d, drilldown, openRecord }) => {
|
|
798
|
+
const rows = t.rows || [];
|
|
799
|
+
if (rows.length === 0) return; // component renders the honest F6 empty-state
|
|
800
|
+
const list = el(d, 'div', 'glance-list');
|
|
801
|
+
for (const item of rows) {
|
|
802
|
+
const row = d.createElement('button');
|
|
803
|
+
row.type = 'button';
|
|
804
|
+
row.className = 'glance-list-row';
|
|
805
|
+
row.setAttribute('aria-label', 'Open the full record');
|
|
806
|
+
row.appendChild(el(d, 'span', 'glance-list-summary', rowText(item)));
|
|
807
|
+
row.addEventListener('click', () => openRecord(recordNode(d, item)));
|
|
808
|
+
list.appendChild(row);
|
|
809
|
+
}
|
|
810
|
+
drilldown.appendChild(list);
|
|
811
|
+
},
|
|
812
|
+
}));
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// ── Machines glance (F10/F11) + issue #1429 nickname edit ────────────────────
|
|
816
|
+
// The old front page showed a per-machine insider "guards" line ("67 on (16
|
|
817
|
+
// confirmed) · ⚠ 6 off that should be on… as of 3m ago"). On the floor: a plain
|
|
818
|
+
// headline over Online / Attention needed / Dispatcher / Safety-checks tiles. The
|
|
819
|
+
// named safety checks (from GET /guards) drill down with a one-line plain-English
|
|
820
|
+
// explanation each (Layer 2), full posture at Layer 3. Machine NICKNAMES are
|
|
821
|
+
// user-authored, so they NEVER appear at Layer 1 — they live at Layer 2/3, shown
|
|
822
|
+
// through the sanitizer. #1429: the editable nickname lives in the Layer-2 machine
|
|
823
|
+
// row and commits only on Enter/blur; the drill holds it across the 15s poll (F9).
|
|
824
|
+
|
|
825
|
+
function guardPostureProblems(p) {
|
|
826
|
+
if (!p) return 0;
|
|
827
|
+
return (p.offDeviant || 0) + (p.offRuntimeDivergent || 0) + (p.onStale || 0)
|
|
828
|
+
+ (p.missing || 0) + (p.errored || 0) + (p.divergedPendingRestart || 0);
|
|
829
|
+
}
|
|
830
|
+
function machineNeedsAttention(m) {
|
|
831
|
+
if (!m) return false;
|
|
832
|
+
return !m.online
|
|
833
|
+
|| m.clockSkewStatus === 'suspect-clock-removed'
|
|
834
|
+
|| guardPostureProblems(m.guardPosture) > 0;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/** The single population: the machines GET /pool returns. */
|
|
838
|
+
export function machinesPopulation(pool) {
|
|
839
|
+
const machines = pool && Array.isArray(pool.machines) ? pool.machines : [];
|
|
840
|
+
return machines.filter((m) => m && typeof m.machineId === 'string');
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
export function buildMachinesGlance(pool, guards = null) {
|
|
844
|
+
const machines = machinesPopulation(pool);
|
|
845
|
+
const online = machines.filter((m) => m.online);
|
|
846
|
+
const attention = machines.filter(machineNeedsAttention);
|
|
847
|
+
const holder = pool && pool.router && pool.router.holder;
|
|
848
|
+
const dispatcher = holder ? machines.find((m) => m.machineId === holder) : null;
|
|
849
|
+
|
|
850
|
+
let headline;
|
|
851
|
+
if (machines.length === 0) {
|
|
852
|
+
headline = 'No machines are paired yet.';
|
|
853
|
+
} else if (online.length === machines.length && attention.length === 0) {
|
|
854
|
+
headline = machines.length === 1 ? 'This machine is online and healthy.'
|
|
855
|
+
: machines.length === 2 ? 'Both machines are online and healthy.'
|
|
856
|
+
: `All ${machines.length} machines are online and healthy.`;
|
|
857
|
+
} else {
|
|
858
|
+
const lookClause = attention.length === 1 ? '1 needs a look' : `${attention.length} need a look`;
|
|
859
|
+
headline = `${online.length} of ${machines.length} machines online; ${lookClause}.`;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const tiles = [
|
|
863
|
+
{ key: 'online', label: 'Online', value: String(online.length), tone: online.length === machines.length ? 'neutral' : 'warn', rows: online },
|
|
864
|
+
{ key: 'attention', label: 'Attention needed', value: String(attention.length), tone: attention.length ? 'warn' : 'neutral', rows: attention },
|
|
865
|
+
{ key: 'dispatcher', label: 'Dispatcher', value: dispatcher ? 'Assigned' : 'None', tone: 'muted', rows: dispatcher ? [dispatcher] : [] },
|
|
866
|
+
];
|
|
867
|
+
|
|
868
|
+
// The named safety checks come from the LOCAL /guards view (the only place guard
|
|
869
|
+
// NAMES exist without the rate-limited pool-scoped call). Omit the tile when the
|
|
870
|
+
// guards view is unavailable rather than invent an empty one.
|
|
871
|
+
const guardRows = guards && Array.isArray(guards.guards) ? guards.guards : null;
|
|
872
|
+
if (guardRows) {
|
|
873
|
+
const problems = guards.summary ? (
|
|
874
|
+
(guards.summary.offDeviant || 0) + (guards.summary.offRuntimeDivergent || 0)
|
|
875
|
+
+ (guards.summary.onStale || 0) + (guards.summary.missing || 0)
|
|
876
|
+
+ (guards.summary.errored || 0) + (guards.summary.divergedPendingRestart || 0)
|
|
877
|
+
) : 0;
|
|
878
|
+
tiles.push({ key: 'guards', label: 'Safety checks', value: String(guardRows.length), tone: problems ? 'warn' : 'neutral', rows: guardRows });
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
return { headline, tiles, population: machines };
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const MACHINE_STATUS_WORD = {
|
|
885
|
+
offline: 'Offline',
|
|
886
|
+
'suspect-clock-removed': 'Clock out of sync — paused for new conversations',
|
|
887
|
+
};
|
|
888
|
+
function machineStatusWord(m) {
|
|
889
|
+
if (!m.online) return 'Offline';
|
|
890
|
+
if (m.clockSkewStatus === 'suspect-clock-removed') return MACHINE_STATUS_WORD['suspect-clock-removed'];
|
|
891
|
+
if (machineNeedsAttention(m)) return 'Online — needs a look';
|
|
892
|
+
return 'Online and healthy';
|
|
893
|
+
}
|
|
894
|
+
function machineSpecLine(hw) {
|
|
895
|
+
if (!hw) return 'specs not reported yet';
|
|
896
|
+
const chip = (hw.cpuModel && hw.cpuModel !== 'unknown') ? hw.cpuModel
|
|
897
|
+
: ((hw.platform || '') + (hw.arch ? ' ' + hw.arch : '')).trim();
|
|
898
|
+
const cores = hw.cpuCores ? hw.cpuCores + ' cores' : '';
|
|
899
|
+
const ram = hw.totalMemBytes ? Math.round(hw.totalMemBytes / 1073741824) + ' GB' : '';
|
|
900
|
+
return [chip, cores, ram].filter(Boolean).join(' · ') || 'specs not reported yet';
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/** Plain one-line explanation of a guard's effective state — no per-key dictionary. */
|
|
904
|
+
const GUARD_EFFECTIVE_WORD = {
|
|
905
|
+
'on-confirmed': 'On and verified working',
|
|
906
|
+
'on-unverified': 'On (not yet verified this run)',
|
|
907
|
+
'on-stale': 'On, but its last check is stale — worth a look',
|
|
908
|
+
'on-dry-run': 'On in practice-only mode (not acting for real yet)',
|
|
909
|
+
'off-runtime-divergent': 'Off even though the settings say on — needs a look',
|
|
910
|
+
'diverged-pending-restart': 'Changed; waiting for a restart to take effect',
|
|
911
|
+
'errored': 'Hit an error — needs a look',
|
|
912
|
+
'missing': 'Expected but not found — needs a look',
|
|
913
|
+
};
|
|
914
|
+
function guardExplanation(g) {
|
|
915
|
+
if (g.effective === 'off') {
|
|
916
|
+
return g.offClass === 'diverged-from-default'
|
|
917
|
+
? 'Off, though the default is on — worth a look'
|
|
918
|
+
: 'Off by default — intentionally quiet';
|
|
919
|
+
}
|
|
920
|
+
return GUARD_EFFECTIVE_WORD[g.effective] || String(g.effective || 'unknown');
|
|
921
|
+
}
|
|
922
|
+
function guardNeedsLook(g) {
|
|
923
|
+
return ['on-stale', 'off-runtime-divergent', 'diverged-pending-restart', 'errored', 'missing'].includes(g.effective)
|
|
924
|
+
|| (g.effective === 'off' && g.offClass === 'diverged-from-default');
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
export function machineRowText(m) {
|
|
928
|
+
const nick = sanitizeForDisplay(m.nickname || m.machineId || 'A machine', 'label');
|
|
929
|
+
return `${nick} — ${machineStatusWord(m)}`;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/** Layer-3 machine record — specs, sessions, and this machine's guard posture in
|
|
933
|
+
* plain words. The raw counts that used to sit on the front page live here. */
|
|
934
|
+
export function machineRecordNode(doc, m, opts = {}) {
|
|
935
|
+
const online = !!m.online;
|
|
936
|
+
const sessions = (m.activeSessionCount != null)
|
|
937
|
+
? `${m.activeSessionCount}${m.maxSessions ? ' / ' + m.maxSessions : ''} conversation${m.activeSessionCount === 1 ? '' : 's'}`
|
|
938
|
+
: '—';
|
|
939
|
+
const rows = [
|
|
940
|
+
['Name', m.nickname || m.machineId || '—'],
|
|
941
|
+
['Status', machineStatusWord(m)],
|
|
942
|
+
['Specs', machineSpecLine(m.hardware)],
|
|
943
|
+
['Conversations', online ? sessions : '—'],
|
|
944
|
+
];
|
|
945
|
+
const p = m.guardPosture;
|
|
946
|
+
if (p) {
|
|
947
|
+
const on = (p.onConfirmed || 0) + (p.onUnverified || 0) + (p.onDryRun || 0);
|
|
948
|
+
rows.push(['Safety checks on', String(on)]);
|
|
949
|
+
const problems = guardPostureProblems(p);
|
|
950
|
+
if (problems > 0) rows.push(['Safety checks needing a look', String(problems)]);
|
|
951
|
+
} else {
|
|
952
|
+
rows.push(['Safety checks', 'not reported yet']);
|
|
953
|
+
}
|
|
954
|
+
const wrap = recordFields(doc, rows);
|
|
955
|
+
// #1429: the editable nickname commits ONLY on Enter/blur, with optimistic local
|
|
956
|
+
// echo; the poll stays authority for external renames. The drill holds it (F9).
|
|
957
|
+
if (typeof opts.onRename === 'function' && m.machineId) {
|
|
958
|
+
const editRow = el(doc, 'div', 'glance-record-row');
|
|
959
|
+
editRow.appendChild(el(doc, 'span', 'glance-record-key', 'Rename'));
|
|
960
|
+
const input = doc.createElement('input');
|
|
961
|
+
input.type = 'text';
|
|
962
|
+
input.className = 'glance-record-input machine-nick';
|
|
963
|
+
input.value = m.nickname || m.machineId;
|
|
964
|
+
input.setAttribute('data-mid', m.machineId);
|
|
965
|
+
input.setAttribute('aria-label', 'Machine nickname — press Enter or click away to save');
|
|
966
|
+
let last = input.value;
|
|
967
|
+
const commit = () => {
|
|
968
|
+
const next = input.value.trim();
|
|
969
|
+
if (next === '' || next === last) { input.value = last; return; }
|
|
970
|
+
const prev = last;
|
|
971
|
+
last = next; // optimistic local echo — keep the typed value
|
|
972
|
+
// prev lets the caller revert this optimistic echo if the save fails.
|
|
973
|
+
opts.onRename(m.machineId, next, input, prev);
|
|
974
|
+
};
|
|
975
|
+
input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); input.blur(); } });
|
|
976
|
+
input.addEventListener('blur', commit);
|
|
977
|
+
editRow.appendChild(input);
|
|
978
|
+
wrap.appendChild(editRow);
|
|
979
|
+
}
|
|
980
|
+
return wrap;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
export function guardRowText(g) {
|
|
984
|
+
return `${humanizeToken(g.key)} — ${guardExplanation(g)}`;
|
|
985
|
+
}
|
|
986
|
+
export function guardRecordNode(doc, g) {
|
|
987
|
+
const rt = g.runtime;
|
|
988
|
+
const rows = [
|
|
989
|
+
['Check', humanizeToken(g.key)],
|
|
990
|
+
['In plain words', guardExplanation(g)],
|
|
991
|
+
['Turned on in settings', g.configEnabled == null ? 'not set' : (g.configEnabled ? 'yes' : 'no')],
|
|
992
|
+
['On by default', g.defaultEnabled == null ? '—' : (g.defaultEnabled ? 'yes' : 'no')],
|
|
993
|
+
['Runs in', g.process === 'lifeline' ? 'the always-on lifeline' : 'the main server'],
|
|
994
|
+
];
|
|
995
|
+
if (g.loadBearing) rows.push(['Load-bearing', 'yes — a safety-critical check']);
|
|
996
|
+
if (rt && typeof rt === 'object') {
|
|
997
|
+
if (rt.dryRun) rows.push(['Mode', 'practice-only (not acting for real yet)']);
|
|
998
|
+
if (typeof rt.jobCount === 'number') rows.push(['Watched items', String(rt.jobCount)]);
|
|
999
|
+
}
|
|
1000
|
+
if (g.error) rows.push(['Error', g.error]);
|
|
1001
|
+
return recordFields(doc, rows);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Build the Machines glance spec with drill wiring. `guards` is the LOCAL /guards
|
|
1006
|
+
* view ({ guards, summary }) or null. `opts.onRename(machineId, nickname, input)`
|
|
1007
|
+
* wires the #1429 nickname edit onto each machine's Layer-3 record.
|
|
1008
|
+
*/
|
|
1009
|
+
export function machinesGlanceSpec(doc, pool, guards = null, opts = {}) {
|
|
1010
|
+
const base = buildMachinesGlance(pool, guards);
|
|
1011
|
+
const tiles = base.tiles.map((t) => {
|
|
1012
|
+
if (t.key === 'guards') {
|
|
1013
|
+
return {
|
|
1014
|
+
key: t.key, label: t.label, value: t.value, tone: t.tone,
|
|
1015
|
+
onActivate: ({ doc: d, drilldown, openRecord }) => {
|
|
1016
|
+
const rows = (t.rows || []).slice().sort((a, b) => (guardNeedsLook(b) ? 1 : 0) - (guardNeedsLook(a) ? 1 : 0));
|
|
1017
|
+
if (rows.length === 0) return;
|
|
1018
|
+
const list = el(d, 'div', 'glance-list');
|
|
1019
|
+
for (const g of rows) {
|
|
1020
|
+
const row = d.createElement('button');
|
|
1021
|
+
row.type = 'button';
|
|
1022
|
+
row.className = 'glance-list-row';
|
|
1023
|
+
if (guardNeedsLook(g)) row.setAttribute('data-tone', 'warn');
|
|
1024
|
+
row.setAttribute('aria-label', 'Open the full record');
|
|
1025
|
+
row.appendChild(el(d, 'span', 'glance-list-summary', guardRowText(g)));
|
|
1026
|
+
row.addEventListener('click', () => openRecord(guardRecordNode(d, g)));
|
|
1027
|
+
list.appendChild(row);
|
|
1028
|
+
}
|
|
1029
|
+
drilldown.appendChild(list);
|
|
1030
|
+
},
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
return {
|
|
1034
|
+
key: t.key, label: t.label, value: t.value, tone: t.tone,
|
|
1035
|
+
onActivate: ({ doc: d, drilldown, openRecord }) => {
|
|
1036
|
+
const rows = t.rows || [];
|
|
1037
|
+
if (rows.length === 0) return;
|
|
1038
|
+
const list = el(d, 'div', 'glance-list');
|
|
1039
|
+
for (const m of rows) {
|
|
1040
|
+
const row = d.createElement('button');
|
|
1041
|
+
row.type = 'button';
|
|
1042
|
+
row.className = 'glance-list-row';
|
|
1043
|
+
if (machineNeedsAttention(m)) row.setAttribute('data-tone', 'warn');
|
|
1044
|
+
row.setAttribute('aria-label', 'Open the full record');
|
|
1045
|
+
row.appendChild(el(d, 'span', 'glance-list-summary', machineRowText(m)));
|
|
1046
|
+
row.addEventListener('click', () => openRecord(machineRecordNode(d, m, { onRename: opts.onRename })));
|
|
1047
|
+
list.appendChild(row);
|
|
1048
|
+
}
|
|
1049
|
+
drilldown.appendChild(list);
|
|
1050
|
+
},
|
|
1051
|
+
};
|
|
1052
|
+
});
|
|
1053
|
+
return { headline: base.headline, tiles, population: base.population };
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// ── Health glance (F10/F11) ──────────────────────────────────────────────────
|
|
1057
|
+
// The old front page dumped ~390 words of subsystem prose. On the floor: a plain
|
|
1058
|
+
// headline ("All systems are operational." / "N subsystems need attention.") over
|
|
1059
|
+
// Subsystems / Need attention / Recent events tiles. Each subsystem's full prose
|
|
1060
|
+
// description, processes, and metrics move to its Layer-3 record — nothing lost.
|
|
1061
|
+
|
|
1062
|
+
/** The single population: the subsystems GET /systems/status reports as active. */
|
|
1063
|
+
export function healthPopulation(systems) {
|
|
1064
|
+
const caps = systems && Array.isArray(systems.activeCapabilities) ? systems.activeCapabilities : [];
|
|
1065
|
+
return caps.filter((c) => c && typeof c.id === 'string');
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
export function buildHealthGlance(systems) {
|
|
1069
|
+
const caps = healthPopulation(systems);
|
|
1070
|
+
const errored = caps.filter((c) => c.status === 'error');
|
|
1071
|
+
const events = systems && Array.isArray(systems.recentEvents) ? systems.recentEvents : [];
|
|
1072
|
+
const healthy = (systems && systems.health === 'healthy') && errored.length === 0;
|
|
1073
|
+
|
|
1074
|
+
let headline;
|
|
1075
|
+
if (caps.length === 0) {
|
|
1076
|
+
headline = 'No subsystems are reporting yet.';
|
|
1077
|
+
} else if (healthy) {
|
|
1078
|
+
headline = 'All systems are operational.';
|
|
1079
|
+
} else {
|
|
1080
|
+
headline = errored.length === 1
|
|
1081
|
+
? '1 subsystem needs attention.'
|
|
1082
|
+
: `${errored.length} subsystems need attention.`;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
const tiles = [
|
|
1086
|
+
{ key: 'subsystems', label: 'Subsystems', value: String(caps.length), tone: 'neutral', rows: caps },
|
|
1087
|
+
{ key: 'attention', label: 'Need attention', value: String(errored.length), tone: errored.length ? 'warn' : 'neutral', rows: errored },
|
|
1088
|
+
{ key: 'events', label: 'Recent events', value: String(events.length), tone: 'muted', rows: events },
|
|
1089
|
+
];
|
|
1090
|
+
|
|
1091
|
+
return { headline, tiles, population: caps };
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
const CAP_STAT_LABEL = {
|
|
1095
|
+
recoveries: 'Recovered', interventions: 'Auto-fixed', activeCases: 'Active',
|
|
1096
|
+
coherencePassed: 'Checks OK', coherenceFailed: 'Issues', memoryPercent: 'Memory %',
|
|
1097
|
+
enabledJobs: 'Jobs', activeJobSessions: 'Running', queueLength: 'Queued',
|
|
1098
|
+
weeklyUsage: 'Weekly %', fiveHourRate: '5h rate %', topicMappings: 'Topics',
|
|
1099
|
+
totalMessages: 'Messages', proposals: 'Proposals', gaps: 'Gaps', learningsApplied: 'Applied',
|
|
1100
|
+
};
|
|
1101
|
+
|
|
1102
|
+
export function healthCapRowText(c) {
|
|
1103
|
+
const word = c.status === 'error' ? 'needs attention' : 'working';
|
|
1104
|
+
return `${sanitizeForDisplay(c.label || c.id || 'A subsystem', 'label')} — ${word}`;
|
|
1105
|
+
}
|
|
1106
|
+
export function healthCapRecordNode(doc, c) {
|
|
1107
|
+
const rows = [
|
|
1108
|
+
['Subsystem', c.label || c.id || '—'],
|
|
1109
|
+
['Status', c.status === 'error' ? 'Needs attention' : 'Working'],
|
|
1110
|
+
['What it does', c.description || '—'],
|
|
1111
|
+
['Right now', c.metric || '—'],
|
|
1112
|
+
];
|
|
1113
|
+
for (const p of (c.processes || [])) {
|
|
1114
|
+
rows.push([p.name, p.status === 'running' ? 'Running' : 'Error']);
|
|
1115
|
+
}
|
|
1116
|
+
for (const [key, label] of Object.entries(CAP_STAT_LABEL)) {
|
|
1117
|
+
const v = c.stats ? c.stats[key] : undefined;
|
|
1118
|
+
if (v != null && v !== 0 && v !== false) rows.push([label, String(v)]);
|
|
1119
|
+
}
|
|
1120
|
+
return recordFields(doc, rows);
|
|
1121
|
+
}
|
|
1122
|
+
export function healthEventRowText(e) {
|
|
1123
|
+
return sanitizeForDisplay(e.narrative || e.subsystem || 'An event', 'summary');
|
|
1124
|
+
}
|
|
1125
|
+
export function healthEventRecordNode(doc, e) {
|
|
1126
|
+
const when = e.timestamp ? defaultFmtTs(e.timestamp) : '—';
|
|
1127
|
+
return recordFields(doc, [
|
|
1128
|
+
['What happened', e.narrative || '—'],
|
|
1129
|
+
['Part', e.subsystem || '—'],
|
|
1130
|
+
['When', when],
|
|
1131
|
+
]);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
export function healthGlanceSpec(doc, systems) {
|
|
1135
|
+
const base = buildHealthGlance(systems);
|
|
1136
|
+
const tiles = base.tiles.map((t) => {
|
|
1137
|
+
const rowText = t.key === 'events' ? healthEventRowText : healthCapRowText;
|
|
1138
|
+
const recordNode = t.key === 'events' ? healthEventRecordNode : healthCapRecordNode;
|
|
1139
|
+
return {
|
|
1140
|
+
key: t.key, label: t.label, value: t.value, tone: t.tone,
|
|
1141
|
+
onActivate: ({ doc: d, drilldown, openRecord }) => {
|
|
1142
|
+
const rows = t.rows || [];
|
|
1143
|
+
if (rows.length === 0) return;
|
|
1144
|
+
const list = el(d, 'div', 'glance-list');
|
|
1145
|
+
for (const item of rows) {
|
|
1146
|
+
const row = d.createElement('button');
|
|
1147
|
+
row.type = 'button';
|
|
1148
|
+
row.className = 'glance-list-row';
|
|
1149
|
+
if (t.key !== 'events' && item.status === 'error') row.setAttribute('data-tone', 'warn');
|
|
1150
|
+
row.setAttribute('aria-label', 'Open the full record');
|
|
1151
|
+
row.appendChild(el(d, 'span', 'glance-list-summary', rowText(item)));
|
|
1152
|
+
row.addEventListener('click', () => openRecord(recordNode(d, item)));
|
|
1153
|
+
list.appendChild(row);
|
|
1154
|
+
}
|
|
1155
|
+
drilldown.appendChild(list);
|
|
1156
|
+
},
|
|
1157
|
+
};
|
|
1158
|
+
});
|
|
1159
|
+
return { headline: base.headline, tiles, population: base.population };
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// ── Spend glance (F10/F11) ───────────────────────────────────────────────────
|
|
1163
|
+
// The old front page leaned on "paid door / metered / reflows". On the floor: a
|
|
1164
|
+
// plain headline ("Nothing is being billed per-call right now.") over Estimated
|
|
1165
|
+
// cost / Text processed / Pay-per-use access tiles. "Metered / paid door" reads as
|
|
1166
|
+
// "pay-per-use"; the price-reflow detail and per-model math move to Layer 3.
|
|
1167
|
+
|
|
1168
|
+
export function buildSpendGlance(summary, caps = null) {
|
|
1169
|
+
const totals = (summary && summary.totals) || {};
|
|
1170
|
+
const rows = (summary && Array.isArray(summary.rows)) ? summary.rows : [];
|
|
1171
|
+
const keys = (caps && Array.isArray(caps.keys)) ? caps.keys : [];
|
|
1172
|
+
const meteredLiveYet = !!(summary && summary.meteredLiveYet) || !!(caps && caps.meteredLiveYet);
|
|
1173
|
+
|
|
1174
|
+
const netUsd = Number(totals.netUsd) || 0;
|
|
1175
|
+
const headline = meteredLiveYet
|
|
1176
|
+
? `About ${fmtUsd(netUsd)} of pay-per-use AI so far.`
|
|
1177
|
+
: 'Nothing is being billed per-call right now.';
|
|
1178
|
+
|
|
1179
|
+
const tokensTotal = (Number(totals.tokensIn) || 0) + (Number(totals.tokensOut) || 0);
|
|
1180
|
+
const tiles = [
|
|
1181
|
+
{ key: 'cost', label: 'Estimated cost', value: fmtUsd(netUsd), tone: 'neutral', rows, mode: 'cost' },
|
|
1182
|
+
{ key: 'usage', label: 'Text processed', value: fmtCount(tokensTotal), tone: 'muted', rows, mode: 'usage' },
|
|
1183
|
+
{ key: 'access', label: 'Pay-per-use access', value: String(keys.length), tone: 'muted', rows: keys, mode: 'access' },
|
|
1184
|
+
];
|
|
1185
|
+
|
|
1186
|
+
return { headline, tiles, population: rows };
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function plainPriceBasis(r) {
|
|
1190
|
+
if (r.priceBasis === 'subscription-zero') return 'Subscription — not billed per use';
|
|
1191
|
+
if (r.priceBasis === 'no-matching-point') return 'No price on file yet';
|
|
1192
|
+
return String(r.priceBasis || '—') + (r.priceStale ? ' (out of date)' : '');
|
|
1193
|
+
}
|
|
1194
|
+
function plainGoLive(state) {
|
|
1195
|
+
if (state === 'live') return 'Live';
|
|
1196
|
+
if (state === 'disarmed') return 'Turned off';
|
|
1197
|
+
return 'Not switched on yet';
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
export function spendRowCostText(r) {
|
|
1201
|
+
return `${modelPhrase(r)} — ${fmtUsd(Number(r.netUsd) || 0)}`;
|
|
1202
|
+
}
|
|
1203
|
+
export function spendRowUsageText(r) {
|
|
1204
|
+
const total = (Number(r.tokensIn) || 0) + (Number(r.tokensOut) || 0);
|
|
1205
|
+
return `${modelPhrase(r)} — ${fmtCount(total)} pieces of text`;
|
|
1206
|
+
}
|
|
1207
|
+
export function spendKeyRowText(k) {
|
|
1208
|
+
return `${humanizeToken(k.provider || k.door || 'A provider')} — pay-per-use · ${plainGoLive(k.goLiveState)}`;
|
|
1209
|
+
}
|
|
1210
|
+
export function spendRowRecordNode(doc, r) {
|
|
1211
|
+
return recordFields(doc, [
|
|
1212
|
+
['Model', friendlyModel(r.modelId) || r.modelId || '—'],
|
|
1213
|
+
['How it is reached', humanizeToken(r.door)],
|
|
1214
|
+
['Access type', doorClassWord(r.doorClass)],
|
|
1215
|
+
['Text in', fmtCount(r.tokensIn)],
|
|
1216
|
+
['Text out', fmtCount(r.tokensOut)],
|
|
1217
|
+
['Gross cost', fmtUsd(r.grossUsd, 4)],
|
|
1218
|
+
['Net cost', fmtUsd(r.netUsd, 4)],
|
|
1219
|
+
['How it is priced', plainPriceBasis(r)],
|
|
1220
|
+
]);
|
|
1221
|
+
}
|
|
1222
|
+
export function spendKeyRecordNode(doc, k) {
|
|
1223
|
+
return recordFields(doc, [
|
|
1224
|
+
['Provider', k.provider || '—'],
|
|
1225
|
+
['How it is reached', humanizeToken(k.door)],
|
|
1226
|
+
['Daily limit', fmtUsd(k.dailyCapUsd)],
|
|
1227
|
+
['Lifetime limit', fmtUsd(k.lifetimeCapUsd)],
|
|
1228
|
+
['Used today', fmtUsd(k.committedDayUsd)],
|
|
1229
|
+
['Used in total', fmtUsd(k.committedLifetimeUsd)],
|
|
1230
|
+
['Status', plainGoLive(k.goLiveState) + (k.frozen ? ' · paused' : '')],
|
|
1231
|
+
]);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
export function spendGlanceSpec(doc, summary, caps = null) {
|
|
1235
|
+
const base = buildSpendGlance(summary, caps);
|
|
1236
|
+
const tiles = base.tiles.map((t) => ({
|
|
1237
|
+
key: t.key, label: t.label, value: t.value, tone: t.tone,
|
|
1238
|
+
onActivate: ({ doc: d, drilldown, openRecord }) => {
|
|
1239
|
+
const rows = t.rows || [];
|
|
1240
|
+
if (rows.length === 0) return;
|
|
1241
|
+
const list = el(d, 'div', 'glance-list');
|
|
1242
|
+
for (const item of rows) {
|
|
1243
|
+
const row = d.createElement('button');
|
|
1244
|
+
row.type = 'button';
|
|
1245
|
+
row.className = 'glance-list-row';
|
|
1246
|
+
row.setAttribute('aria-label', 'Open the full record');
|
|
1247
|
+
const text = t.mode === 'access' ? spendKeyRowText(item)
|
|
1248
|
+
: t.mode === 'usage' ? spendRowUsageText(item)
|
|
1249
|
+
: spendRowCostText(item);
|
|
1250
|
+
row.appendChild(el(d, 'span', 'glance-list-summary', text));
|
|
1251
|
+
const recNode = t.mode === 'access' ? spendKeyRecordNode(d, item) : spendRowRecordNode(d, item);
|
|
1252
|
+
row.addEventListener('click', () => openRecord(recNode));
|
|
1253
|
+
list.appendChild(row);
|
|
1254
|
+
}
|
|
1255
|
+
drilldown.appendChild(list);
|
|
1256
|
+
},
|
|
1257
|
+
}));
|
|
1258
|
+
return { headline: base.headline, tiles, population: base.population };
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// ── Routing Map glance (F10/F11) ─────────────────────────────────────────────
|
|
1262
|
+
// The old front page used "lane / nature / door". On the floor: a plain headline
|
|
1263
|
+
// ("Background AI work runs on X, with Y as backup.") over one tile per lane
|
|
1264
|
+
// (plain-named) + a Job-types tile. Each lane drills into its ordered fallback
|
|
1265
|
+
// list of plain model names (Layer 2); each opens the full door/model config
|
|
1266
|
+
// (Layer 3). The insider door flags live at Layer 3, not the front page.
|
|
1267
|
+
|
|
1268
|
+
const CHAIN_ORDER = ['FAST', 'SORT', 'JUDGE', 'WRITE'];
|
|
1269
|
+
const CHAIN_LABEL = { FAST: 'Quick tasks', SORT: 'Sorting', JUDGE: 'Judging', WRITE: 'Writing' };
|
|
1270
|
+
|
|
1271
|
+
/** The primary lane's first two positions drive the headline (X + backup Y). */
|
|
1272
|
+
function primaryLane(map) {
|
|
1273
|
+
const chains = map && Array.isArray(map.chains) ? map.chains : [];
|
|
1274
|
+
return chains.find((c) => c.chain === 'FAST') || chains[0] || null;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
export function buildRoutingMapGlance(map) {
|
|
1278
|
+
const chains = map && Array.isArray(map.chains) ? map.chains : [];
|
|
1279
|
+
const components = map && Array.isArray(map.components) ? map.components : [];
|
|
1280
|
+
|
|
1281
|
+
const lead = primaryLane(map);
|
|
1282
|
+
const positions = (lead && Array.isArray(lead.positions)) ? lead.positions : [];
|
|
1283
|
+
let headline;
|
|
1284
|
+
if (chains.length === 0) {
|
|
1285
|
+
headline = 'No background AI routing is set up yet.';
|
|
1286
|
+
} else {
|
|
1287
|
+
const x = modelPhrase(positions[0]) || 'the built-in model';
|
|
1288
|
+
const y = modelPhrase(positions[1]);
|
|
1289
|
+
headline = y
|
|
1290
|
+
? `Background AI work runs on ${x}, with ${y} as backup.`
|
|
1291
|
+
: `Background AI work runs on ${x}.`;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
const tiles = [];
|
|
1295
|
+
const ordered = chains.slice().sort((a, b) => CHAIN_ORDER.indexOf(a.chain) - CHAIN_ORDER.indexOf(b.chain));
|
|
1296
|
+
for (const c of ordered) {
|
|
1297
|
+
if (tiles.length >= GLANCE_MAX_TILES - 1) break; // leave room for the Job-types tile
|
|
1298
|
+
const pos = Array.isArray(c.positions) ? c.positions : [];
|
|
1299
|
+
tiles.push({
|
|
1300
|
+
key: 'lane-' + String(c.chain || '').toLowerCase(),
|
|
1301
|
+
label: CHAIN_LABEL[c.chain] || humanizeToken(c.chain),
|
|
1302
|
+
value: String(pos.length),
|
|
1303
|
+
tone: 'neutral',
|
|
1304
|
+
rows: pos,
|
|
1305
|
+
mode: 'lane',
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
tiles.push({ key: 'jobs', label: 'Job types', value: String(components.length), tone: 'muted', rows: components, mode: 'jobs' });
|
|
1309
|
+
|
|
1310
|
+
return { headline, tiles, population: components };
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
export function laneRowText(p, i) {
|
|
1314
|
+
const rank = typeof i === 'number' ? `${i + 1}. ` : '';
|
|
1315
|
+
return `${rank}${modelPhrase(p)}${p.doorClass === 'metered' ? ' (pay-per-use)' : ''}`;
|
|
1316
|
+
}
|
|
1317
|
+
export function laneRecordNode(doc, p) {
|
|
1318
|
+
return recordFields(doc, [
|
|
1319
|
+
['Model', friendlyModel(p.modelId) || p.modelId || '—'],
|
|
1320
|
+
['How it is reached', humanizeToken(p.door)],
|
|
1321
|
+
['Access type', doorClassWord(p.doorClass)],
|
|
1322
|
+
['Safe for untrusted input', p.injectionSafe ? 'yes' : 'no'],
|
|
1323
|
+
['Pay-per-use', p.moneyGated ? 'yes' : 'no'],
|
|
1324
|
+
['Skipped for now', p.skippedInIncrementA ? 'yes' : 'no'],
|
|
1325
|
+
]);
|
|
1326
|
+
}
|
|
1327
|
+
export function jobRowText(c) {
|
|
1328
|
+
const lane = CHAIN_LABEL[c.chain] || 'default routing';
|
|
1329
|
+
return `${humanizeToken(c.component)} — ${lane}`;
|
|
1330
|
+
}
|
|
1331
|
+
export function jobRecordNode(doc, c) {
|
|
1332
|
+
const exposure = c.injectionExposure
|
|
1333
|
+
? (c.injectionExposure.exposed ? 'yes' : 'no')
|
|
1334
|
+
: (c.untrustedInput === true ? 'yes' : c.untrustedInput === false ? 'no' : '—');
|
|
1335
|
+
return recordFields(doc, [
|
|
1336
|
+
['Job kind', humanizeToken(c.component)],
|
|
1337
|
+
['Type', humanizeToken(c.category)],
|
|
1338
|
+
['Lane', CHAIN_LABEL[c.chain] || 'default routing'],
|
|
1339
|
+
['Safety-critical', c.criticalGate ? 'yes' : 'no'],
|
|
1340
|
+
['Can see untrusted input', exposure],
|
|
1341
|
+
]);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
export function routingMapGlanceSpec(doc, map) {
|
|
1345
|
+
const base = buildRoutingMapGlance(map);
|
|
1346
|
+
const tiles = base.tiles.map((t) => ({
|
|
1347
|
+
key: t.key, label: t.label, value: t.value, tone: t.tone,
|
|
1348
|
+
onActivate: ({ doc: d, drilldown, openRecord }) => {
|
|
1349
|
+
const rows = t.rows || [];
|
|
1350
|
+
if (rows.length === 0) return;
|
|
1351
|
+
const list = el(d, 'div', 'glance-list');
|
|
1352
|
+
rows.forEach((item, i) => {
|
|
1353
|
+
const row = d.createElement('button');
|
|
1354
|
+
row.type = 'button';
|
|
1355
|
+
row.className = 'glance-list-row';
|
|
1356
|
+
row.setAttribute('aria-label', 'Open the full record');
|
|
1357
|
+
const text = t.mode === 'jobs' ? jobRowText(item) : laneRowText(item, i);
|
|
1358
|
+
row.appendChild(el(d, 'span', 'glance-list-summary', text));
|
|
1359
|
+
const recNode = t.mode === 'jobs' ? jobRecordNode(d, item) : laneRecordNode(d, item);
|
|
1360
|
+
row.addEventListener('click', () => openRecord(recNode));
|
|
1361
|
+
list.appendChild(row);
|
|
1362
|
+
});
|
|
1363
|
+
drilldown.appendChild(list);
|
|
1364
|
+
},
|
|
1365
|
+
}));
|
|
1366
|
+
return { headline: base.headline, tiles, population: base.population };
|
|
1367
|
+
}
|