@dna113p/machines 0.3.3 → 0.3.5

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.
@@ -0,0 +1,690 @@
1
+ /**
2
+ * The page `machine ui` serves: one self-contained document, with no outside
3
+ * scripts, styles, or fonts, so it works offline and loads nothing but its own
4
+ * data. The token lets only this page answer a run.
5
+ */
6
+ export function runUiPage(token) {
7
+ return page.replace("__MACHINES_UI_TOKEN__", token);
8
+ }
9
+ // The page script avoids template literals so that it can sit inside this one.
10
+ const page = String.raw `<!doctype html>
11
+ <html lang="en">
12
+ <head>
13
+ <meta charset="utf-8">
14
+ <meta name="viewport" content="width=device-width, initial-scale=1">
15
+ <title>Machines</title>
16
+ <style>
17
+ :root {
18
+ --bg: #f6f7f9; --panel: #ffffff; --panel-2: #f0f2f5; --ink: #16181d; --muted: #646b78; --faint: #9aa1ad;
19
+ --line: #e1e4ea; --edge: #c3c8d2; --accent: #4f5bd5; --accent-soft: rgba(79, 91, 213, 0.12);
20
+ --agent: #7c4dcc; --human: #c77700; --operation: #1f7a99; --final: #2f8f4e; --group: #8a8f99;
21
+ --running: #2f6fdb; --waiting: #c77700; --completed: #2f8f4e; --failed: #cc3a3a; --lost: #8a8f99;
22
+ --shadow: 0 1px 2px rgba(16, 24, 40, 0.06), 0 1px 3px rgba(16, 24, 40, 0.08);
23
+ color-scheme: light;
24
+ }
25
+ @media (prefers-color-scheme: dark) {
26
+ :root {
27
+ --bg: #0f1115; --panel: #171a21; --panel-2: #1e222b; --ink: #e8eaef; --muted: #9aa1ad; --faint: #6b7280;
28
+ --line: #2a2f3a; --edge: #3d4452; --accent: #8b94ff; --accent-soft: rgba(139, 148, 255, 0.16);
29
+ --agent: #b18cff; --human: #f0a531; --operation: #4fb3d4; --final: #5cc282; --group: #8a8f99;
30
+ --running: #6c9dff; --waiting: #f0a531; --completed: #5cc282; --failed: #ff6b6b; --lost: #8a8f99;
31
+ --shadow: none;
32
+ color-scheme: dark;
33
+ }
34
+ }
35
+ * { box-sizing: border-box; }
36
+ html, body { margin: 0; height: 100%; }
37
+ body { background: var(--bg); color: var(--ink); font: 14px/1.45 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
38
+ button { font: inherit; color: inherit; }
39
+ .top { display: flex; align-items: center; gap: 16px; padding: 10px 16px; border-bottom: 1px solid var(--line); background: var(--panel); position: sticky; top: 0; z-index: 5; }
40
+ .brand { font-weight: 650; display: flex; align-items: center; gap: 8px; white-space: nowrap; }
41
+ .brand .logo { color: var(--accent); }
42
+ .brand .project { color: var(--muted); font-weight: 500; }
43
+ .counts { display: flex; gap: 8px; flex-wrap: wrap; flex: 1; }
44
+ .chip { display: inline-flex; align-items: center; gap: 6px; padding: 2px 9px; border-radius: 999px; background: var(--panel-2); color: var(--muted); font-size: 12.5px; white-space: nowrap; }
45
+ .chip b { color: var(--ink); font-weight: 600; }
46
+ .live { font-size: 12.5px; color: var(--muted); display: flex; align-items: center; gap: 6px; white-space: nowrap; }
47
+ .live::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: var(--faint); }
48
+ .live.on::before { background: var(--completed); box-shadow: 0 0 0 3px rgba(47, 143, 78, 0.18); }
49
+ .layout { display: grid; grid-template-columns: 300px minmax(0, 1fr) 340px; gap: 16px; padding: 16px; align-items: start; }
50
+ @media (max-width: 1250px) { .layout { grid-template-columns: 280px minmax(0, 1fr); } .panels { grid-column: 1 / -1; } }
51
+ @media (max-width: 780px) { .layout { grid-template-columns: minmax(0, 1fr); padding: 12px 16px; } .top { flex-wrap: wrap; } }
52
+ .card { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; box-shadow: var(--shadow); }
53
+ .runs { display: flex; flex-direction: column; gap: 14px; position: sticky; top: 64px; max-height: calc(100vh - 80px); overflow: auto; }
54
+ @media (max-width: 780px) { .runs { position: static; max-height: none; } }
55
+ .section-title { font-size: 11.5px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); font-weight: 600; margin: 0 4px 6px; display: flex; justify-content: space-between; }
56
+ .run { width: 100%; text-align: left; border: 1px solid var(--line); background: var(--panel); border-radius: 10px; padding: 9px 11px; cursor: pointer; display: grid; grid-template-columns: 10px minmax(0, 1fr); gap: 4px 9px; margin-bottom: 6px; box-shadow: var(--shadow); }
57
+ .run:hover { border-color: var(--edge); }
58
+ .run.selected { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
59
+ .run .dot { margin-top: 6px; }
60
+ .run .title { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
61
+ .run .sub { grid-column: 2; color: var(--muted); font-size: 12.5px; display: flex; gap: 6px; flex-wrap: wrap; }
62
+ .run .child { grid-column: 2; font-size: 12px; color: var(--faint); }
63
+ .dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; flex: none; background: var(--lost); }
64
+ .dot.running { background: var(--running); box-shadow: 0 0 0 3px rgba(47, 111, 219, 0.18); }
65
+ .dot.waiting { background: var(--waiting); animation: pulse 1.6s ease-in-out infinite; }
66
+ .dot.completed { background: var(--completed); }
67
+ .dot.failed { background: var(--failed); }
68
+ .dot.ok { background: var(--completed); } .dot.warn { background: var(--waiting); } .dot.bad { background: var(--failed); } .dot.active { background: var(--running); } .dot.idle { background: var(--faint); }
69
+ @keyframes pulse { 50% { box-shadow: 0 0 0 5px rgba(199, 119, 0, 0.18); } }
70
+ .empty { color: var(--muted); padding: 18px; text-align: center; font-size: 13px; }
71
+ .detail { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
72
+ .head { padding: 14px 16px; display: flex; flex-direction: column; gap: 8px; }
73
+ .head h1 { font-size: 18px; margin: 0; line-height: 1.3; overflow-wrap: anywhere; }
74
+ .pill { display: inline-flex; align-items: center; gap: 6px; padding: 2px 10px; border-radius: 999px; font-size: 12.5px; font-weight: 600; background: var(--panel-2); }
75
+ .meta { display: flex; flex-wrap: wrap; gap: 6px 18px; color: var(--muted); font-size: 12.5px; }
76
+ .meta b { color: var(--ink); font-weight: 600; }
77
+ .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12.5px; }
78
+ .banner { padding: 10px 14px; border-radius: 10px; font-size: 13px; }
79
+ .banner.limit { background: rgba(199, 119, 0, 0.1); border: 1px solid rgba(199, 119, 0, 0.35); }
80
+ .banner.error { background: rgba(204, 58, 58, 0.08); border: 1px solid rgba(204, 58, 58, 0.35); white-space: pre-wrap; overflow-wrap: anywhere; }
81
+ .ask { padding: 14px 16px; border-color: var(--waiting); box-shadow: 0 0 0 3px rgba(199, 119, 0, 0.12); display: flex; flex-direction: column; gap: 10px; }
82
+ .ask h2, .block h2 { font-size: 13px; margin: 0; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); }
83
+ .prompt { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 360px; overflow: auto; background: var(--panel-2); border-radius: 8px; padding: 10px 12px; font-size: 13.5px; }
84
+ .choices { display: flex; flex-wrap: wrap; gap: 8px; }
85
+ .btn { border: 1px solid var(--line); background: var(--panel); border-radius: 8px; padding: 6px 12px; cursor: pointer; }
86
+ .btn:hover { border-color: var(--accent); }
87
+ .btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
88
+ .btn:disabled { opacity: 0.5; cursor: default; }
89
+ textarea { width: 100%; min-height: 70px; resize: vertical; border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px; background: var(--panel); color: var(--ink); font: inherit; }
90
+ .row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
91
+ .hint { color: var(--muted); font-size: 12.5px; }
92
+ .block { padding: 14px 16px; display: flex; flex-direction: column; gap: 10px; }
93
+ .graph-head { display: flex; justify-content: space-between; align-items: center; gap: 10px; flex-wrap: wrap; }
94
+ .legend { display: flex; gap: 12px; flex-wrap: wrap; font-size: 12px; color: var(--muted); }
95
+ .legend span { display: inline-flex; align-items: center; gap: 5px; }
96
+ .legend i { width: 10px; height: 10px; border-radius: 3px; display: inline-block; }
97
+ .tools { display: flex; gap: 6px; }
98
+ .tools .btn { padding: 2px 9px; }
99
+ .graph { position: relative; height: clamp(420px, calc(100vh - 250px), 760px); border-radius: 10px; background: var(--panel-2); overflow: hidden; touch-action: none; cursor: grab; }
100
+ .graph.dragging { cursor: grabbing; }
101
+ .graph.none { height: auto; cursor: default; }
102
+ .graph svg { width: 100%; height: 100%; display: block; }
103
+ .node rect.base { fill: var(--panel); }
104
+ .node rect.box { fill: var(--panel); stroke: var(--line); stroke-width: 1.2; }
105
+ .node.visited rect.box { fill: var(--accent-soft); }
106
+ .node.current rect.box { stroke: var(--accent); stroke-width: 2.6; }
107
+ .node.current.failed rect.box { stroke: var(--failed); }
108
+ .node.current.waiting rect.box { stroke: var(--waiting); }
109
+ .node.current.completed rect.box { stroke: var(--completed); }
110
+ .node .halo { fill: none; stroke: var(--accent); stroke-width: 6; opacity: 0.25; animation: halo 1.8s ease-in-out infinite; }
111
+ @keyframes halo { 50% { opacity: 0.05; } }
112
+ .node text { fill: var(--ink); font-size: 12.5px; font-weight: 600; }
113
+ .node text.kind { fill: var(--muted); font-size: 10.5px; font-weight: 500; }
114
+ .node .badge { fill: var(--accent); }
115
+ .node text.count { fill: #fff; font-size: 10px; font-weight: 700; }
116
+ .node.unreached { opacity: 0.55; }
117
+ .edge path { fill: none; stroke: var(--edge); stroke-width: 1.3; }
118
+ .edge.terminal path { stroke-dasharray: 3 4; opacity: 0.7; }
119
+ .edge.visited path { stroke: var(--accent); stroke-width: 2.4; opacity: 1; stroke-dasharray: none; }
120
+ .edge.next path { stroke: var(--accent); stroke-dasharray: 5 5; animation: dash 1s linear infinite; opacity: 1; }
121
+ @keyframes dash { to { stroke-dashoffset: -20; } }
122
+ .edge text { font-size: 10.5px; fill: var(--muted); paint-order: stroke; stroke: var(--panel-2); stroke-width: 4px; stroke-linejoin: round; }
123
+ .edge.visited text, .edge.next text { fill: var(--accent); font-weight: 600; }
124
+ .edge:hover path { stroke: var(--ink); }
125
+ .trail { display: flex; flex-direction: column; max-height: 320px; overflow: auto; }
126
+ .step { display: grid; grid-template-columns: 82px 1fr auto; gap: 10px; padding: 5px 2px; border-bottom: 1px dashed var(--line); font-size: 13px; align-items: baseline; }
127
+ .step:last-child { border-bottom: 0; }
128
+ .step .t { color: var(--faint); font-size: 12px; }
129
+ .step .d { color: var(--muted); font-size: 12px; }
130
+ .step.now .s { color: var(--accent); font-weight: 650; }
131
+ .note { white-space: pre-wrap; overflow-wrap: anywhere; font-size: 13px; }
132
+ .panels { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
133
+ @media (max-width: 1250px) { .panels { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); align-items: start; } }
134
+ .panel { padding: 12px 14px; display: flex; flex-direction: column; gap: 8px; min-width: 0; }
135
+ .panel h3 { margin: 0; font-size: 13.5px; display: flex; justify-content: space-between; gap: 8px; }
136
+ .panel h3 span { color: var(--faint); font-weight: 500; font-size: 12px; }
137
+ .panel .summary { color: var(--muted); font-size: 12.5px; }
138
+ .items { display: flex; flex-direction: column; gap: 2px; }
139
+ .item { display: grid; grid-template-columns: 10px minmax(0, 1fr) auto; gap: 2px 8px; padding: 5px 0; border-bottom: 1px solid var(--line); font-size: 13px; }
140
+ .item:last-child { border-bottom: 0; }
141
+ .item .dot { margin-top: 6px; }
142
+ .item .text { overflow-wrap: anywhere; }
143
+ .item .text a { color: inherit; text-decoration: none; border-bottom: 1px solid var(--edge); }
144
+ .item .text a:hover { color: var(--accent); border-color: var(--accent); }
145
+ .item .detail { grid-column: 2 / 4; color: var(--muted); font-size: 12px; overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
146
+ .item .detail:hover { -webkit-line-clamp: unset; }
147
+ .items { max-height: 420px; overflow: auto; }
148
+ .item .at { color: var(--faint); font-size: 12px; white-space: nowrap; }
149
+ .panel .error { color: var(--failed); font-size: 12.5px; overflow-wrap: anywhere; }
150
+ .subruns { display: flex; flex-direction: column; gap: 4px; }
151
+ .subrun { display: flex; gap: 8px; align-items: center; font-size: 13px; cursor: pointer; padding: 4px 6px; border-radius: 6px; }
152
+ .subrun:hover { background: var(--panel-2); }
153
+ </style>
154
+ </head>
155
+ <body>
156
+ <header class="top">
157
+ <div class="brand"><span class="logo">&#9670;</span> Machines <span class="project" id="project"></span></div>
158
+ <div class="counts" id="counts"></div>
159
+ <div class="live" id="live">connecting</div>
160
+ </header>
161
+ <div class="layout">
162
+ <aside class="runs" id="runs"></aside>
163
+ <main class="detail" id="detail"></main>
164
+ <section class="panels" id="panels"></section>
165
+ </div>
166
+ <script>
167
+ "use strict";
168
+ var TOKEN = "__MACHINES_UI_TOKEN__";
169
+ var data = null;
170
+ var selected = decodeURIComponent(location.hash.slice(1)) || null;
171
+ var views = {};
172
+ var layouts = {};
173
+ var drafts = {};
174
+ var sending = false;
175
+
176
+ function h(tag, attrs) {
177
+ var el = document.createElement(tag);
178
+ if (attrs) for (var key in attrs) {
179
+ var value = attrs[key];
180
+ if (value === undefined || value === null || value === false) continue;
181
+ if (key === "text") el.textContent = value;
182
+ else if (key === "class") el.className = value;
183
+ else if (key.slice(0, 2) === "on") el.addEventListener(key.slice(2), value);
184
+ else el.setAttribute(key, value === true ? "" : value);
185
+ }
186
+ for (var i = 2; i < arguments.length; i++) append(el, arguments[i]);
187
+ return el;
188
+ }
189
+ function append(el, child) {
190
+ if (child === undefined || child === null || child === false) return;
191
+ if (Array.isArray(child)) { child.forEach(function (c) { append(el, c); }); return; }
192
+ el.appendChild(typeof child === "string" ? document.createTextNode(child) : child);
193
+ }
194
+ function s(tag, attrs) {
195
+ var el = document.createElementNS("http://www.w3.org/2000/svg", tag);
196
+ if (attrs) for (var key in attrs) {
197
+ if (attrs[key] === undefined || attrs[key] === null) continue;
198
+ if (key === "text") el.textContent = attrs[key]; else if (key === "class") el.setAttribute("class", attrs[key]); else el.setAttribute(key, attrs[key]);
199
+ }
200
+ for (var i = 2; i < arguments.length; i++) if (arguments[i]) el.appendChild(arguments[i]);
201
+ return el;
202
+ }
203
+
204
+ function duration(ms) {
205
+ if (!(ms >= 0)) return "";
206
+ var sec = Math.floor(ms / 1000);
207
+ if (sec < 60) return sec + "s";
208
+ var min = Math.floor(sec / 60);
209
+ if (min < 60) return min + "m " + String(sec % 60).padStart(2, "0") + "s";
210
+ var hr = Math.floor(min / 60);
211
+ if (hr < 48) return hr + "h " + String(min % 60).padStart(2, "0") + "m";
212
+ return Math.floor(hr / 24) + "d";
213
+ }
214
+ function ago(iso) { var t = Date.parse(iso); return isNaN(t) ? "" : duration(Date.now() - t) + " ago"; }
215
+ function clock(iso) { var d = new Date(iso); return isNaN(d) ? "" : d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); }
216
+ function live(iso, suffix) { return h("span", { "data-since": iso, "data-suffix": suffix || "", text: duration(Date.now() - Date.parse(iso)) + (suffix || "") }); }
217
+ function active(run) { return run.status === "running" || run.status === "waiting"; }
218
+ function title(run) { return run.label || run.machine; }
219
+
220
+ function paths(value) {
221
+ if (typeof value !== "string" || value === "") return [];
222
+ if (value.charAt(0) !== "{") return [value];
223
+ try { return flatten(JSON.parse(value)); } catch (e) { return [value]; }
224
+ }
225
+ function flatten(value) {
226
+ if (typeof value === "string") return [value];
227
+ if (!value || typeof value !== "object") return [];
228
+ var out = [];
229
+ Object.keys(value).forEach(function (key) {
230
+ var below = flatten(value[key]);
231
+ if (below.length === 0) out.push(key); else below.forEach(function (p) { out.push(key + "." + p); });
232
+ });
233
+ return out;
234
+ }
235
+ function stateText(run) { return paths(run.state).join(", "); }
236
+
237
+ function connect() {
238
+ var source = new EventSource("/api/events");
239
+ var liveEl = document.getElementById("live");
240
+ source.onopen = function () { liveEl.textContent = "live"; liveEl.className = "live on"; };
241
+ source.onerror = function () { liveEl.textContent = "reconnecting"; liveEl.className = "live"; };
242
+ source.onmessage = function (event) { data = JSON.parse(event.data); render(); };
243
+ }
244
+
245
+ function render() {
246
+ if (!data) return;
247
+ document.getElementById("project").textContent = data.project.name;
248
+ document.title = "Machines · " + data.project.name;
249
+ var runs = data.runs;
250
+ var byId = {};
251
+ runs.forEach(function (r) { byId[r.id] = r; });
252
+ if (!selected || !byId[selected]) {
253
+ var pick = runs.filter(function (r) { return !r.parent || !byId[r.parent]; });
254
+ var first = pick.find(function (r) { return r.status === "waiting"; }) || pick.find(function (r) { return r.status === "running"; }) || pick[0];
255
+ selected = first ? first.id : null;
256
+ }
257
+ // Sub-runs are counted with the run that started them.
258
+ renderCounts(runs.filter(function (r) { return !r.parent || !byId[r.parent]; }));
259
+ renderRuns(runs, byId);
260
+ renderDetail(selected ? byId[selected] : null, runs, byId);
261
+ renderPanels(data.panels);
262
+ }
263
+
264
+ function renderCounts(runs) {
265
+ var count = function (status) { return runs.filter(function (r) { return r.status === status; }).length; };
266
+ var day = Date.now() - 24 * 3600 * 1000;
267
+ var failed = runs.filter(function (r) { return r.status === "failed" && Date.parse(r.updatedAt) > day; }).length;
268
+ var el = document.getElementById("counts");
269
+ el.replaceChildren.apply(el, [
270
+ h("span", { class: "chip" }, h("span", { class: "dot running" }), h("b", { text: String(count("running")) }), "running"),
271
+ h("span", { class: "chip" }, h("span", { class: "dot waiting" }), h("b", { text: String(count("waiting")) }), "waiting on you"),
272
+ failed ? h("span", { class: "chip" }, h("span", { class: "dot failed" }), h("b", { text: String(failed) }), "failed today") : null,
273
+ count("lost") ? h("span", { class: "chip" }, h("span", { class: "dot" }), h("b", { text: String(count("lost")) }), "lost") : null
274
+ ].filter(Boolean));
275
+ }
276
+
277
+ function renderRuns(runs, byId) {
278
+ var top = runs.filter(function (r) { return !r.parent || !byId[r.parent]; });
279
+ var children = {};
280
+ runs.forEach(function (r) { if (r.parent && byId[r.parent]) (children[r.parent] = children[r.parent] || []).push(r); });
281
+ var groups = [
282
+ ["Waiting on you", top.filter(function (r) { return r.status === "waiting"; })],
283
+ ["Running", top.filter(function (r) { return r.status === "running"; })],
284
+ ["Lost", top.filter(function (r) { return r.status === "lost"; })],
285
+ ["Finished", top.filter(function (r) { return r.status === "completed" || r.status === "failed"; })],
286
+ ];
287
+ var el = document.getElementById("runs");
288
+ var sections = groups.filter(function (g) { return g[1].length; }).map(function (g) {
289
+ return h("div", null, h("div", { class: "section-title" }, h("span", { text: g[0] }), h("span", { text: String(g[1].length) })),
290
+ g[1].map(function (r) { return runCard(r, children[r.id] || []); }));
291
+ });
292
+ if (sections.length === 0) sections = [h("div", { class: "card empty" }, "No published runs yet. Runs started by machine run, Dispatch, MCP, or Pi appear here as they start.", h("div", { class: "mono", text: data.directory }))];
293
+ el.replaceChildren.apply(el, sections);
294
+ }
295
+
296
+ function runCard(run, kids) {
297
+ var running = kids.filter(active);
298
+ return h("button", { class: "run" + (run.id === selected ? " selected" : ""), onclick: function () { select(run.id); } },
299
+ h("span", { class: "dot " + run.status }),
300
+ h("span", { class: "title", title: title(run), text: title(run) }),
301
+ h("span", { class: "sub" }, [run.label ? run.machine : null, stateText(run) || null].filter(Boolean).concat([""]).join(" · "),
302
+ active(run) ? live(run.startedAt) : h("span", { text: ago(run.updatedAt) })),
303
+ kids.length ? h("span", { class: "child", text: kids.length + " sub-run" + (kids.length > 1 ? "s" : "") + (running.length ? ", " + running.length + " active" : "") }) : null);
304
+ }
305
+
306
+ function select(id) {
307
+ selected = id;
308
+ history.replaceState(null, "", "#" + encodeURIComponent(id));
309
+ render();
310
+ }
311
+
312
+ var statusWords = { running: "Running", waiting: "Waiting on you", completed: "Completed", failed: "Failed", lost: "Lost: its process is gone" };
313
+
314
+ function renderDetail(run, runs, byId) {
315
+ var el = document.getElementById("detail");
316
+ if (!run) { el.replaceChildren(h("div", { class: "card empty" }, "Select a run to see its Machine.")); return; }
317
+ var parts = [];
318
+ var agent = run.agent ? [run.agent.harness, run.agent.model, run.agent.thinking].filter(Boolean).join(" · ") : "";
319
+ parts.push(h("div", { class: "card head" },
320
+ h("div", { class: "row" }, h("span", { class: "pill" }, h("span", { class: "dot " + run.status }), statusWords[run.status] || run.status),
321
+ run.parent && byId[run.parent] ? h("button", { class: "btn", onclick: function () { select(run.parent); }, text: "↑ parent run" }) : null),
322
+ h("h1", { text: title(run) }),
323
+ h("div", { class: "meta" },
324
+ h("span", null, "Machine ", h("b", { text: run.machine })),
325
+ stateText(run) ? h("span", null, "State ", h("b", { text: stateText(run) }), run.stateSince && active(run) ? h("span", null, " for ", live(run.stateSince)) : null) : null,
326
+ agent ? h("span", null, "Agent ", h("b", { text: agent })) : null,
327
+ h("span", null, "Started ", h("b", { text: clock(run.startedAt) })),
328
+ h("span", null, active(run) ? "Running for " : "Took ", active(run) ? live(run.startedAt) : h("b", { text: duration(Date.parse(run.updatedAt) - Date.parse(run.startedAt)) }))),
329
+ h("div", { class: "meta mono", title: run.path, text: run.cwd })));
330
+ if (run.limit) parts.push(h("div", { class: "banner limit" }, "Waiting for an Agent usage limit: " + run.limit.text + (run.limit.until ? " (until " + clock(run.limit.until) + ")" : "")));
331
+ if (run.status === "waiting" && run.human) parts.push(askCard(run));
332
+ parts.push(graphCard(run));
333
+ var kids = runs.filter(function (r) { return r.parent === run.id; });
334
+ if (kids.length) parts.push(h("div", { class: "card block" }, h("h2", { text: "Sub-runs" }), h("div", { class: "subruns" }, kids.map(function (k) {
335
+ return h("div", { class: "subrun", onclick: function () { select(k.id); } }, h("span", { class: "dot " + k.status }), h("span", { text: title(k) }), h("span", { class: "hint", text: stateText(k) }));
336
+ }))));
337
+ if (run.trail && run.trail.length) parts.push(trailCard(run));
338
+ if (run.error) parts.push(h("div", { class: "banner error", text: run.error }));
339
+ if (run.note) parts.push(h("div", { class: "card block" }, h("h2", { text: "Result" }), h("div", { class: "note", text: run.note })));
340
+ var keep = document.activeElement && document.activeElement.tagName === "TEXTAREA" ? document.activeElement : null;
341
+ if (keep && el.contains(keep)) {
342
+ // Someone is typing an answer: leave the page as it is until they stop.
343
+ return;
344
+ }
345
+ el.replaceChildren.apply(el, parts);
346
+ drawGraph(run);
347
+ }
348
+
349
+ function askCard(run) {
350
+ var human = run.human;
351
+ var key = run.id + ":" + (human.requestId || "");
352
+ var status = h("div", { class: "hint" });
353
+ var card = h("div", { class: "card ask" }, h("h2", { text: "The run is asking" }), h("div", { class: "prompt", text: human.prompt }));
354
+ if (run.answeredBy === "host") {
355
+ card.appendChild(h("div", { class: "hint", text: "This run is answered through the program hosting it, such as Dispatch or an MCP session. Answer it there." }));
356
+ return card;
357
+ }
358
+ if (!human.requestId) {
359
+ card.appendChild(h("div", { class: "hint", text: "This run takes its answer only from the terminal it runs in." }));
360
+ return card;
361
+ }
362
+ function submit(text, question) {
363
+ if (sending || !text.trim()) return;
364
+ sending = true;
365
+ status.textContent = "Sending…";
366
+ card.querySelectorAll("button, textarea").forEach(function (b) { b.disabled = true; });
367
+ fetch("/api/respond", { method: "POST", headers: { "content-type": "application/json", "x-machines-ui": TOKEN },
368
+ body: JSON.stringify({ runId: run.id, requestId: human.requestId, response: text, question: question }) })
369
+ .then(function (r) { return r.json().then(function (body) { if (!r.ok) throw new Error(body.error || r.statusText); }); })
370
+ .then(function () { delete drafts[key]; status.textContent = "The run took it."; })
371
+ .catch(function (e) { status.textContent = e.message; card.querySelectorAll("button, textarea").forEach(function (b) { b.disabled = false; }); })
372
+ .finally(function () { sending = false; });
373
+ }
374
+ if (human.choices && human.choices.length) {
375
+ card.appendChild(h("div", { class: "choices" }, human.choices.map(function (c) {
376
+ return h("button", { class: "btn", text: c, onclick: function () { submit(c, false); } });
377
+ })));
378
+ } else {
379
+ var box = h("textarea", { placeholder: "Your answer", oninput: function () { drafts[key] = box.value; },
380
+ onblur: function () { setTimeout(render, 0); } });
381
+ box.value = drafts[key] || "";
382
+ var asQuestion = h("input", { type: "checkbox", id: "q" });
383
+ card.appendChild(box);
384
+ if (human.suggestions && human.suggestions.length) card.appendChild(h("div", { class: "choices" }, human.suggestions.map(function (c) {
385
+ return h("button", { class: "btn", text: c, onclick: function () { box.value = c; drafts[key] = c; box.focus(); } });
386
+ })));
387
+ card.appendChild(h("div", { class: "row" },
388
+ h("button", { class: "btn primary", text: "Send", onmousedown: function (e) { e.preventDefault(); }, onclick: function () { submit(box.value, human.discussion ? asQuestion.checked : false); } }),
389
+ human.discussion ? h("label", { class: "hint" }, asQuestion, " Ask a question instead of answering") : null));
390
+ }
391
+ card.appendChild(status);
392
+ card.appendChild(h("div", { class: "hint mono", text: "machine respond " + run.id + " " + human.requestId + " <response>" }));
393
+ return card;
394
+ }
395
+
396
+ function trailCard(run) {
397
+ var trail = run.trail;
398
+ var end = active(run) ? Date.now() : Date.parse(run.updatedAt);
399
+ var rows = trail.map(function (step, i) {
400
+ var next = i + 1 < trail.length ? Date.parse(trail[i + 1].at) : end;
401
+ var current = i === trail.length - 1 && active(run);
402
+ return h("div", { class: "step" + (current ? " now" : "") },
403
+ h("span", { class: "t", text: clock(step.at) }), h("span", { class: "s", text: step.state }),
404
+ h("span", { class: "d" }, current ? live(step.at) : duration(next - Date.parse(step.at))));
405
+ }).reverse();
406
+ return h("div", { class: "card block" }, h("h2", { text: "Path so far · " + trail.length + " step" + (trail.length > 1 ? "s" : "") }), h("div", { class: "trail" }, rows));
407
+ }
408
+
409
+ /* ---------- graph ---------- */
410
+
411
+ var NODE_W = 172, NODE_H = 46, GAP_X = 34, RANK_GAP = 84, PAD = 40, LANES = 130;
412
+
413
+ function layout(graph) {
414
+ var states = graph.states;
415
+ var byId = {};
416
+ states.forEach(function (st) { byId[st.id] = st; });
417
+ var out = {}, incoming = {};
418
+ states.forEach(function (st) { out[st.id] = []; incoming[st.id] = []; });
419
+ graph.edges.forEach(function (e) { if (byId[e.from] && byId[e.to]) { out[e.from].push(e); incoming[e.to].push(e); } });
420
+ var mark = {}, back = new Set(), order = [], discovered = [];
421
+ function dfs(id) {
422
+ mark[id] = 1; discovered.push(id);
423
+ out[id].forEach(function (e) {
424
+ if (e.to === e.from || mark[e.to] === 1) back.add(e);
425
+ else if (!mark[e.to]) dfs(e.to);
426
+ });
427
+ mark[id] = 2; order.push(id);
428
+ }
429
+ var reached = {};
430
+ if (graph.initial && byId[graph.initial]) dfs(graph.initial);
431
+ discovered.forEach(function (id) { reached[id] = true; });
432
+ states.forEach(function (st) { if (!mark[st.id]) dfs(st.id); });
433
+ var rank = {};
434
+ states.forEach(function (st) { rank[st.id] = 0; });
435
+ order.slice().reverse().forEach(function (id) {
436
+ out[id].forEach(function (e) { if (!back.has(e)) rank[e.to] = Math.max(rank[e.to], rank[id] + 1); });
437
+ });
438
+ // Move a state down to just above its nearest next state, so its edges stay short; the first state stays on top.
439
+ order.forEach(function (id) {
440
+ var below = out[id].filter(function (e) { return !back.has(e); }).map(function (e) { return rank[e.to] - 1; });
441
+ if (below.length && incoming[id].length) rank[id] = Math.max(rank[id], Math.min.apply(null, below));
442
+ });
443
+ var max = 0;
444
+ states.forEach(function (st) { max = Math.max(max, rank[st.id]); });
445
+ var rows = [];
446
+ for (var r = 0; r <= max; r++) rows.push([]);
447
+ discovered.forEach(function (id) { rows[rank[id]].push(id); });
448
+ var pos = {};
449
+ function index() { rows.forEach(function (row) { row.forEach(function (id, i) { pos[id] = i - (row.length - 1) / 2; }); }); }
450
+ index();
451
+ function sweep(down) {
452
+ var range = [];
453
+ for (var i = 0; i <= max; i++) range.push(i);
454
+ if (!down) range.reverse();
455
+ range.forEach(function (r) {
456
+ var row = rows[r];
457
+ var score = {};
458
+ row.forEach(function (id) {
459
+ var near = (down ? incoming[id].map(function (e) { return e.from; }) : out[id].map(function (e) { return e.to; }))
460
+ .filter(function (n) { return n !== id && (down ? rank[n] < r : rank[n] > r); });
461
+ score[id] = near.length ? near.reduce(function (sum, n) { return sum + pos[n]; }, 0) / near.length : pos[id];
462
+ });
463
+ row.sort(function (a, b) { return score[a] - score[b]; });
464
+ row.forEach(function (id, i) { pos[id] = i - (row.length - 1) / 2; });
465
+ });
466
+ }
467
+ for (var k = 0; k < 6; k++) sweep(k % 2 === 0);
468
+ var widest = Math.max.apply(null, rows.map(function (row) { return row.length; }));
469
+ var width = widest * NODE_W + (widest - 1) * GAP_X;
470
+ var nodes = {};
471
+ rows.forEach(function (row, r) {
472
+ row.forEach(function (id) {
473
+ nodes[id] = { id: id, x: LANES + PAD + width / 2 + pos[id] * (NODE_W + GAP_X) - NODE_W / 2, y: PAD + r * RANK_GAP, rank: r, state: byId[id], reached: !!reached[id] };
474
+ });
475
+ });
476
+ return { nodes: nodes, edges: graph.edges.filter(function (e) { return nodes[e.from] && nodes[e.to]; }), back: back,
477
+ width: width + PAD * 2 + LANES * 2, height: (max + 1) * RANK_GAP + PAD * 2 };
478
+ }
479
+
480
+ var KIND = { agent: "var(--agent)", human: "var(--human)", operation: "var(--operation)", final: "var(--final)", group: "var(--group)", state: "var(--group)" };
481
+
482
+ function graphCard(run) {
483
+ var legend = h("div", { class: "legend" }, ["agent", "operation", "human", "final"].map(function (k) {
484
+ return h("span", null, h("i", { style: "background:" + KIND[k] }), k);
485
+ }));
486
+ var tools = h("div", { class: "tools" },
487
+ h("button", { class: "btn", title: "Zoom out", text: "−", onclick: function () { zoomBy(run, 1 / 1.25); } }),
488
+ h("button", { class: "btn", title: "Zoom in", text: "+", onclick: function () { zoomBy(run, 1.25); } }),
489
+ h("button", { class: "btn", title: "Fit the whole Machine", text: "Fit", onclick: function () { fit(run, true); } }),
490
+ h("button", { class: "btn", title: "Show the current state", text: "Current", onclick: function () { fit(run, false); } }));
491
+ var box = h("div", { class: "graph" + (run.graph ? "" : " none"), id: "graph" });
492
+ if (!run.graph) box.appendChild(h("div", { class: "empty", text: "This run did not publish its Machine's graph. Runs started by a Machines release that draws graphs publish one." }));
493
+ return h("div", { class: "card block" }, h("div", { class: "graph-head" }, h("h2", { text: "Machine" }), legend, tools), box);
494
+ }
495
+
496
+ function layoutFor(run) {
497
+ var key = run.id + ":" + run.graph.states.length + ":" + run.graph.edges.length;
498
+ if (!layouts[key]) layouts[key] = layout(run.graph);
499
+ return layouts[key];
500
+ }
501
+
502
+ function trailFacts(run, lay) {
503
+ var counts = {}, pairs = {};
504
+ var trail = run.trail || [];
505
+ var prev = null;
506
+ trail.forEach(function (step) {
507
+ var ids = step.state.split(", ");
508
+ ids.forEach(function (id) { counts[id] = (counts[id] || 0) + 1; });
509
+ if (prev) prev.forEach(function (a) { ids.forEach(function (b) { pairs[a + "\u0000" + b] = (pairs[a + "\u0000" + b] || 0) + 1; }); });
510
+ prev = ids;
511
+ });
512
+ return { counts: counts, pairs: pairs };
513
+ }
514
+
515
+ function drawGraph(run) {
516
+ var box = document.getElementById("graph");
517
+ if (!box || !run.graph) return;
518
+ var lay = layoutFor(run);
519
+ var current = paths(run.state);
520
+ var facts = trailFacts(run, lay);
521
+ var svg = s("svg", { viewBox: "0 0 " + box.clientWidth + " " + box.clientHeight });
522
+ var defs = s("defs");
523
+ ["edge", "accent"].forEach(function (name) {
524
+ defs.appendChild(s("marker", { id: "arrow-" + name, viewBox: "0 0 10 10", refX: "9", refY: "5", markerWidth: "7", markerHeight: "7", orient: "auto-start-reverse" },
525
+ s("path", { d: "M 0 0 L 10 5 L 0 10 z", fill: name === "accent" ? "var(--accent)" : "var(--edge)" })));
526
+ });
527
+ svg.appendChild(defs);
528
+ var view = s("g", { class: "viewport" });
529
+ svg.appendChild(view);
530
+ var edgeLayer = s("g"), nodeLayer = s("g");
531
+ view.appendChild(edgeLayer); view.appendChild(nodeLayer);
532
+ var nexts = {};
533
+ if (active(run)) current.forEach(function (c) { nexts[c] = true; });
534
+ var rightCount = {}, leftCount = {};
535
+ lay.edges.forEach(function (e) {
536
+ var a = lay.nodes[e.from], b = lay.nodes[e.to];
537
+ var visited = facts.pairs[e.from + "\u0000" + e.to];
538
+ var next = nexts[e.from] && !visited;
539
+ var terminal = b.state.kind === "final" || /^(stuck|failure|failed|blocked|parked|park)$/i.test(b.state.id);
540
+ var d, lx, ly;
541
+ if (e.from === e.to) {
542
+ var x0 = a.x + NODE_W, y0 = a.y + NODE_H / 2;
543
+ d = "M " + x0 + " " + (y0 - 8) + " C " + (x0 + 34) + " " + (y0 - 30) + " " + (x0 + 34) + " " + (y0 + 30) + " " + x0 + " " + (y0 + 8);
544
+ lx = x0 + 30; ly = y0;
545
+ } else if (b.rank === a.rank + 1) {
546
+ var sx = a.x + NODE_W / 2, sy = a.y + NODE_H, tx = b.x + NODE_W / 2, ty = b.y;
547
+ var bend = Math.max(28, (ty - sy) * 0.45);
548
+ d = "M " + sx + " " + sy + " C " + sx + " " + (sy + bend) + " " + tx + " " + (ty - bend) + " " + tx + " " + ty;
549
+ lx = (sx + tx) / 2; ly = (sy + ty) / 2;
550
+ } else if (b.rank > a.rank) {
551
+ // A long way down goes round the left of the states between, so it never runs through one.
552
+ var lane2 = (leftCount[e.to] = (leftCount[e.to] || 0) + 1);
553
+ var px = a.x, py = a.y + NODE_H / 2, qx = b.x, qy = b.y + NODE_H / 2;
554
+ var west = Math.min(px, qx) - 26 - 12 * Math.min(lane2, 7) - 5 * Math.min(b.rank - a.rank, 8);
555
+ d = "M " + px + " " + py + " C " + west + " " + py + " " + west + " " + qy + " " + qx + " " + qy;
556
+ lx = west + 4; ly = py + Math.min(RANK_GAP * 0.6, (qy - py) / 2);
557
+ } else {
558
+ var lane = (rightCount[e.to] = (rightCount[e.to] || 0) + 1);
559
+ var ax = a.x + NODE_W, ay = a.y + NODE_H / 2, bx = b.x + NODE_W, by = b.y + NODE_H / 2;
560
+ var out = Math.max(ax, bx) + 26 + 12 * Math.min(lane, 6) + 6 * Math.min(Math.abs(a.rank - b.rank), 8);
561
+ d = "M " + ax + " " + ay + " C " + out + " " + ay + " " + out + " " + by + " " + bx + " " + by;
562
+ lx = out - 6; ly = (ay + by) / 2;
563
+ }
564
+ var hot = visited || next;
565
+ var g = s("g", { class: "edge" + (terminal ? " terminal" : "") + (visited ? " visited" : "") + (next ? " next" : "") },
566
+ s("title", { text: e.from + " → " + e.to + " on " + e.event + (visited ? " (taken " + visited + "×)" : "") }),
567
+ s("path", { d: d, "marker-end": "url(#arrow-" + (hot ? "accent" : "edge") + ")" }));
568
+ if (hot || lay.edges.length <= 24) g.appendChild(s("text", { x: lx, y: ly, "text-anchor": "middle", "dominant-baseline": "middle", text: e.event + (visited > 1 ? " ×" + visited : "") }));
569
+ (hot ? nodeLayer.parentNode : edgeLayer).insertBefore(g, hot ? nodeLayer : null);
570
+ });
571
+ Object.keys(lay.nodes).forEach(function (id) {
572
+ var n = lay.nodes[id];
573
+ var st = n.state;
574
+ var isCurrent = current.indexOf(id) >= 0;
575
+ var count = facts.counts[id] || 0;
576
+ var short = id.split(".").pop();
577
+ var name = short.length > 21 ? short.slice(0, 20) + "…" : short;
578
+ var kind = st.kind === "agent" ? "agent · " + st.role : st.kind;
579
+ var g = s("g", { class: "node" + (count ? " visited" : "") + (isCurrent ? " current " + run.status : "") + (n.reached ? "" : " unreached"), transform: "translate(" + n.x + "," + n.y + ")" },
580
+ s("title", { text: id + " (" + kind + ")" + (count ? ", entered " + count + "×" : "") }));
581
+ if (isCurrent && active(run)) g.appendChild(s("rect", { class: "halo", x: -5, y: -5, width: NODE_W + 10, height: NODE_H + 10, rx: 14 }));
582
+ g.appendChild(s("rect", { class: "base", width: NODE_W, height: NODE_H, rx: 10 }));
583
+ g.appendChild(s("rect", { class: "box", width: NODE_W, height: NODE_H, rx: 10, "stroke-dasharray": st.kind === "group" ? "4 3" : null }));
584
+ g.appendChild(s("rect", { x: 0, y: 8, width: 4, height: NODE_H - 16, rx: 2, fill: KIND[st.kind] || KIND.state }));
585
+ g.appendChild(s("text", { x: 14, y: 19, text: name }));
586
+ g.appendChild(s("text", { class: "kind", x: 14, y: 35, text: kind.length > 26 ? kind.slice(0, 25) + "…" : kind }));
587
+ if (count > 1) {
588
+ g.appendChild(s("rect", { class: "badge", x: NODE_W - 30, y: -8, width: 30, height: 17, rx: 8.5 }));
589
+ g.appendChild(s("text", { class: "count", x: NODE_W - 15, y: 4, "text-anchor": "middle", text: "×" + count }));
590
+ }
591
+ nodeLayer.appendChild(g);
592
+ });
593
+ box.replaceChildren(svg);
594
+ // The whole Machine when it can be read at that size; otherwise the part around where the run is.
595
+ if (!views[run.id]) fit(run, !active(run) || Math.min(box.clientWidth / lay.width, box.clientHeight / lay.height) >= 0.6); else apply(run);
596
+ bindPanZoom(box, run);
597
+ }
598
+
599
+ function apply(run) {
600
+ var v = views[run.id];
601
+ var g = document.querySelector("#graph .viewport");
602
+ if (g && v) g.setAttribute("transform", "translate(" + v.x + "," + v.y + ") scale(" + v.k + ")");
603
+ }
604
+
605
+ function fit(run, whole) {
606
+ var box = document.getElementById("graph");
607
+ if (!box || !run.graph) return;
608
+ var lay = layoutFor(run);
609
+ var w = box.clientWidth, hgt = box.clientHeight;
610
+ var k = Math.min(1, w / lay.width, whole ? hgt / lay.height : 1);
611
+ k = Math.max(k, whole ? 0.15 : 0.45);
612
+ var v = { k: k, x: (w - lay.width * k) / 2, y: (hgt - lay.height * k) / 2 };
613
+ var current = paths(run.state).map(function (id) { return lay.nodes[id]; }).filter(Boolean)[0];
614
+ if (!whole || lay.height * k > hgt) {
615
+ if (current) v.y = hgt / 2 - (current.y + NODE_H / 2) * k;
616
+ else v.y = 10;
617
+ if (lay.width * k > w && current) v.x = w / 2 - (current.x + NODE_W / 2) * k;
618
+ }
619
+ views[run.id] = v;
620
+ apply(run);
621
+ }
622
+
623
+ function zoomBy(run, factor, cx, cy) {
624
+ var box = document.getElementById("graph");
625
+ var v = views[run.id];
626
+ if (!box || !v) return;
627
+ if (cx === undefined) { cx = box.clientWidth / 2; cy = box.clientHeight / 2; }
628
+ var k = Math.min(2.5, Math.max(0.1, v.k * factor));
629
+ v.x = cx - (cx - v.x) * (k / v.k);
630
+ v.y = cy - (cy - v.y) * (k / v.k);
631
+ v.k = k;
632
+ apply(run);
633
+ }
634
+
635
+ function bindPanZoom(box, run) {
636
+ if (box.dataset.bound === run.id) return;
637
+ box.dataset.bound = run.id;
638
+ var start = null;
639
+ box.onwheel = function (e) {
640
+ e.preventDefault();
641
+ var rect = box.getBoundingClientRect();
642
+ if (e.ctrlKey || e.metaKey) zoomBy(currentRun(), Math.exp(-e.deltaY * 0.01), e.clientX - rect.left, e.clientY - rect.top);
643
+ else { var v = views[currentRun().id]; v.x -= e.deltaX; v.y -= e.deltaY; apply(currentRun()); }
644
+ };
645
+ box.onpointerdown = function (e) { start = { x: e.clientX, y: e.clientY, v: Object.assign({}, views[currentRun().id]) }; box.setPointerCapture(e.pointerId); box.classList.add("dragging"); };
646
+ box.onpointermove = function (e) {
647
+ if (!start) return;
648
+ var v = views[currentRun().id];
649
+ v.x = start.v.x + e.clientX - start.x; v.y = start.v.y + e.clientY - start.y;
650
+ apply(currentRun());
651
+ };
652
+ box.onpointerup = box.onpointercancel = function () { start = null; box.classList.remove("dragging"); };
653
+ }
654
+ function currentRun() { return data.runs.find(function (r) { return r.id === selected; }); }
655
+
656
+ /* ---------- panels ---------- */
657
+
658
+ function renderPanels(panels) {
659
+ var el = document.getElementById("panels");
660
+ if (!panels.length) {
661
+ el.replaceChildren(h("div", { class: "card panel" }, h("h3", { text: "Project panels" }),
662
+ h("div", { class: "summary" }, "A project adds its own panels here, such as tickets or pull requests, by naming commands that print JSON in .machines/ui.json.")));
663
+ return;
664
+ }
665
+ el.replaceChildren.apply(el, panels.map(function (p) {
666
+ return h("div", { class: "card panel" },
667
+ h("h3", null, p.title, p.checkedAt ? h("span", { text: ago(p.checkedAt) }) : null),
668
+ p.summary ? h("div", { class: "summary", text: p.summary }) : null,
669
+ p.error ? h("div", { class: "error", text: p.error }) : null,
670
+ p.items.length ? h("div", { class: "items" }, p.items.map(function (it) {
671
+ return h("div", { class: "item" }, h("span", { class: "dot " + (it.status || "idle") }),
672
+ h("span", { class: "text" }, it.href ? h("a", { href: it.href, target: "_blank", rel: "noopener noreferrer", text: it.text }) : it.text),
673
+ h("span", { class: "at", text: it.at ? ago(it.at) : "" }),
674
+ it.detail ? h("span", { class: "detail", text: it.detail }) : null);
675
+ })) : (!p.error ? h("div", { class: "summary", text: p.checkedAt ? "Nothing to show." : "Loading…" }) : null));
676
+ }));
677
+ }
678
+
679
+ setInterval(function () {
680
+ document.querySelectorAll("[data-since]").forEach(function (el) {
681
+ el.textContent = duration(Date.now() - Date.parse(el.getAttribute("data-since"))) + (el.getAttribute("data-suffix") || "");
682
+ });
683
+ }, 1000);
684
+ window.addEventListener("resize", function () { if (data && selected) { var r = currentRun(); if (r) drawGraph(r); } });
685
+ window.addEventListener("hashchange", function () { var id = decodeURIComponent(location.hash.slice(1)); if (id && id !== selected) select(id); });
686
+ connect();
687
+ </script>
688
+ </body>
689
+ </html>
690
+ `;