@animalabs/connectome-host 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +20 -0
- package/.github/workflows/publish.yml +65 -0
- package/ARCHITECTURE.md +355 -0
- package/CHANGELOG.md +30 -0
- package/HEADLESS-FLEET-PLAN.md +330 -0
- package/README.md +189 -0
- package/UNIFIED-TREE-PLAN.md +242 -0
- package/bun.lock +288 -0
- package/docs/AGENT-MEMORY-GUIDE.md +214 -0
- package/docs/AGENT-ONBOARDING.md +541 -0
- package/docs/ATTENTION-AND-GATING.md +120 -0
- package/docs/DEPLOYMENTS.md +130 -0
- package/docs/DEV-ENVIRONMENT.md +215 -0
- package/docs/LIBRARY-PIPELINE.md +286 -0
- package/docs/LOCUS-ROUTING-DESIGN.md +115 -0
- package/docs/claudeai-evacuation.md +228 -0
- package/docs/debug-context-api.md +208 -0
- package/docs/webui-deployment.md +219 -0
- package/package.json +33 -0
- package/recipes/SETUP.md +308 -0
- package/recipes/TRIUMVIRATE-SETUP.md +467 -0
- package/recipes/claude-export-revive.json +19 -0
- package/recipes/clerk.json +157 -0
- package/recipes/knowledge-miner.json +174 -0
- package/recipes/knowledge-reviewer.json +76 -0
- package/recipes/mcpl-editor-test.json +38 -0
- package/recipes/prompts/transplant-addendum.md +17 -0
- package/recipes/triumvirate.json +63 -0
- package/recipes/webui-fleet-test.json +27 -0
- package/recipes/webui-test.json +16 -0
- package/recipes/zulip-miner.json +87 -0
- package/scripts/connectome-doctor +373 -0
- package/scripts/evacuator.ts +956 -0
- package/scripts/import-claudeai-export.ts +747 -0
- package/scripts/lib/line-reader.ts +53 -0
- package/scripts/test-historical-thinking.ts +148 -0
- package/scripts/warmup-session.ts +336 -0
- package/src/agent-name.ts +58 -0
- package/src/commands.ts +1074 -0
- package/src/headless.ts +528 -0
- package/src/index.ts +867 -0
- package/src/logging-adapter.ts +208 -0
- package/src/mcpl-config.ts +199 -0
- package/src/modules/activity-module.ts +270 -0
- package/src/modules/channel-mode-module.ts +260 -0
- package/src/modules/fleet-module.ts +1705 -0
- package/src/modules/fleet-types.ts +225 -0
- package/src/modules/lessons-module.ts +465 -0
- package/src/modules/mcpl-admin-module.ts +345 -0
- package/src/modules/retrieval-module.ts +327 -0
- package/src/modules/settings-module.ts +143 -0
- package/src/modules/subagent-module.ts +2074 -0
- package/src/modules/subscription-gc-module.ts +322 -0
- package/src/modules/time-module.ts +85 -0
- package/src/modules/tui-module.ts +76 -0
- package/src/modules/web-ui-curve-page.ts +249 -0
- package/src/modules/web-ui-module.ts +2595 -0
- package/src/recipe.ts +1003 -0
- package/src/session-manager.ts +278 -0
- package/src/state/agent-tree-reducer.ts +431 -0
- package/src/state/fleet-tree-aggregator.ts +195 -0
- package/src/strategies/frontdesk-strategy.ts +364 -0
- package/src/synesthete.ts +68 -0
- package/src/tui.ts +2200 -0
- package/src/types/bun-ffi.d.ts +6 -0
- package/src/web/protocol.ts +648 -0
- package/test/agent-name.test.ts +62 -0
- package/test/agent-tree-fleet-launch.test.ts +64 -0
- package/test/agent-tree-reducer-parity.test.ts +194 -0
- package/test/agent-tree-reducer.test.ts +443 -0
- package/test/agent-tree-rollup.test.ts +109 -0
- package/test/autobio-progress-snapshot.test.ts +40 -0
- package/test/claudeai-export-importer.test.ts +386 -0
- package/test/evacuator.test.ts +332 -0
- package/test/fleet-commands.test.ts +244 -0
- package/test/fleet-no-subfleets.test.ts +147 -0
- package/test/fleet-orchestration.test.ts +353 -0
- package/test/fleet-recipe.test.ts +124 -0
- package/test/fleet-route.test.ts +44 -0
- package/test/fleet-smoke.test.ts +479 -0
- package/test/fleet-subscribe-union-e2e.test.ts +123 -0
- package/test/fleet-subscribe-union.test.ts +85 -0
- package/test/fleet-tree-aggregator-e2e.test.ts +110 -0
- package/test/fleet-tree-aggregator.test.ts +215 -0
- package/test/frontdesk-strategy.test.ts +326 -0
- package/test/headless-describe.test.ts +182 -0
- package/test/headless-smoke.test.ts +190 -0
- package/test/logging-adapter.test.ts +87 -0
- package/test/mcpl-admin-module.test.ts +213 -0
- package/test/mcpl-agent-overlay.test.ts +126 -0
- package/test/mock-headless-child.ts +133 -0
- package/test/recipe-cache-ttl.test.ts +37 -0
- package/test/recipe-env.test.ts +157 -0
- package/test/recipe-path-resolution.test.ts +170 -0
- package/test/subagent-async-timeout.test.ts +381 -0
- package/test/subagent-fork-materialise.test.ts +241 -0
- package/test/subagent-peek-scoping.test.ts +180 -0
- package/test/subagent-peek-zombie.test.ts +148 -0
- package/test/subagent-reaper-in-flight.test.ts +229 -0
- package/test/subscription-gc-module.test.ts +136 -0
- package/test/warmup-handoff.test.ts +150 -0
- package/test/web-ui-bind.test.ts +51 -0
- package/test/web-ui-module.test.ts +246 -0
- package/test/web-ui-protocol.test.ts +0 -0
- package/tsconfig.json +14 -0
- package/web/bun.lock +357 -0
- package/web/index.html +12 -0
- package/web/package.json +24 -0
- package/web/src/App.tsx +1931 -0
- package/web/src/Context.tsx +158 -0
- package/web/src/ContextDocument.tsx +150 -0
- package/web/src/Files.tsx +271 -0
- package/web/src/Lessons.tsx +164 -0
- package/web/src/Mcpl.tsx +310 -0
- package/web/src/Stream.tsx +119 -0
- package/web/src/TreeSidebar.tsx +283 -0
- package/web/src/Usage.tsx +182 -0
- package/web/src/main.tsx +7 -0
- package/web/src/styles.css +26 -0
- package/web/src/tree.ts +268 -0
- package/web/src/wire.ts +120 -0
- package/web/tsconfig.json +21 -0
- package/web/vite.config.ts +32 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The context-curve page served at GET /curve (web-ui-module).
|
|
3
|
+
*
|
|
4
|
+
* Self-contained HTML: fetches /debug/context/curve (same origin, same basic
|
|
5
|
+
* auth) and renders (a) an XY plot of cumulative raw-history tokens vs
|
|
6
|
+
* cumulative rendered context tokens — each segment one compiled entry, slope
|
|
7
|
+
* = local compression rate, colored by fold level — and (b) the full raw
|
|
8
|
+
* compiled window, filterable by kind. Both themes; no external resources.
|
|
9
|
+
*
|
|
10
|
+
* Born 2026-07-12 during the mythos inversion incident: the curve makes a
|
|
11
|
+
* misallocated resolution profile visible at a glance (an inversion reads as
|
|
12
|
+
* a flat shelf right before the tail; depth debt as a too-steep middle).
|
|
13
|
+
*/
|
|
14
|
+
export const CURVE_PAGE_HTML = `<!doctype html>
|
|
15
|
+
<html lang="en">
|
|
16
|
+
<head>
|
|
17
|
+
<meta charset="utf-8">
|
|
18
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
19
|
+
<title>context curve</title>
|
|
20
|
+
<style>
|
|
21
|
+
:root{
|
|
22
|
+
--surface:#FBFAF7; --ink:#211E19; --ink-2:#5C574D; --muted:#8A857C;
|
|
23
|
+
--hair:#E8E4DB; --hair-2:#F0EDE5;
|
|
24
|
+
--c-raw:#B45309; --c-l1:#6D97E9; --c-l2:#4B78D6; --c-l3:#3B6FD4; --c-l4:#24408F;
|
|
25
|
+
--chip-ink:#FBFAF7; --tip-bg:#211E19; --tip-ink:#FBFAF7;
|
|
26
|
+
}
|
|
27
|
+
@media (prefers-color-scheme: dark){:root{
|
|
28
|
+
--surface:#17191D; --ink:#E9E6DF; --ink-2:#B0ACA2; --muted:#8B8FA0;
|
|
29
|
+
--hair:#2A2D34; --hair-2:#22252B;
|
|
30
|
+
--c-raw:#C97426; --c-l1:#8FB0F4; --c-l2:#6E9CEF; --c-l3:#5D8FE8; --c-l4:#4573C9;
|
|
31
|
+
--chip-ink:#17191D; --tip-bg:#E9E6DF; --tip-ink:#17191D;
|
|
32
|
+
}}
|
|
33
|
+
html{background:var(--surface)}
|
|
34
|
+
body{margin:0;background:var(--surface);color:var(--ink);
|
|
35
|
+
font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif}
|
|
36
|
+
.wrap{max-width:1080px;margin:0 auto;padding:28px 24px 80px}
|
|
37
|
+
h1{font-family:Palatino,"Iowan Old Style","Palatino Linotype",serif;
|
|
38
|
+
font-size:26px;font-weight:600;margin:0 0 4px;text-wrap:balance}
|
|
39
|
+
.meta{color:var(--muted);font-size:13px;margin:0 0 20px}
|
|
40
|
+
.mono,pre,code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-variant-numeric:tabular-nums}
|
|
41
|
+
.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin:0 0 24px}
|
|
42
|
+
.tile{border:1px solid var(--hair);border-radius:6px;padding:11px 13px}
|
|
43
|
+
.tile .k{font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
|
|
44
|
+
.tile .v{font-size:22px;font-weight:600;margin-top:2px}
|
|
45
|
+
.tile .v small{font-size:12px;font-weight:400;color:var(--ink-2)}
|
|
46
|
+
.plotbox{border:1px solid var(--hair);border-radius:6px;padding:14px 10px 6px;margin-bottom:10px}
|
|
47
|
+
.legend{display:flex;flex-wrap:wrap;gap:14px;align-items:center;padding:6px 8px 10px;font-size:13px;color:var(--ink-2)}
|
|
48
|
+
.legend .sw{display:inline-block;width:14px;height:4px;border-radius:2px;vertical-align:middle;margin-right:6px}
|
|
49
|
+
svg text{font:11px ui-monospace,Menlo,monospace;fill:var(--muted)}
|
|
50
|
+
.gridline{stroke:var(--hair-2);stroke-width:1}
|
|
51
|
+
.axis{stroke:var(--hair);stroke-width:1}
|
|
52
|
+
.guide{stroke:var(--muted);stroke-width:1;stroke-dasharray:3 4;opacity:.6}
|
|
53
|
+
.seg{fill:none;stroke-width:2.5;stroke-linecap:round}
|
|
54
|
+
.seg.raw{stroke:var(--c-raw)} .seg.L1{stroke:var(--c-l1)} .seg.L2{stroke:var(--c-l2)}
|
|
55
|
+
.seg.L3{stroke:var(--c-l3)} .seg.L4{stroke:var(--c-l4)} .seg.L5{stroke:var(--c-l4)}
|
|
56
|
+
.hit{fill:none;stroke:transparent;stroke-width:16;cursor:pointer}
|
|
57
|
+
.seg.hot{stroke-width:4.5}
|
|
58
|
+
.xhair{stroke:var(--muted);stroke-width:1;stroke-dasharray:2 3;opacity:0;pointer-events:none}
|
|
59
|
+
#tip{position:fixed;z-index:10;max-width:380px;background:var(--tip-bg);color:var(--tip-ink);
|
|
60
|
+
border-radius:6px;padding:10px 12px;font-size:12.5px;line-height:1.45;pointer-events:none;
|
|
61
|
+
opacity:0;transition:opacity .08s;box-shadow:0 4px 18px rgba(0,0,0,.25)}
|
|
62
|
+
@media (prefers-reduced-motion: reduce){#tip{transition:none}}
|
|
63
|
+
#tip .h{font-weight:600;margin-bottom:2px}
|
|
64
|
+
#tip .d{opacity:.75}
|
|
65
|
+
.chip{display:inline-block;min-width:2.2em;text-align:center;border-radius:4px;padding:1px 7px;
|
|
66
|
+
font:600 11px/1.7 ui-monospace,Menlo,monospace;color:var(--chip-ink)}
|
|
67
|
+
.chip.raw{background:var(--c-raw)} .chip.L1{background:var(--c-l1)} .chip.L2{background:var(--c-l2)}
|
|
68
|
+
.chip.L3{background:var(--c-l3)} .chip.L4{background:var(--c-l4)} .chip.L5{background:var(--c-l4)}
|
|
69
|
+
.filters{display:flex;flex-wrap:wrap;gap:8px;margin:30px 0 14px;position:sticky;top:0;
|
|
70
|
+
background:var(--surface);padding:10px 0;border-bottom:1px solid var(--hair);z-index:5}
|
|
71
|
+
.filters button{border:1px solid var(--hair);background:none;color:var(--ink-2);border-radius:99px;
|
|
72
|
+
padding:4px 14px;font:13px system-ui,sans-serif;cursor:pointer}
|
|
73
|
+
.filters button[aria-pressed="true"]{border-color:var(--ink-2);color:var(--ink);font-weight:600}
|
|
74
|
+
.filters button:focus-visible{outline:2px solid var(--c-l3);outline-offset:2px}
|
|
75
|
+
.entry{border:1px solid var(--hair);border-radius:6px;margin-bottom:8px;scroll-margin-top:70px}
|
|
76
|
+
.entry[hidden]{display:none}
|
|
77
|
+
.entry summary{display:flex;gap:10px;align-items:baseline;flex-wrap:wrap;cursor:pointer;
|
|
78
|
+
padding:9px 12px;list-style:none}
|
|
79
|
+
.entry summary::-webkit-details-marker{display:none}
|
|
80
|
+
.entry summary:focus-visible{outline:2px solid var(--c-l3);outline-offset:-2px}
|
|
81
|
+
.entry .who{font-weight:600;font-size:13.5px}
|
|
82
|
+
.entry .stat{color:var(--muted);font-size:12px;margin-left:auto;white-space:nowrap}
|
|
83
|
+
.entry .peek{color:var(--ink-2);font-size:13px;flex-basis:100%;overflow:hidden;
|
|
84
|
+
text-overflow:ellipsis;white-space:nowrap;margin:0}
|
|
85
|
+
.entry[open] .peek{display:none}
|
|
86
|
+
.entry pre{margin:0;border-top:1px solid var(--hair);padding:12px 14px;white-space:pre-wrap;
|
|
87
|
+
word-break:break-word;font-size:12.5px;line-height:1.5;max-height:480px;overflow-y:auto}
|
|
88
|
+
.entry .imgs{color:var(--muted);font-size:12px;padding:0 14px 10px}
|
|
89
|
+
.note{color:var(--muted);font-size:13px;margin:6px 0 0}
|
|
90
|
+
#status{color:var(--muted);padding:40px 0;text-align:center}
|
|
91
|
+
</style>
|
|
92
|
+
</head>
|
|
93
|
+
<body>
|
|
94
|
+
<div class="wrap">
|
|
95
|
+
<h1 id="title">context curve</h1>
|
|
96
|
+
<p class="meta" id="meta">loading…</p>
|
|
97
|
+
<div id="status">compiling the live window…</div>
|
|
98
|
+
<div id="content" hidden>
|
|
99
|
+
<div class="tiles" id="tiles"></div>
|
|
100
|
+
<div class="plotbox">
|
|
101
|
+
<svg id="plot" viewBox="0 0 1040 470" role="img"
|
|
102
|
+
aria-label="Cumulative raw tokens versus rendered context tokens, colored by fold level"></svg>
|
|
103
|
+
<div class="legend" id="legend"></div>
|
|
104
|
+
<p class="note" style="padding:0 8px 8px">Each segment is one context entry, oldest → newest.
|
|
105
|
+
Slope = how much window each token of history costs: flat = deeply folded, 45° = verbatim.
|
|
106
|
+
The dashed guide is 1:1 (uncompressed). Hover for detail; click a segment to jump to its text below.</p>
|
|
107
|
+
</div>
|
|
108
|
+
<div class="filters" id="filters" role="group" aria-label="Filter entries by kind"></div>
|
|
109
|
+
<div id="list"></div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
<div id="tip" role="status"></div>
|
|
113
|
+
<script>
|
|
114
|
+
(async () => {
|
|
115
|
+
const status = document.getElementById('status');
|
|
116
|
+
let payload;
|
|
117
|
+
try {
|
|
118
|
+
const res = await fetch('/debug/context/curve' + location.search, { credentials: 'same-origin' });
|
|
119
|
+
if (!res.ok) throw new Error('HTTP ' + res.status + ': ' + (await res.text()).slice(0, 200));
|
|
120
|
+
payload = await res.json();
|
|
121
|
+
} catch (e) {
|
|
122
|
+
status.textContent = 'failed to load: ' + e.message;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const D = payload.entries;
|
|
126
|
+
document.getElementById('title').textContent = payload.agent + ' — compiled context';
|
|
127
|
+
document.title = payload.agent + ' — context curve';
|
|
128
|
+
document.getElementById('meta').textContent =
|
|
129
|
+
'branch ' + payload.branch + ' · compiled ' + payload.generatedAt.slice(0, 16).replace('T', ' ') +
|
|
130
|
+
'Z · budget ' + Math.round(payload.budget.maxTokens / 1000) + 'k (hard ' +
|
|
131
|
+
Math.round((payload.budget.maxTokens - payload.budget.reserveForResponse) / 1000) + 'k)';
|
|
132
|
+
|
|
133
|
+
let cx = 0, cy = 0;
|
|
134
|
+
for (const e of D) { e.x0 = cx; e.y0 = cy; cx += e.rawCovered; cy += e.rendered; e.x1 = cx; e.y1 = cy; }
|
|
135
|
+
const KINDS = ['raw','L1','L2','L3','L4','L5'].filter(k => D.some(e => e.kind === k));
|
|
136
|
+
const fmt = n => n >= 1e6 ? (n/1e6).toFixed(2)+'M' : n >= 1000 ? Math.round(n/1000)+'k' : String(n);
|
|
137
|
+
const esc = s => s.replace(/&/g,'&').replace(/</g,'<');
|
|
138
|
+
const date = s => s ? new Date(s).toISOString().slice(0,16).replace('T',' ') : '';
|
|
139
|
+
|
|
140
|
+
const totRaw = cx, totRen = cy;
|
|
141
|
+
document.getElementById('tiles').innerHTML =
|
|
142
|
+
'<div class="tile"><div class="k">Raw history covered</div><div class="v mono">' + fmt(totRaw) + ' <small>tok est</small></div></div>' +
|
|
143
|
+
'<div class="tile"><div class="k">Rendered context</div><div class="v mono">' + fmt(totRen) + ' <small>tok est</small></div></div>' +
|
|
144
|
+
'<div class="tile"><div class="k">Compression</div><div class="v mono">' + (totRaw/Math.max(1,totRen)).toFixed(1) + ':1</div></div>' +
|
|
145
|
+
'<div class="tile"><div class="k">Entries</div><div class="v mono">' + D.length + ' <small>' +
|
|
146
|
+
KINDS.map(k => k + ':' + D.filter(e => e.kind === k).length).join(' · ') + '</small></div></div>';
|
|
147
|
+
|
|
148
|
+
// ---- plot ----
|
|
149
|
+
const svg = document.getElementById('plot');
|
|
150
|
+
const W = 1040, H = 470, m = {l:58, r:16, t:34, b:34};
|
|
151
|
+
const maxX = totRaw, maxY = totRen * 1.04;
|
|
152
|
+
const sx = x => m.l + (W-m.l-m.r) * x / maxX;
|
|
153
|
+
const sy = y => H - m.b - (H-m.t-m.b) * y / maxY;
|
|
154
|
+
let S = '';
|
|
155
|
+
const yt = 5, xt = 6;
|
|
156
|
+
for (let i = 0; i <= yt; i++){
|
|
157
|
+
const v = maxY/yt*i, y = sy(v);
|
|
158
|
+
S += '<line class="gridline" x1="'+m.l+'" x2="'+(W-m.r)+'" y1="'+y+'" y2="'+y+'"/>';
|
|
159
|
+
S += '<text x="'+(m.l-8)+'" y="'+(y+4)+'" text-anchor="end">'+fmt(Math.round(v))+'</text>';
|
|
160
|
+
}
|
|
161
|
+
for (let i = 0; i <= xt; i++){
|
|
162
|
+
const v = maxX/xt*i, x = sx(v);
|
|
163
|
+
S += '<text x="'+x+'" y="'+(H-m.b+18)+'" text-anchor="middle">'+fmt(Math.round(v))+'</text>';
|
|
164
|
+
}
|
|
165
|
+
S += '<line class="axis" x1="'+m.l+'" x2="'+(W-m.r)+'" y1="'+sy(0)+'" y2="'+sy(0)+'"/>';
|
|
166
|
+
S += '<text x="'+(W-m.r)+'" y="'+(H-6)+'" text-anchor="end">cumulative raw tokens (history) →</text>';
|
|
167
|
+
S += '<text x="14" y="18" text-anchor="start">rendered ↑</text>';
|
|
168
|
+
const gx = Math.min(maxX, maxY);
|
|
169
|
+
S += '<line class="guide" x1="'+sx(0)+'" y1="'+sy(0)+'" x2="'+sx(gx)+'" y2="'+sy(gx)+'"/>';
|
|
170
|
+
S += '<text x="'+(sx(gx)+6)+'" y="'+(sy(gx)+2)+'">1:1</text>';
|
|
171
|
+
for (const e of D){
|
|
172
|
+
const p = 'x1="'+sx(e.x0)+'" y1="'+sy(e.y0)+'" x2="'+sx(e.x1)+'" y2="'+sy(e.y1)+'"';
|
|
173
|
+
S += '<line class="seg '+e.kind+'" data-i="'+e.i+'" '+p+'/>';
|
|
174
|
+
S += '<line class="hit" data-i="'+e.i+'" '+p+'/>';
|
|
175
|
+
}
|
|
176
|
+
S += '<line id="xh" class="xhair" y1="'+m.t+'" y2="'+(H-m.b)+'" x1="0" x2="0"/>';
|
|
177
|
+
svg.innerHTML = S;
|
|
178
|
+
|
|
179
|
+
document.getElementById('legend').innerHTML = KINDS.map(k =>
|
|
180
|
+
'<span><span class="sw" style="background:var(--c-'+k.toLowerCase()+')"></span>' +
|
|
181
|
+
(k === 'raw' ? 'raw (verbatim)' : k + ' summary') + ' · ' + D.filter(e=>e.kind===k).length + '</span>'
|
|
182
|
+
).join('');
|
|
183
|
+
|
|
184
|
+
const tip = document.getElementById('tip');
|
|
185
|
+
const xh = document.getElementById('xh');
|
|
186
|
+
let hot = null;
|
|
187
|
+
function show(e, ev){
|
|
188
|
+
const el = svg.querySelector('.seg[data-i="'+e.i+'"]');
|
|
189
|
+
if (hot && hot !== el) hot.classList.remove('hot');
|
|
190
|
+
el.classList.add('hot'); hot = el;
|
|
191
|
+
const r = e.rawCovered && e.rendered ? (e.rawCovered/e.rendered).toFixed(1) : '—';
|
|
192
|
+
tip.innerHTML =
|
|
193
|
+
'<div class="h"><span class="chip '+e.kind+'">'+e.kind+'</span> '+esc(e.id ?? '')+' · '+esc(e.participant)+'</div>' +
|
|
194
|
+
'<div>'+fmt(e.rawCovered)+' raw → '+fmt(e.rendered)+' rendered ('+r+':1) · '+e.msgCount+' msg'+(e.nImages ? ' · '+e.nImages+' img' : '')+'</div>' +
|
|
195
|
+
'<div class="d">'+date(e.dateFirst)+(e.dateLast && e.dateLast !== e.dateFirst ? ' → '+date(e.dateLast) : '')+'</div>' +
|
|
196
|
+
'<div class="d">'+esc(e.text.slice(0,150))+'…</div>';
|
|
197
|
+
tip.style.opacity = 1;
|
|
198
|
+
const tw = 390;
|
|
199
|
+
tip.style.left = Math.min(ev.clientX + 14, innerWidth - tw) + 'px';
|
|
200
|
+
tip.style.top = Math.min(ev.clientY + 14, innerHeight - 150) + 'px';
|
|
201
|
+
const pt = svg.createSVGPoint(); pt.x = ev.clientX; pt.y = ev.clientY;
|
|
202
|
+
const loc = pt.matrixTransform(svg.getScreenCTM().inverse());
|
|
203
|
+
xh.setAttribute('x1', loc.x); xh.setAttribute('x2', loc.x); xh.style.opacity = 1;
|
|
204
|
+
}
|
|
205
|
+
svg.addEventListener('pointermove', ev => {
|
|
206
|
+
const t = ev.target.closest('.hit');
|
|
207
|
+
if (t) show(D[+t.dataset.i], ev);
|
|
208
|
+
});
|
|
209
|
+
svg.addEventListener('pointerleave', () => {
|
|
210
|
+
tip.style.opacity = 0; xh.style.opacity = 0;
|
|
211
|
+
if (hot) hot.classList.remove('hot'); hot = null;
|
|
212
|
+
});
|
|
213
|
+
svg.addEventListener('click', ev => {
|
|
214
|
+
const t = ev.target.closest('.hit');
|
|
215
|
+
if (!t) return;
|
|
216
|
+
const el = document.getElementById('e' + t.dataset.i);
|
|
217
|
+
if (el){ el.hidden = false; el.open = true; el.scrollIntoView({behavior:'smooth', block:'start'}); }
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// ---- entries ----
|
|
221
|
+
document.getElementById('list').innerHTML = D.map(e => {
|
|
222
|
+
const r = e.rawCovered && e.rendered ? (e.rawCovered/e.rendered).toFixed(1) : '—';
|
|
223
|
+
return '<details class="entry" id="e'+e.i+'" data-kind="'+e.kind+'">' +
|
|
224
|
+
'<summary><span class="chip '+e.kind+'">'+e.kind+'</span><span class="who">'+esc(e.participant)+'</span>' +
|
|
225
|
+
'<span class="stat mono">'+fmt(e.rawCovered)+'→'+fmt(e.rendered)+' ('+r+':1)'+(e.dateFirst ? ' · '+date(e.dateFirst) : '')+'</span>' +
|
|
226
|
+
'<p class="peek">'+esc(e.text.slice(0,180))+'</p></summary>' +
|
|
227
|
+
(e.nImages ? '<div class="imgs">['+e.nImages+' inline image'+(e.nImages>1?'s':'')+' not shown in this dump]</div>' : '') +
|
|
228
|
+
'<pre>'+esc(e.text)+'</pre></details>';
|
|
229
|
+
}).join('');
|
|
230
|
+
|
|
231
|
+
// ---- filters ----
|
|
232
|
+
const fl = document.getElementById('filters');
|
|
233
|
+
fl.innerHTML = ['all', ...KINDS].map(k =>
|
|
234
|
+
'<button aria-pressed="'+(k==='all')+'" data-k="'+k+'">'+k+' · '+(k==='all' ? D.length : D.filter(e=>e.kind===k).length)+'</button>'
|
|
235
|
+
).join('');
|
|
236
|
+
fl.addEventListener('click', ev => {
|
|
237
|
+
const b = ev.target.closest('button'); if (!b) return;
|
|
238
|
+
fl.querySelectorAll('button').forEach(x => x.setAttribute('aria-pressed', String(x === b)));
|
|
239
|
+
const k = b.dataset.k;
|
|
240
|
+
document.querySelectorAll('.entry').forEach(el => { el.hidden = k !== 'all' && el.dataset.kind !== k; });
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
status.hidden = true;
|
|
244
|
+
document.getElementById('content').hidden = false;
|
|
245
|
+
})();
|
|
246
|
+
</script>
|
|
247
|
+
</body>
|
|
248
|
+
</html>
|
|
249
|
+
`;
|