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.
- aggregate_api/__init__.py +41 -0
- aggregate_api/__main__.py +154 -0
- aggregate_api/app.py +206 -0
- aggregate_api/audit.py +395 -0
- aggregate_api/bounds.py +331 -0
- aggregate_api/cache.py +319 -0
- aggregate_api/capability.py +823 -0
- aggregate_api/completion.py +219 -0
- aggregate_api/config.py +363 -0
- aggregate_api/cors.py +61 -0
- aggregate_api/examples.py +620 -0
- aggregate_api/layer_pricing.py +840 -0
- aggregate_api/library.py +94 -0
- aggregate_api/library_notes.py +96 -0
- aggregate_api/models.py +1407 -0
- aggregate_api/net.py +281 -0
- aggregate_api/pnl.py +101 -0
- aggregate_api/pricing.py +778 -0
- aggregate_api/resources.py +257 -0
- aggregate_api/routes/__init__.py +8 -0
- aggregate_api/routes/decl.py +327 -0
- aggregate_api/routes/examples.py +82 -0
- aggregate_api/routes/meta.py +282 -0
- aggregate_api/routes/objects.py +4119 -0
- aggregate_api/routes/status.py +466 -0
- aggregate_api/serializers.py +565 -0
- aggregate_api/sessions.py +353 -0
- aggregate_api/static/aggregate-api-logo-512.png +0 -0
- aggregate_api/static/aggregate-api-logo.png +0 -0
- aggregate_api/static/aggregate-api-trim.png +0 -0
- aggregate_api/static/android-chrome-192x192.png +0 -0
- aggregate_api/static/android-chrome-512x512.png +0 -0
- aggregate_api/static/apple-touch-icon.png +0 -0
- aggregate_api/static/assets/bootstrap-icons-BeopsB42.woff +0 -0
- aggregate_api/static/assets/bootstrap-icons-mSm7cUeB.woff2 +0 -0
- aggregate_api/static/assets/bootstrap-ohb1VZ53.js +5 -0
- aggregate_api/static/assets/codemirror-h62DHGGa.js +14 -0
- aggregate_api/static/assets/csv-grid.worker-DKzHGXac.js +4 -0
- aggregate_api/static/assets/echarts-B7o9sc00.js +40 -0
- aggregate_api/static/assets/echarts-gl-DG1Uf6wE.js +4282 -0
- aggregate_api/static/assets/lite-CUlcD8p4.css +1 -0
- aggregate_api/static/assets/lite-Dd2TnT4M.js +1 -0
- aggregate_api/static/assets/main-Bxhxa55v.css +9 -0
- aggregate_api/static/assets/main-CmoEiPit.js +9 -0
- aggregate_api/static/assets/tables-BHCF7qIF.js +8 -0
- aggregate_api/static/assets/tables-CxvajLr7.css +1 -0
- aggregate_api/static/favicon-16x16.png +0 -0
- aggregate_api/static/favicon-32x32.png +0 -0
- aggregate_api/static/favicon.ico +0 -0
- aggregate_api/static/index.html +912 -0
- aggregate_api/static/lite.html +83 -0
- aggregate_api/static/logo.png +0 -0
- aggregate_api/static/site.webmanifest +14 -0
- aggregate_api/static/sw.js +78 -0
- aggregate_api/status.py +536 -0
- aggregate_api/status_page.html +546 -0
- aggregate_api/tables.py +316 -0
- aggregate_api-1.0.0.dist-info/METADATA +187 -0
- aggregate_api-1.0.0.dist-info/RECORD +63 -0
- aggregate_api-1.0.0.dist-info/WHEEL +5 -0
- aggregate_api-1.0.0.dist-info/entry_points.txt +2 -0
- aggregate_api-1.0.0.dist-info/licenses/LICENSE +28 -0
- 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>
|