aggregate_api 1.0.0__py3-none-any.whl

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.
Files changed (63) hide show
  1. aggregate_api/__init__.py +41 -0
  2. aggregate_api/__main__.py +154 -0
  3. aggregate_api/app.py +206 -0
  4. aggregate_api/audit.py +395 -0
  5. aggregate_api/bounds.py +331 -0
  6. aggregate_api/cache.py +319 -0
  7. aggregate_api/capability.py +823 -0
  8. aggregate_api/completion.py +219 -0
  9. aggregate_api/config.py +363 -0
  10. aggregate_api/cors.py +61 -0
  11. aggregate_api/examples.py +620 -0
  12. aggregate_api/layer_pricing.py +840 -0
  13. aggregate_api/library.py +94 -0
  14. aggregate_api/library_notes.py +96 -0
  15. aggregate_api/models.py +1407 -0
  16. aggregate_api/net.py +281 -0
  17. aggregate_api/pnl.py +101 -0
  18. aggregate_api/pricing.py +778 -0
  19. aggregate_api/resources.py +257 -0
  20. aggregate_api/routes/__init__.py +8 -0
  21. aggregate_api/routes/decl.py +327 -0
  22. aggregate_api/routes/examples.py +82 -0
  23. aggregate_api/routes/meta.py +282 -0
  24. aggregate_api/routes/objects.py +4119 -0
  25. aggregate_api/routes/status.py +466 -0
  26. aggregate_api/serializers.py +565 -0
  27. aggregate_api/sessions.py +353 -0
  28. aggregate_api/static/aggregate-api-logo-512.png +0 -0
  29. aggregate_api/static/aggregate-api-logo.png +0 -0
  30. aggregate_api/static/aggregate-api-trim.png +0 -0
  31. aggregate_api/static/android-chrome-192x192.png +0 -0
  32. aggregate_api/static/android-chrome-512x512.png +0 -0
  33. aggregate_api/static/apple-touch-icon.png +0 -0
  34. aggregate_api/static/assets/bootstrap-icons-BeopsB42.woff +0 -0
  35. aggregate_api/static/assets/bootstrap-icons-mSm7cUeB.woff2 +0 -0
  36. aggregate_api/static/assets/bootstrap-ohb1VZ53.js +5 -0
  37. aggregate_api/static/assets/codemirror-h62DHGGa.js +14 -0
  38. aggregate_api/static/assets/csv-grid.worker-DKzHGXac.js +4 -0
  39. aggregate_api/static/assets/echarts-B7o9sc00.js +40 -0
  40. aggregate_api/static/assets/echarts-gl-DG1Uf6wE.js +4282 -0
  41. aggregate_api/static/assets/lite-CUlcD8p4.css +1 -0
  42. aggregate_api/static/assets/lite-Dd2TnT4M.js +1 -0
  43. aggregate_api/static/assets/main-Bxhxa55v.css +9 -0
  44. aggregate_api/static/assets/main-CmoEiPit.js +9 -0
  45. aggregate_api/static/assets/tables-BHCF7qIF.js +8 -0
  46. aggregate_api/static/assets/tables-CxvajLr7.css +1 -0
  47. aggregate_api/static/favicon-16x16.png +0 -0
  48. aggregate_api/static/favicon-32x32.png +0 -0
  49. aggregate_api/static/favicon.ico +0 -0
  50. aggregate_api/static/index.html +912 -0
  51. aggregate_api/static/lite.html +83 -0
  52. aggregate_api/static/logo.png +0 -0
  53. aggregate_api/static/site.webmanifest +14 -0
  54. aggregate_api/static/sw.js +78 -0
  55. aggregate_api/status.py +536 -0
  56. aggregate_api/status_page.html +546 -0
  57. aggregate_api/tables.py +316 -0
  58. aggregate_api-1.0.0.dist-info/METADATA +187 -0
  59. aggregate_api-1.0.0.dist-info/RECORD +63 -0
  60. aggregate_api-1.0.0.dist-info/WHEEL +5 -0
  61. aggregate_api-1.0.0.dist-info/entry_points.txt +2 -0
  62. aggregate_api-1.0.0.dist-info/licenses/LICENSE +28 -0
  63. aggregate_api-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,546 @@
1
+ <!DOCTYPE html>
2
+ <meta charset="utf-8">
3
+ <meta name="viewport" content="width=device-width, initial-scale=1">
4
+ <meta name="robots" content="noindex, nofollow">
5
+ <title>aggregate api status</title>
6
+ <!--
7
+ The operator's page. One self-contained file: inline CSS, inline JS, no
8
+ imports, no build step, no network beyond the one fetch of /v1/status.
9
+
10
+ It does not go through Vite and does not live in static/, deliberately. The
11
+ web build wipes static/ on every run, so a copy there would be deleted by the
12
+ next deploy, and a status page whose delivery depends on the pipeline it
13
+ reports on cannot report on that pipeline failing. Served instead by
14
+ routes/status.py, straight out of the package.
15
+
16
+ An instrument, not a dashboard: tables rather than charts, monospace numbers,
17
+ and every count labeled with the window or the process start it is measured
18
+ against. See dev/done/plan-site-status-page.md.
19
+ -->
20
+ <style>
21
+ :root {
22
+ --ink: #16191d;
23
+ --dim: #5c6570;
24
+ --faint: #8b949e;
25
+ --rule: #dfe3e8;
26
+ --panel: #ffffff;
27
+ --ground: #f5f6f8;
28
+ --accent: #0d6efd;
29
+ --warn: #b54708;
30
+ --bad: #b42318;
31
+ --good: #027a48;
32
+ }
33
+ @media (prefers-color-scheme: dark) {
34
+ :root {
35
+ --ink: #e6e9ee; --dim: #9aa4b0; --faint: #6b747f; --rule: #2b3138;
36
+ --panel: #171b20; --ground: #0e1114; --accent: #6ea8fe;
37
+ --warn: #f6a561; --bad: #f27a72; --good: #6dd3a1;
38
+ }
39
+ }
40
+ * { box-sizing: border-box; }
41
+ body {
42
+ margin: 0;
43
+ background: var(--ground);
44
+ color: var(--ink);
45
+ font: 13px/1.45 ui-sans-serif, system-ui, "Segoe UI", Roboto, sans-serif;
46
+ }
47
+ code, .num, td.num, .mono {
48
+ font-family: ui-monospace, "Cascadia Mono", Consolas, "Liberation Mono", monospace;
49
+ font-variant-numeric: tabular-nums;
50
+ }
51
+ header {
52
+ position: sticky; top: 0; z-index: 5;
53
+ display: flex; flex-wrap: wrap; gap: 8px 18px; align-items: baseline;
54
+ padding: 10px 16px;
55
+ background: var(--panel);
56
+ border-bottom: 1px solid var(--rule);
57
+ }
58
+ header h1 { margin: 0; font-size: 14px; font-weight: 600; letter-spacing: .01em; }
59
+ header .meta { color: var(--dim); font-size: 12px; }
60
+ header .spacer { flex: 1 1 auto; }
61
+ button {
62
+ font: inherit; color: var(--ink); background: var(--ground);
63
+ border: 1px solid var(--rule); border-radius: 4px;
64
+ padding: 3px 10px; cursor: pointer;
65
+ }
66
+ button:hover { border-color: var(--accent); color: var(--accent); }
67
+ main { padding: 14px 16px 60px; display: grid; gap: 14px; }
68
+ section {
69
+ background: var(--panel); border: 1px solid var(--rule); border-radius: 6px;
70
+ padding: 10px 12px 12px;
71
+ }
72
+ section > h2 {
73
+ margin: 0 0 8px; font-size: 12px; font-weight: 600;
74
+ text-transform: uppercase; letter-spacing: .06em; color: var(--dim);
75
+ }
76
+ section > h2 .note { text-transform: none; letter-spacing: 0; font-weight: 400; }
77
+ .kv { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 2px 18px; }
78
+ .kv div { display: flex; justify-content: space-between; gap: 10px; padding: 1px 0; border-bottom: 1px dotted var(--rule); }
79
+ .kv span:first-child { color: var(--dim); }
80
+ .kv span:last-child {
81
+ font-family: ui-monospace, Consolas, monospace; font-variant-numeric: tabular-nums;
82
+ text-align: right; overflow-wrap: anywhere;
83
+ }
84
+ table { border-collapse: collapse; width: 100%; margin-top: 6px; }
85
+ th, td { text-align: left; padding: 3px 8px 3px 0; border-bottom: 1px solid var(--rule); vertical-align: top; }
86
+ th { color: var(--dim); font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; }
87
+ td.num, th.num { text-align: right; font-family: ui-monospace, Consolas, monospace; font-variant-numeric: tabular-nums; }
88
+ tbody tr:hover { background: var(--ground); }
89
+ .prog {
90
+ font-family: ui-monospace, Consolas, monospace; font-size: 12px;
91
+ white-space: pre-wrap; overflow-wrap: anywhere; cursor: pointer;
92
+ max-height: 1.5em; overflow: hidden; color: var(--ink);
93
+ }
94
+ .prog.open { max-height: none; }
95
+ .prog::after { content: " \25BE"; color: var(--faint); }
96
+ .prog.open::after { content: " \25B4"; }
97
+ .empty { color: var(--faint); font-style: italic; }
98
+ .warn { color: var(--warn); }
99
+ .bad { color: var(--bad); }
100
+ .good { color: var(--good); }
101
+ .pill { border: 1px solid currentColor; border-radius: 10px; padding: 0 6px; font-size: 11px; }
102
+ #error { color: var(--bad); padding: 10px 16px; }
103
+ .cols2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 14px; }
104
+ </style>
105
+
106
+ <header>
107
+ <h1>aggregate api status</h1>
108
+ <span class="meta" id="ident">loading</span>
109
+ <span class="spacer"></span>
110
+ <span class="meta" id="cost"></span>
111
+ <span class="meta" id="tick"></span>
112
+ <button id="pause">pause</button>
113
+ <button id="now">refresh</button>
114
+ </header>
115
+ <div id="error" hidden></div>
116
+ <main id="main"></main>
117
+
118
+ <script>
119
+ 'use strict';
120
+
121
+ // Substituted by routes/status.py from AGGAPI_STATUS_REFRESH_S. Inlined rather
122
+ // than fetched so the page's first render costs one round trip, not two.
123
+ const REFRESH_MS = Number('__REFRESH_MS__') || 10000;
124
+
125
+ // '..' from /v1/status/page resolves to /v1/status. Derived from the current
126
+ // location rather than hardcoded so the page works unchanged behind a prefix.
127
+ const ENDPOINT = new URL('../status', location.href).pathname;
128
+
129
+ let timer = null;
130
+ let paused = false;
131
+ let nextAt = 0;
132
+
133
+ // ---------------------------------------------------------------- formatting
134
+ const NBSP = ' ';
135
+
136
+ function num(v, digits) {
137
+ if (v === null || v === undefined) return '–';
138
+ if (typeof v !== 'number') return String(v);
139
+ return v.toLocaleString(undefined, {
140
+ minimumFractionDigits: digits || 0, maximumFractionDigits: digits || 0,
141
+ });
142
+ }
143
+
144
+ function bytes(v) {
145
+ if (v === null || v === undefined) return '–';
146
+ const units = ['B', 'kB', 'MB', 'GB', 'TB'];
147
+ let n = v, i = 0;
148
+ while (n >= 1024 && i < units.length - 1) { n /= 1024; i += 1; }
149
+ return `${n.toFixed(n < 10 && i > 0 ? 1 : 0)}${NBSP}${units[i]}`;
150
+ }
151
+
152
+ function secs(v) {
153
+ if (v === null || v === undefined) return '–';
154
+ if (v < 90) return `${v.toFixed(0)}${NBSP}s`;
155
+ if (v < 5400) return `${(v / 60).toFixed(1)}${NBSP}min`;
156
+ if (v < 172800) return `${(v / 3600).toFixed(1)}${NBSP}h`;
157
+ return `${(v / 86400).toFixed(1)}${NBSP}d`;
158
+ }
159
+
160
+ function rate(v) {
161
+ if (v === null || v === undefined) return '–';
162
+ return `${(v * 100).toFixed(1)}%`;
163
+ }
164
+
165
+ function ms(v) {
166
+ if (v === null || v === undefined) return '–';
167
+ return `${num(v, v < 10 ? 2 : 0)}${NBSP}ms`;
168
+ }
169
+
170
+ /**
171
+ * A session id, as it arrived.
172
+ *
173
+ * Already shortened: `status.short_session_id` cuts it server side, before the
174
+ * payload is built. The id is a namespace and not a security boundary, so
175
+ * anyone holding one gets that session's objects, and truncating here instead
176
+ * would leave the JSON carrying full ids for anyone who saved it. This is a
177
+ * label rather than a formatter, kept so the intent is visible at the call
178
+ * sites that print one.
179
+ */
180
+ function sess(id) {
181
+ return id || '–';
182
+ }
183
+
184
+ /**
185
+ * A timestamp, cut to what an operator is reading it for.
186
+ *
187
+ * The payload carries full ISO 8601 in UTC, which is right for the contract and
188
+ * far too wide for a table column: five of them wrap and the row doubles in
189
+ * height. Today's stamps show the time alone, older ones gain the date, and the
190
+ * full value stays on the element's title so it is one hover away.
191
+ */
192
+ function when(iso) {
193
+ if (!iso) return '–';
194
+ const at = new Date(iso);
195
+ if (Number.isNaN(at.getTime())) return iso;
196
+ const pad = (n) => String(n).padStart(2, '0');
197
+ const clock = `${pad(at.getHours())}:${pad(at.getMinutes())}:${pad(at.getSeconds())}`;
198
+ const text = at.toDateString() === new Date().toDateString()
199
+ ? clock
200
+ : `${pad(at.getMonth() + 1)}-${pad(at.getDate())} ${clock.slice(0, 5)}`;
201
+ return el('span', { title: iso, text: text });
202
+ }
203
+
204
+ function el(tag, attrs, kids) {
205
+ const node = document.createElement(tag);
206
+ for (const [k, v] of Object.entries(attrs || {})) {
207
+ if (v === null || v === undefined || v === false) continue;
208
+ if (k === 'class') node.className = v;
209
+ else if (k === 'text') node.textContent = v;
210
+ else node.setAttribute(k, v === true ? '' : String(v));
211
+ }
212
+ for (const kid of kids || []) {
213
+ if (kid === null || kid === undefined) continue;
214
+ node.append(typeof kid === 'string' ? document.createTextNode(kid) : kid);
215
+ }
216
+ return node;
217
+ }
218
+
219
+ function section(title, note, kids) {
220
+ const head = el('h2', { text: title });
221
+ if (note) head.append(' ', el('span', { class: 'note', text: note }));
222
+ return el('section', {}, [head, ...kids]);
223
+ }
224
+
225
+ function kv(pairs) {
226
+ return el('div', { class: 'kv' }, pairs
227
+ .filter((p) => p)
228
+ .map(([k, v, cls]) => el('div', {}, [
229
+ el('span', { text: k }),
230
+ el('span', { class: cls || null, text: v === null || v === undefined ? '–' : String(v) }),
231
+ ])));
232
+ }
233
+
234
+ function table(columns, rows, render) {
235
+ if (!rows || !rows.length) return el('p', { class: 'empty', text: 'nothing to show' });
236
+ const head = el('tr', {}, columns.map((c) => el('th', {
237
+ class: c.num ? 'num' : null, text: c.label,
238
+ })));
239
+ const body = rows.map((row) => el('tr', {}, render(row).map((cell, i) => {
240
+ const td = el('td', { class: columns[i].num ? 'num' : null });
241
+ td.append(typeof cell === 'string' || typeof cell === 'number'
242
+ ? document.createTextNode(String(cell)) : cell);
243
+ return td;
244
+ })));
245
+ return el('table', {}, [el('thead', {}, [head]), el('tbody', {}, body)]);
246
+ }
247
+
248
+ /** A program cell: one line, click to expand. Truncation is marked, not hidden. */
249
+ function program(text, truncated) {
250
+ const node = el('div', {
251
+ class: 'prog',
252
+ title: 'click to expand',
253
+ text: truncated ? `${text}… [truncated]` : text,
254
+ });
255
+ node.addEventListener('click', () => node.classList.toggle('open'));
256
+ return node;
257
+ }
258
+
259
+ // ------------------------------------------------------------------ sections
260
+ function identity(d) {
261
+ const i = d.identity;
262
+ return section('identity', `pid ${i.pid}`, [kv([
263
+ ['aggregate_api', i.version],
264
+ ['aggregate', i.aggregate_version],
265
+ ['greater-tables', i.tables_version],
266
+ ['python', i.python],
267
+ ['platform', i.platform],
268
+ ['bind', `${i.host}:${i.port}`],
269
+ ['started', i.started_at],
270
+ ['uptime', secs(i.uptime_s)],
271
+ ['library', i.library],
272
+ ['library recipes', num(i.library_recipes)],
273
+ ['library load', ms(i.library_load_ms)],
274
+ ['generated in', ms(d.generated_in_ms)],
275
+ ])]);
276
+ }
277
+
278
+ function settings(d) {
279
+ return section('settings', 'live, from AGGAPI_* env vars', [kv(
280
+ Object.entries(d.settings).map(([k, v]) => [
281
+ k, Array.isArray(v) ? (v.join(', ') || 'none') : String(v),
282
+ ]))]);
283
+ }
284
+
285
+ function sessions(d) {
286
+ const s = d.sessions;
287
+ return section('sessions', 'forks of the process recipe base', [
288
+ kv([
289
+ ['live', `${num(s.live)} / ${num(s.max)}`],
290
+ ['high water', num(s.high_water)],
291
+ ['ttl', secs(s.ttl_s)],
292
+ ['taken since start', num(s.taken)],
293
+ ['dropped', num(s.dropped)],
294
+ ['expired (ttl)', num(s.expired)],
295
+ ['evicted (capacity)', num(s.evicted)],
296
+ ]),
297
+ table([
298
+ { label: 'session' }, { label: 'created' },
299
+ { label: 'age', num: true }, { label: 'idle', num: true },
300
+ { label: 'requests', num: true }, { label: 'builds', num: true },
301
+ { label: 'recipes', num: true },
302
+ ], s.rows, (r) => [
303
+ sess(r.session_id), when(r.created_at),
304
+ secs(r.age_s), secs(r.idle_s),
305
+ num(r.requests), num(r.builds), num(r.recipes),
306
+ ]),
307
+ ]);
308
+ }
309
+
310
+ function cache(d) {
311
+ const c = d.cache;
312
+ return section('object cache', 'rows are LRU first, so the next eviction reads first', [
313
+ kv([
314
+ ['entries', `${num(c.entries)} / ${num(c.max)}`],
315
+ ['hits', num(c.hits)],
316
+ ['misses', num(c.misses)],
317
+ ['hit rate', rate(c.hit_rate)],
318
+ ['puts', num(c.puts)],
319
+ ['evictions', num(c.evictions)],
320
+ ['deletes', num(c.deletes)],
321
+ ]),
322
+ table([
323
+ { label: 'id' }, { label: 'kind' }, { label: 'name' },
324
+ { label: 'log2', num: true }, { label: 'bs', num: true },
325
+ { label: 'created' }, { label: 'notes', num: true },
326
+ { label: 'est. size', num: true }, { label: 'program' },
327
+ ], c.rows, (r) => [
328
+ r.id, r.kind, r.name, num(r.log2), r.bs || '–',
329
+ when(r.created_at), num(r.notes), bytes(r.estimated_bytes),
330
+ program(r.program, r.truncated),
331
+ ]),
332
+ ]);
333
+ }
334
+
335
+ function revalidationCache(title, c) {
336
+ return section(title, 'sits above the object cache and can never cause a build', [kv([
337
+ ['entries', `${num(c.entries)} / ${num(c.max)}`],
338
+ ['hits', num(c.hits)],
339
+ ['misses', num(c.misses)],
340
+ ['hit rate', rate(c.hit_rate)],
341
+ ['evictions', num(c.evictions)],
342
+ ])]);
343
+ }
344
+
345
+ function chartCache(d) {
346
+ return revalidationCache('chart document cache', d.chart_cache);
347
+ }
348
+
349
+ // Same machinery, its own counters. What it saves is larger per hit than the
350
+ // chart cache's: an exhibit rebuild to answer 304 measured 210 ms on a three
351
+ // unit portfolio's tail, against 7 ms for a chart served from cache.
352
+ function exhibitCache(d) {
353
+ return revalidationCache('exhibit document cache', d.exhibit_cache);
354
+ }
355
+
356
+ function buildWindow(label, w) {
357
+ const statuses = Object.entries(w.by_status);
358
+ const scopes = Object.entries(w.by_key_scope);
359
+ return el('section', {}, [
360
+ el('h2', { text: `builds, last ${label}` }),
361
+ kv([
362
+ ['total', num(w.total)],
363
+ ['p50', ms(w.p50_ms)],
364
+ ['p95', ms(w.p95_ms)],
365
+ ['distinct clients', num(w.distinct_clients)],
366
+ ...statuses.map(([k, v]) => [
367
+ `status ${k}`, num(v), k === 'ok' ? 'good' : 'warn',
368
+ ]),
369
+ ...scopes.map(([k, v]) => [`key ${k}`, num(v)]),
370
+ ]),
371
+ el('h2', { text: 'slowest' }),
372
+ table([
373
+ { label: 'when' }, { label: 'elapsed', num: true },
374
+ { label: 'status' }, { label: 'kind' }, { label: 'session' },
375
+ ], w.slowest, (r) => [
376
+ when(r.ts), ms(r.elapsed_ms), r.status, r.kind || '–',
377
+ sess(r.session_id),
378
+ ]),
379
+ el('h2', { text: 'most common errors' }),
380
+ table([{ label: 'message' }, { label: 'count', num: true }],
381
+ w.top_errors, (r) => [r.error_msg, num(r.count)]),
382
+ el('h2', { text: 'busiest clients' }),
383
+ table([{ label: 'address' }, { label: 'builds', num: true }],
384
+ w.top_clients, (r) => [r.ip, num(r.count)]),
385
+ ]);
386
+ }
387
+
388
+ function builds(d) {
389
+ const live = d.builds.live;
390
+ return el('div', { class: 'cols2' }, [
391
+ section('build pipeline', 'live, one slot', [kv([
392
+ ['in flight', num(live.in_flight), live.in_flight ? 'warn' : null],
393
+ ['waiting', num(live.waiting), live.waiting ? 'warn' : null],
394
+ ['started since boot', num(live.started)],
395
+ ['timeouts since boot', num(live.timeouts), live.timeouts ? 'bad' : null],
396
+ ])]),
397
+ buildWindow('hour', d.builds.hour),
398
+ buildWindow('day', d.builds.day),
399
+ ]);
400
+ }
401
+
402
+ function keyScope(d) {
403
+ const k = d.key_scope;
404
+ const bad = k.reasons.preview_unavailable > 0;
405
+ return section('key scope', 'shared against session qualified, since boot', [
406
+ kv([
407
+ ['shared', num(k.shared)],
408
+ ['session qualified', num(k.session)],
409
+ ['session rate', rate(k.session_rate)],
410
+ ['reason: session reference', num(k.reasons.session_reference)],
411
+ ['reason: preview unavailable', num(k.reasons.preview_unavailable),
412
+ bad ? 'warn' : null],
413
+ ['preview calls', num(k.preview_calls)],
414
+ ['preview mean', ms(k.preview_mean_ms)],
415
+ ['preview p95', ms(k.preview_p95_ms)],
416
+ ['refused by preview, built anyway', num(k.unpreviewable_builds),
417
+ k.unpreviewable_builds ? 'bad' : 'good'],
418
+ ]),
419
+ el('h2', {
420
+ text: 'programs the previewer refused and the builder accepted',
421
+ }),
422
+ el('p', {
423
+ class: 'empty',
424
+ text: 'each of these took a private cache slot it did not need. '
425
+ + 'They are an upstream ask against Underwriter.preview, which '
426
+ + 'is why they are kept verbatim.',
427
+ }),
428
+ table([
429
+ { label: 'when' }, { label: 'session' }, { label: 'kind' },
430
+ { label: 'elapsed', num: true }, { label: 'program' },
431
+ ], k.recent_unpreviewable, (r) => [
432
+ when(r.at), sess(r.session_id), r.kind || '–',
433
+ ms(r.elapsed_ms), program(r.program, r.truncated),
434
+ ]),
435
+ ]);
436
+ }
437
+
438
+ function resources(d) {
439
+ const r = d.resources;
440
+ const missing = Object.entries(r.unavailable || {});
441
+ return section('process resources', `source: ${r.source}, ${r.platform}`, [
442
+ kv([
443
+ ['rss', bytes(r.rss_bytes)],
444
+ ['vms', bytes(r.vms_bytes)],
445
+ ['cpu', r.cpu_percent === null ? '–' : `${num(r.cpu_percent, 1)}%`],
446
+ [`threads${r.threads_kind ? ` (${r.threads_kind})` : ''}`, num(r.threads)],
447
+ [`open ${r.open_files_kind || 'files'}`, num(r.open_files)],
448
+ ['load average', r.load_average ? r.load_average.join(' ') : null],
449
+ ['disk free', bytes(r.disk_free_bytes)],
450
+ ['disk total', bytes(r.disk_total_bytes)],
451
+ ]),
452
+ missing.length ? el('h2', { text: 'unavailable' }) : null,
453
+ missing.length ? kv(missing.map(([k, v]) => [k, v, 'warn'])) : null,
454
+ ]);
455
+ }
456
+
457
+ function watch(d) {
458
+ const w = d.watch;
459
+ const reg = w.registries;
460
+ const st = w.static;
461
+ return section('worth an eye', null, [
462
+ kv([
463
+ ['cached objects carrying library notes', num(w.notes.entries_with_notes)],
464
+ ['distinct notes', num(w.notes.distinct)],
465
+ reg.unavailable ? ['registries', reg.unavailable, 'warn'] : null,
466
+ reg.unavailable ? null : ['exhibits registered',
467
+ `${num(reg.exhibits)} (charter ${num(reg.charter_exhibits)})`,
468
+ reg.exhibits === reg.charter_exhibits ? null : 'warn'],
469
+ reg.unavailable ? null : ['charts registered',
470
+ `${num(reg.charts)} (charter ${num(reg.charter_charts)})`,
471
+ reg.charts === reg.charter_charts ? null : 'warn'],
472
+ ['spa bundle', st.mounted ? st.directory : (st.reason || 'not mounted'),
473
+ st.mounted ? null : 'warn'],
474
+ st.mounted ? ['index.html built', st.index_built_at.replace('T', ' ')] : null,
475
+ st.mounted ? ['bundle age', secs(st.index_age_s)] : null,
476
+ ['audit db on disk', bytes(w.audit_db_bytes)],
477
+ ['config set but unused', (w.unused_settings || []).join(', ') || 'none'],
478
+ ]),
479
+ w.notes.most_common.length ? el('h2', { text: 'most common library notes' }) : null,
480
+ w.notes.most_common.length
481
+ ? table([{ label: 'note' }, { label: 'objects', num: true }],
482
+ w.notes.most_common, (r) => [r.note, num(r.count)])
483
+ : null,
484
+ ]);
485
+ }
486
+
487
+ function gate(d) {
488
+ const g = d.gate;
489
+ return section('access', `admitted on ${g.admitted_on}`, [
490
+ kv([['refusals since boot', num(g.refusals), g.refusals ? 'warn' : 'good']]),
491
+ table([{ label: 'when' }, { label: 'address' }, { label: 'path' }],
492
+ g.recent, (r) => [when(r.at), r.address, r.path]),
493
+ ]);
494
+ }
495
+
496
+ // -------------------------------------------------------------------- render
497
+ function render(d) {
498
+ const i = d.identity;
499
+ document.getElementById('ident').textContent =
500
+ `${i.version} on aggregate ${i.aggregate_version}, up ${secs(i.uptime_s)}, `
501
+ + `generated ${d.generated_at}`;
502
+ document.getElementById('cost').textContent = `payload ${ms(d.generated_in_ms)}`;
503
+ const main = document.getElementById('main');
504
+ main.replaceChildren(
505
+ identity(d), sessions(d), cache(d), keyScope(d),
506
+ builds(d), chartCache(d), exhibitCache(d), resources(d), watch(d), gate(d), settings(d),
507
+ );
508
+ }
509
+
510
+ async function poll() {
511
+ const box = document.getElementById('error');
512
+ try {
513
+ const res = await fetch(ENDPOINT, { cache: 'no-store' });
514
+ if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
515
+ render(await res.json());
516
+ box.hidden = true;
517
+ } catch (err) {
518
+ box.hidden = false;
519
+ box.textContent = `could not read ${ENDPOINT}: ${err.message}`;
520
+ }
521
+ nextAt = Date.now() + REFRESH_MS;
522
+ }
523
+
524
+ function schedule() {
525
+ clearInterval(timer);
526
+ timer = setInterval(() => { if (!paused) poll(); }, REFRESH_MS);
527
+ nextAt = Date.now() + REFRESH_MS;
528
+ }
529
+
530
+ document.getElementById('pause').addEventListener('click', (e) => {
531
+ paused = !paused;
532
+ e.target.textContent = paused ? 'resume' : 'pause';
533
+ if (!paused) { poll(); nextAt = Date.now() + REFRESH_MS; }
534
+ });
535
+ document.getElementById('now').addEventListener('click', () => { poll(); });
536
+
537
+ setInterval(() => {
538
+ const tick = document.getElementById('tick');
539
+ tick.textContent = paused
540
+ ? 'paused'
541
+ : `next in ${Math.max(0, Math.round((nextAt - Date.now()) / 1000))}s`;
542
+ }, 250);
543
+
544
+ poll();
545
+ schedule();
546
+ </script>