jskelet 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +5 -0
- package/CHANGELOG.md +63 -0
- package/README.md +21 -7
- package/bin/jskelet.mjs +6 -6
- package/docs/03-routing.md +48 -9
- package/docs/04-render-ve-sablonlar.md +2 -2
- package/docs/05-islands.md +59 -6
- package/docs/06-cache.md +39 -7
- package/docs/07-yapilandirma.md +51 -1
- package/docs/08-build.md +4 -4
- package/docs/09-dev-araclari.md +5 -0
- package/docs/12-panel-ve-oturum.md +384 -0
- package/docs/README.md +25 -2
- package/docs/en/01-getting-started.md +292 -0
- package/docs/en/02-architecture.md +305 -0
- package/docs/en/03-routing.md +493 -0
- package/docs/en/04-rendering.md +504 -0
- package/docs/en/05-islands.md +492 -0
- package/docs/en/06-caching.md +454 -0
- package/docs/en/07-configuration.md +736 -0
- package/docs/en/08-build.md +383 -0
- package/docs/en/09-dev-tools.md +314 -0
- package/docs/en/10-deployment.md +332 -0
- package/docs/en/11-migration.md +360 -0
- package/docs/en/12-dashboards-and-sessions.md +392 -0
- package/docs/en/README.md +112 -0
- package/package.json +4 -2
- package/src/build/build.mjs +1 -1
- package/src/build/tasks/client.mjs +2 -2
- package/src/build/tasks/fonts.mjs +3 -3
- package/src/build/tasks/icons.mjs +1 -1
- package/src/build/tasks/images.mjs +2 -2
- package/src/client/devtools/overlay.js +196 -164
- package/src/client/devtools/report.js +96 -96
- package/src/client/form.js +192 -0
- package/src/client/index.js +10 -1
- package/src/client/registry.js +78 -4
- package/src/client/swap.js +188 -0
- package/src/config/defaults.js +34 -0
- package/src/config/index.js +68 -13
- package/src/config/pattern.js +1 -1
- package/src/dev-server.mjs +1 -1
- package/src/http/control-flow.js +16 -1
- package/src/http/cookies.js +257 -0
- package/src/http/request-context.js +162 -0
- package/src/index.js +19 -2
- package/src/init.mjs +32 -31
- package/src/log.mjs +8 -2
- package/src/logo.png +0 -0
- package/src/runtime/alias-hooks.mjs +1 -1
- package/src/server/assets.js +1 -1
- package/src/server/create-app.js +12 -4
- package/src/server/dev/devtools.js +6 -2
- package/src/server/dev/version-check.mjs +139 -0
- package/src/server/head-hints.js +1 -1
- package/src/server/html-cache.js +10 -4
- package/src/server/middleware/csrf.js +134 -0
- package/src/server/prewarm.js +6 -6
- package/src/server/render.js +199 -16
- package/src/server/router.js +14 -7
- package/src/server/status-page.js +1 -1
- package/src/version.mjs +9 -4
- package/src/views/components/loader.js +1 -1
- package/src/views/helpers/tags.js +53 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* "
|
|
2
|
+
* "Inspect in detail" sayfasının arayüzü.
|
|
3
3
|
*
|
|
4
4
|
* Overlay ile aynı mantık: build'e girmez, dev sunucusu bu dosyayı ham olarak
|
|
5
5
|
* servis eder. Tüm veri tek bir uçtan gelir (`/__jskelet/dev/report/data`),
|
|
@@ -25,13 +25,13 @@ const filters = new Map();
|
|
|
25
25
|
let selectedChunk = null;
|
|
26
26
|
|
|
27
27
|
const TABS = [
|
|
28
|
-
{ id: "overview", label: "
|
|
29
|
-
{ id: "pages", label: "
|
|
30
|
-
{ id: "chunks", label: "Chunk
|
|
31
|
-
{ id: "assets", label: "
|
|
28
|
+
{ id: "overview", label: "Overview" },
|
|
29
|
+
{ id: "pages", label: "Pages" },
|
|
30
|
+
{ id: "chunks", label: "Chunk analysis" },
|
|
31
|
+
{ id: "assets", label: "Assets" },
|
|
32
32
|
{ id: "ssr", label: "SSR & CSR" },
|
|
33
|
-
{ id: "api", label: "API
|
|
34
|
-
{ id: "errors", label: "
|
|
33
|
+
{ id: "api", label: "API requests" },
|
|
34
|
+
{ id: "errors", label: "Errors" },
|
|
35
35
|
];
|
|
36
36
|
|
|
37
37
|
/* ------------------------------------------------------------- yardımcılar */
|
|
@@ -129,19 +129,19 @@ function table(id, columns, rows, options = {}) {
|
|
|
129
129
|
const compare =
|
|
130
130
|
typeof left === "number" && typeof right === "number"
|
|
131
131
|
? left - right
|
|
132
|
-
: String(left ?? "").localeCompare(String(right ?? "")
|
|
132
|
+
: String(left ?? "").localeCompare(String(right ?? ""));
|
|
133
133
|
return sort.dir === "asc" ? compare : -compare;
|
|
134
134
|
});
|
|
135
135
|
|
|
136
136
|
const search = options.search
|
|
137
137
|
? `<div class="toolbar">
|
|
138
138
|
<input type="search" data-filter="${id}" value="${escapeHtml(filters.get(id) ?? "")}"
|
|
139
|
-
placeholder="
|
|
139
|
+
placeholder="Filter — ${visible.length}/${rows.length} rows" />
|
|
140
140
|
</div>`
|
|
141
141
|
: "";
|
|
142
142
|
|
|
143
143
|
if (!rows.length) {
|
|
144
|
-
return `${search}<div class="empty">
|
|
144
|
+
return `${search}<div class="empty">No records.</div>`;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
return `${search}
|
|
@@ -191,42 +191,42 @@ function overview() {
|
|
|
191
191
|
const islandReady = pages.reduce((sum, page) => sum + (page.islands?.ready ?? 0), 0);
|
|
192
192
|
|
|
193
193
|
return `
|
|
194
|
-
<h2>Site
|
|
194
|
+
<h2>Site wide<span class="note">${pages.length} pages known, ${measured.length} of them measured in the browser</span></h2>
|
|
195
195
|
${metrics([
|
|
196
196
|
{
|
|
197
|
-
label: "
|
|
197
|
+
label: "Average LCP",
|
|
198
198
|
value: ms(average),
|
|
199
199
|
tone: tone(average, [2500, 4000]),
|
|
200
200
|
ratio: average == null ? null : average / 4000,
|
|
201
|
-
sub: `
|
|
201
|
+
sub: `worst ${ms(worst[0]?.metrics?.lcp)}`,
|
|
202
202
|
},
|
|
203
203
|
{
|
|
204
|
-
label: "
|
|
204
|
+
label: "Measured pages",
|
|
205
205
|
value: `${measured.length}/${pages.length}`,
|
|
206
206
|
ratio: pages.length ? measured.length / pages.length : null,
|
|
207
207
|
tone: "good",
|
|
208
|
-
sub: "
|
|
208
|
+
sub: "browse to measure",
|
|
209
209
|
},
|
|
210
|
-
{ label: "JS
|
|
211
|
-
{ label: "CSS
|
|
212
|
-
{ label: "SSR HTML", value: size(ssrBytes), sub: "
|
|
210
|
+
{ label: "JS output", value: size(jsBytes), sub: `${js.length} files` },
|
|
211
|
+
{ label: "CSS output", value: size(cssBytes), sub: `${css.length} files` },
|
|
212
|
+
{ label: "SSR HTML", value: size(ssrBytes), sub: "total of known pages" },
|
|
213
213
|
{
|
|
214
214
|
label: "Island (CSR)",
|
|
215
215
|
value: `${islandReady}/${islandTotal}`,
|
|
216
216
|
ratio: islandTotal ? islandReady / islandTotal : null,
|
|
217
217
|
tone: "good",
|
|
218
|
-
sub: "
|
|
218
|
+
sub: "mounted / marked",
|
|
219
219
|
},
|
|
220
220
|
{
|
|
221
|
-
label: "HTML
|
|
221
|
+
label: "HTML cache",
|
|
222
222
|
value: String(data.cache?.size ?? 0),
|
|
223
|
-
sub: `${size((data.cache?.entries ?? []).reduce((sum, item) => sum + item.bytes, 0))}
|
|
223
|
+
sub: `${size((data.cache?.entries ?? []).reduce((sum, item) => sum + item.bytes, 0))} in memory`,
|
|
224
224
|
},
|
|
225
225
|
{
|
|
226
|
-
label: "
|
|
226
|
+
label: "Server API calls",
|
|
227
227
|
value: String((data.serverApi ?? []).length),
|
|
228
228
|
tone: apiErrors ? "bad" : "good",
|
|
229
|
-
sub: apiErrors ? `${apiErrors}
|
|
229
|
+
sub: apiErrors ? `${apiErrors} errors` : "no errors",
|
|
230
230
|
},
|
|
231
231
|
{
|
|
232
232
|
label: "Prewarm",
|
|
@@ -237,19 +237,19 @@ function overview() {
|
|
|
237
237
|
tone: data.prewarm?.failed ? "mid" : "good",
|
|
238
238
|
},
|
|
239
239
|
{
|
|
240
|
-
label: "
|
|
240
|
+
label: "Process",
|
|
241
241
|
value: size(data.process?.memory?.rss),
|
|
242
|
-
sub: `${data.process?.node} · ${Math.round(data.process?.uptime ?? 0)}
|
|
242
|
+
sub: `${data.process?.node} · ${Math.round(data.process?.uptime ?? 0)} s`,
|
|
243
243
|
},
|
|
244
244
|
])}
|
|
245
245
|
|
|
246
|
-
<h2>
|
|
246
|
+
<h2>Slowest pages<span class="note">by LCP</span></h2>
|
|
247
247
|
${
|
|
248
248
|
worst.length
|
|
249
249
|
? table(
|
|
250
250
|
"worst",
|
|
251
251
|
[
|
|
252
|
-
{ key: "url", label: "
|
|
252
|
+
{ key: "url", label: "Page", render: (row) => link(row.url) },
|
|
253
253
|
{ key: "lcp", label: "LCP", value: (row) => row.metrics.lcp, render: (row) => `<span class="${tone(row.metrics.lcp, [2500, 4000])}">${ms(row.metrics.lcp)}</span>` },
|
|
254
254
|
{ key: "cls", label: "CLS", value: (row) => row.metrics.cls ?? 0, render: (row) => (row.metrics.cls ?? 0).toFixed(3) },
|
|
255
255
|
{ key: "inp", label: "INP", value: (row) => row.metrics.inp ?? 0, render: (row) => ms(row.metrics.inp) },
|
|
@@ -258,10 +258,10 @@ function overview() {
|
|
|
258
258
|
],
|
|
259
259
|
worst,
|
|
260
260
|
)
|
|
261
|
-
: `<div class="empty">
|
|
261
|
+
: `<div class="empty">No browser measurements yet. Pages land here as you browse the site.</div>`
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
-
<h2>Web Vitals
|
|
264
|
+
<h2>Web Vitals distribution</h2>
|
|
265
265
|
<div class="card">
|
|
266
266
|
${["lcp", "fcp", "ttfb", "inp"]
|
|
267
267
|
.map((key) => {
|
|
@@ -273,7 +273,7 @@ function overview() {
|
|
|
273
273
|
const p = (ratio) => values[Math.min(values.length - 1, Math.floor(values.length * ratio))];
|
|
274
274
|
const bounds = { lcp: [2500, 4000], fcp: [1800, 3000], ttfb: [800, 1800], inp: [200, 500] }[key];
|
|
275
275
|
return `<div class="barrow">
|
|
276
|
-
<span class="name">${key.toUpperCase()} —
|
|
276
|
+
<span class="name">${key.toUpperCase()} — median ${ms(p(0.5))}, p75 ${ms(p(0.75))}, p95 ${ms(p(0.95))}</span>
|
|
277
277
|
<span class="size ${tone(p(0.75), bounds)}">${ms(p(0.75))}</span>
|
|
278
278
|
<span class="track"><span class="${tone(p(0.75), bounds)}" style="width:${Math.min(100, (p(0.75) / (bounds[1] * 1.5)) * 100)}%"></span></span>
|
|
279
279
|
</div>`;
|
|
@@ -287,19 +287,19 @@ function pagesTab() {
|
|
|
287
287
|
const rows = data.pages ?? [];
|
|
288
288
|
|
|
289
289
|
return `
|
|
290
|
-
<h2>
|
|
290
|
+
<h2>Pages<span class="note">browser measurements build up as you browse; SSR columns come from the prewarm round</span></h2>
|
|
291
291
|
${table(
|
|
292
292
|
"pages",
|
|
293
293
|
[
|
|
294
|
-
{ key: "url", label: "
|
|
295
|
-
{ key: "visits", label: "
|
|
294
|
+
{ key: "url", label: "Page", render: (row) => link(row.url) },
|
|
295
|
+
{ key: "visits", label: "Visits", value: (row) => row.visits },
|
|
296
296
|
{ key: "lcp", label: "LCP", value: (row) => row.metrics?.lcp ?? -1, render: (row) => `<span class="${tone(row.metrics?.lcp, [2500, 4000])}">${ms(row.metrics?.lcp)}</span>` },
|
|
297
297
|
{ key: "cls", label: "CLS", value: (row) => row.metrics?.cls ?? -1, render: (row) => (row.metrics?.cls == null ? "—" : `<span class="${tone(row.metrics.cls * 1000, [100, 250])}">${row.metrics.cls.toFixed(3)}</span>`) },
|
|
298
298
|
{ key: "inp", label: "INP", value: (row) => row.metrics?.inp ?? -1, render: (row) => `<span class="${tone(row.metrics?.inp, [200, 500])}">${ms(row.metrics?.inp)}</span>` },
|
|
299
299
|
{ key: "fcp", label: "FCP", value: (row) => row.metrics?.fcp ?? -1, render: (row) => ms(row.metrics?.fcp) },
|
|
300
300
|
{ key: "ttfb", label: "TTFB", value: (row) => row.metrics?.ttfb ?? -1, render: (row) => `<span class="${tone(row.metrics?.ttfb, [800, 1800])}">${ms(row.metrics?.ttfb)}</span>` },
|
|
301
301
|
{ key: "blocking", label: "Blocking", value: (row) => row.metrics?.blocking ?? -1, render: (row) => ms(row.metrics?.blocking) },
|
|
302
|
-
{ key: "requests", label: "
|
|
302
|
+
{ key: "requests", label: "Requests", value: (row) => row.resources?.count ?? 0 },
|
|
303
303
|
{ key: "transfer", label: "Transfer", value: (row) => row.resources?.bytes ?? 0, render: (row) => size(row.resources?.bytes) },
|
|
304
304
|
{ key: "islands", label: "Island", value: (row) => row.islands?.total ?? 0, render: (row) => (row.islands?.total ? `${row.islands.ready}/${row.islands.total}` : "—") },
|
|
305
305
|
{ key: "api", label: "API", value: (row) => row.api?.length ?? 0 },
|
|
@@ -316,7 +316,7 @@ function pagesTab() {
|
|
|
316
316
|
function chunksTab() {
|
|
317
317
|
const build = data.build ?? {};
|
|
318
318
|
if (!build.available) {
|
|
319
|
-
return `<div class="empty">Chunk
|
|
319
|
+
return `<div class="empty">Chunk analysis needs <code>build/generated/metafile.json</code>. It appears once <code>npm run dev</code> or <code>npm run build</code> has completed a round.</div>`;
|
|
320
320
|
}
|
|
321
321
|
|
|
322
322
|
const outputs = build.outputs ?? [];
|
|
@@ -327,7 +327,7 @@ function chunksTab() {
|
|
|
327
327
|
const groupTotal = (build.groups ?? []).reduce((sum, item) => sum + item.bytes, 0) || 1;
|
|
328
328
|
|
|
329
329
|
return `
|
|
330
|
-
<h2
|
|
330
|
+
<h2>Outputs<span class="note">${outputs.length} files · ${size(totalBytes)} raw</span></h2>
|
|
331
331
|
<div class="split">
|
|
332
332
|
<div>
|
|
333
333
|
${table(
|
|
@@ -335,18 +335,18 @@ function chunksTab() {
|
|
|
335
335
|
[
|
|
336
336
|
{
|
|
337
337
|
key: "file",
|
|
338
|
-
label: "
|
|
338
|
+
label: "File",
|
|
339
339
|
render: (row) =>
|
|
340
340
|
`${link(row.file)} <span class="tag ${row.isChunk ? "chunk" : "entry"}">${row.isChunk ? "chunk" : "entry"}</span>`,
|
|
341
341
|
},
|
|
342
|
-
{ key: "bytes", label: "
|
|
342
|
+
{ key: "bytes", label: "Raw", value: (row) => row.bytes, render: (row) => size(row.bytes) },
|
|
343
343
|
{ key: "gzip", label: "Gzip", value: (row) => row.gzip ?? 0, render: (row) => size(row.gzip) },
|
|
344
344
|
{ key: "brotli", label: "Brotli", value: (row) => row.brotli ?? 0, render: (row) => size(row.brotli) },
|
|
345
|
-
{ key: "inputCount", label: "
|
|
346
|
-
{ key: "imports", label: "
|
|
345
|
+
{ key: "inputCount", label: "Modules", value: (row) => row.inputCount },
|
|
346
|
+
{ key: "imports", label: "Imports", value: (row) => row.imports.length },
|
|
347
347
|
{
|
|
348
348
|
key: "share",
|
|
349
|
-
label: "
|
|
349
|
+
label: "Share",
|
|
350
350
|
value: (row) => row.bytes / heaviest,
|
|
351
351
|
render: (row) => `${Math.round((row.bytes / totalBytes) * 100)}%`,
|
|
352
352
|
},
|
|
@@ -361,12 +361,12 @@ function chunksTab() {
|
|
|
361
361
|
</div>
|
|
362
362
|
<div>
|
|
363
363
|
<div class="card">
|
|
364
|
-
<h2 style="margin-top:0">
|
|
364
|
+
<h2 style="margin-top:0">Selected output</h2>
|
|
365
365
|
${
|
|
366
366
|
selected
|
|
367
367
|
? `<div class="hint" style="margin-bottom:10px">
|
|
368
368
|
<code>${escapeHtml(selected.file)}</code><br />
|
|
369
|
-
${size(selected.bytes)}
|
|
369
|
+
${size(selected.bytes)} raw · ${size(selected.gzip)} gzip · ${selected.inputCount} modules
|
|
370
370
|
${selected.entry ? `<br />entry: <code>${escapeHtml(selected.entry)}</code>` : ""}
|
|
371
371
|
</div>
|
|
372
372
|
<div class="bars">
|
|
@@ -382,19 +382,19 @@ function chunksTab() {
|
|
|
382
382
|
</div>
|
|
383
383
|
${
|
|
384
384
|
selected.imports.length
|
|
385
|
-
? `<h2>
|
|
385
|
+
? `<h2>Chunks it imports</h2>
|
|
386
386
|
<div class="hint">${selected.imports
|
|
387
387
|
.map((item) => `<div><code>${escapeHtml(item.path)}</code> <span class="tag">${escapeHtml(item.kind)}</span></div>`)
|
|
388
388
|
.join("")}</div>`
|
|
389
389
|
: ""
|
|
390
390
|
}`
|
|
391
|
-
: `<div class="hint">
|
|
391
|
+
: `<div class="hint">Pick an output on the left.</div>`
|
|
392
392
|
}
|
|
393
393
|
</div>
|
|
394
394
|
</div>
|
|
395
395
|
</div>
|
|
396
396
|
|
|
397
|
-
<h2>
|
|
397
|
+
<h2>Source groups<span class="note">package / folder share across all outputs</span></h2>
|
|
398
398
|
<div class="card bars">
|
|
399
399
|
${(build.groups ?? [])
|
|
400
400
|
.slice(0, 25)
|
|
@@ -415,19 +415,19 @@ function assetsTab() {
|
|
|
415
415
|
const total = assets.reduce((sum, item) => sum + (item.bytes ?? 0), 0);
|
|
416
416
|
|
|
417
417
|
return `
|
|
418
|
-
<h2>Manifest
|
|
418
|
+
<h2>Manifest assets<span class="note">${assets.length} entries · ${size(total)}</span></h2>
|
|
419
419
|
${table(
|
|
420
420
|
"assets",
|
|
421
421
|
[
|
|
422
|
-
{ key: "name", label: "
|
|
423
|
-
{ key: "url", label: "
|
|
424
|
-
{ key: "kind", label: "
|
|
425
|
-
{ key: "bytes", label: "
|
|
422
|
+
{ key: "name", label: "Name" },
|
|
423
|
+
{ key: "url", label: "Path", render: (row) => link(row.url) },
|
|
424
|
+
{ key: "kind", label: "Kind", render: (row) => `<span class="tag">${escapeHtml(row.kind)}</span>` },
|
|
425
|
+
{ key: "bytes", label: "Raw", value: (row) => row.bytes ?? 0, render: (row) => size(row.bytes) },
|
|
426
426
|
{ key: "gzip", label: "Gzip", value: (row) => row.gzip ?? 0, render: (row) => size(row.gzip) },
|
|
427
427
|
{ key: "brotli", label: "Brotli", value: (row) => row.brotli ?? 0, render: (row) => size(row.brotli) },
|
|
428
428
|
{
|
|
429
429
|
key: "ratio",
|
|
430
|
-
label: "
|
|
430
|
+
label: "Savings",
|
|
431
431
|
value: (row) => (row.bytes && row.gzip ? 1 - row.gzip / row.bytes : 0),
|
|
432
432
|
render: (row) =>
|
|
433
433
|
row.bytes && row.gzip
|
|
@@ -459,36 +459,36 @@ function ssrTab() {
|
|
|
459
459
|
const partial = csr.filter((page) => page.islands.ready < page.islands.total);
|
|
460
460
|
|
|
461
461
|
return `
|
|
462
|
-
<h2>
|
|
462
|
+
<h2>Server render & client hydration</h2>
|
|
463
463
|
${metrics([
|
|
464
|
-
{ label: "SSR
|
|
465
|
-
{ label: "
|
|
466
|
-
{ label: "
|
|
467
|
-
{ label: "
|
|
468
|
-
{ label: "
|
|
469
|
-
{ label: "
|
|
464
|
+
{ label: "SSR pages", value: String(pages.filter((page) => page.html?.bytes).length), sub: "HTML size known" },
|
|
465
|
+
{ label: "Pages with islands", value: `${csr.length}/${pages.length}`, ratio: pages.length ? csr.length / pages.length : null, tone: "good" },
|
|
466
|
+
{ label: "Partial hydration", value: String(partial.length), tone: partial.length ? "mid" : "good", sub: "some islands did not mount" },
|
|
467
|
+
{ label: "Distinct islands", value: String(islands.size) },
|
|
468
|
+
{ label: "Cache entries", value: String(data.cache?.size ?? 0) },
|
|
469
|
+
{ label: "Cache size", value: size(cache.reduce((sum, item) => sum + item.bytes, 0)) },
|
|
470
470
|
])}
|
|
471
471
|
|
|
472
|
-
<h2>Island
|
|
472
|
+
<h2>Island inventory<span class="note">islands seen on the pages you browsed</span></h2>
|
|
473
473
|
${table(
|
|
474
474
|
"islands",
|
|
475
475
|
[
|
|
476
476
|
{ key: "name", label: "Island" },
|
|
477
|
-
{ key: "pages", label: "
|
|
477
|
+
{ key: "pages", label: "Page count", value: (row) => row.pages },
|
|
478
478
|
],
|
|
479
479
|
[...islands.values()],
|
|
480
480
|
{ search: true },
|
|
481
481
|
)}
|
|
482
482
|
|
|
483
|
-
<h2>HTML
|
|
483
|
+
<h2>HTML cache<span class="note">entries in memory, with time to stale</span></h2>
|
|
484
484
|
${table(
|
|
485
485
|
"cache",
|
|
486
486
|
[
|
|
487
|
-
{ key: "key", label: "
|
|
488
|
-
{ key: "status", label: "
|
|
487
|
+
{ key: "key", label: "Key" },
|
|
488
|
+
{ key: "status", label: "Status", render: (row) => `<span class="status ${row.status >= 400 ? "bad" : ""}">${row.status}</span>` },
|
|
489
489
|
{ key: "bytes", label: "HTML", value: (row) => row.bytes, render: (row) => size(row.bytes) },
|
|
490
|
-
{ key: "expiresIn", label: "
|
|
491
|
-
{ key: "encodings", label: "
|
|
490
|
+
{ key: "expiresIn", label: "Freshness", value: (row) => row.expiresIn, render: (row) => (row.stale ? `<span class="mid">stale</span>` : `${row.expiresIn} s`) },
|
|
491
|
+
{ key: "encodings", label: "Compression", render: (row) => (row.encodings.length ? row.encodings.map((item) => `<span class="tag">${escapeHtml(item)}</span>`).join(" ") : "—") },
|
|
492
492
|
],
|
|
493
493
|
cache,
|
|
494
494
|
{ search: true },
|
|
@@ -522,46 +522,46 @@ function apiTab() {
|
|
|
522
522
|
);
|
|
523
523
|
|
|
524
524
|
return `
|
|
525
|
-
<h2>
|
|
525
|
+
<h2>Server side calls<span class="note">outbound requests made during SSR, grouped by path</span></h2>
|
|
526
526
|
${table(
|
|
527
527
|
"api-groups",
|
|
528
528
|
[
|
|
529
|
-
{ key: "key", label: "
|
|
529
|
+
{ key: "key", label: "Path" },
|
|
530
530
|
{ key: "host", label: "Host" },
|
|
531
|
-
{ key: "calls", label: "
|
|
532
|
-
{ key: "avg", label: "
|
|
533
|
-
{ key: "total", label: "
|
|
534
|
-
{ key: "bytes", label: "
|
|
535
|
-
{ key: "errors", label: "
|
|
531
|
+
{ key: "calls", label: "Calls", value: (row) => row.calls },
|
|
532
|
+
{ key: "avg", label: "Avg. time", value: (row) => row.ms / row.calls, render: (row) => `<span class="${tone(row.ms / row.calls, [200, 800])}">${ms(row.ms / row.calls)}</span>` },
|
|
533
|
+
{ key: "total", label: "Total time", value: (row) => row.ms, render: (row) => ms(row.ms) },
|
|
534
|
+
{ key: "bytes", label: "Body", value: (row) => row.bytes, render: (row) => size(row.bytes) },
|
|
535
|
+
{ key: "errors", label: "Errors", value: (row) => row.errors, render: (row) => (row.errors ? `<span class="bad">${row.errors}</span>` : "0") },
|
|
536
536
|
],
|
|
537
537
|
[...grouped.values()],
|
|
538
538
|
{ search: true },
|
|
539
539
|
)}
|
|
540
540
|
|
|
541
|
-
<h2>
|
|
541
|
+
<h2>Recent calls</h2>
|
|
542
542
|
${table(
|
|
543
543
|
"api-calls",
|
|
544
544
|
[
|
|
545
545
|
{ key: "url", label: "URL" },
|
|
546
|
-
{ key: "method", label: "
|
|
547
|
-
{ key: "status", label: "
|
|
548
|
-
{ key: "ms", label: "
|
|
549
|
-
{ key: "bytes", label: "
|
|
550
|
-
{ key: "error", label: "
|
|
546
|
+
{ key: "method", label: "Method" },
|
|
547
|
+
{ key: "status", label: "Status", render: (row) => `<span class="status ${row.error ? "bad" : ""}">${row.status || "—"}</span>` },
|
|
548
|
+
{ key: "ms", label: "Time", value: (row) => row.ms, render: (row) => `<span class="${tone(row.ms, [200, 800])}">${ms(row.ms)}</span>` },
|
|
549
|
+
{ key: "bytes", label: "Body", value: (row) => row.bytes, render: (row) => size(row.bytes) },
|
|
550
|
+
{ key: "error", label: "Error", render: (row) => (row.error ? `<span class="bad">${escapeHtml(row.error)}</span>` : "—") },
|
|
551
551
|
],
|
|
552
552
|
server.slice(0, 200),
|
|
553
553
|
{ search: true },
|
|
554
554
|
)}
|
|
555
555
|
|
|
556
|
-
<h2>
|
|
556
|
+
<h2>Browser side calls<span class="note">fetch requests made by islands</span></h2>
|
|
557
557
|
${table(
|
|
558
558
|
"api-client",
|
|
559
559
|
[
|
|
560
560
|
{ key: "url", label: "URL" },
|
|
561
|
-
{ key: "page", label: "
|
|
562
|
-
{ key: "status", label: "
|
|
563
|
-
{ key: "ms", label: "
|
|
564
|
-
{ key: "bytes", label: "
|
|
561
|
+
{ key: "page", label: "Page", render: (row) => link(row.page) },
|
|
562
|
+
{ key: "status", label: "Status", render: (row) => `<span class="status ${row.status >= 400 || !row.status ? "bad" : ""}">${row.status || "—"}</span>` },
|
|
563
|
+
{ key: "ms", label: "Time", value: (row) => row.ms, render: (row) => `<span class="${tone(row.ms, [200, 800])}">${ms(row.ms)}</span>` },
|
|
564
|
+
{ key: "bytes", label: "Body", value: (row) => row.bytes, render: (row) => size(row.bytes) },
|
|
565
565
|
],
|
|
566
566
|
client,
|
|
567
567
|
{ search: true },
|
|
@@ -571,27 +571,27 @@ function apiTab() {
|
|
|
571
571
|
|
|
572
572
|
function errorsTab() {
|
|
573
573
|
return `
|
|
574
|
-
<h2>
|
|
574
|
+
<h2>Errors<span class="note">server log</span></h2>
|
|
575
575
|
${table(
|
|
576
576
|
"errors",
|
|
577
577
|
[
|
|
578
|
-
{ key: "message", label: "
|
|
579
|
-
{ key: "level", label: "
|
|
580
|
-
{ key: "at", label: "
|
|
578
|
+
{ key: "message", label: "Message" },
|
|
579
|
+
{ key: "level", label: "Level", render: (row) => `<span class="tag">${escapeHtml(row.level)}</span>` },
|
|
580
|
+
{ key: "at", label: "Time", value: (row) => row.at, render: (row) => new Date(row.at).toLocaleTimeString() },
|
|
581
581
|
],
|
|
582
582
|
data.errors ?? [],
|
|
583
583
|
{ search: true },
|
|
584
584
|
)}
|
|
585
585
|
|
|
586
|
-
<h2>
|
|
586
|
+
<h2>Recent requests</h2>
|
|
587
587
|
${table(
|
|
588
588
|
"requests",
|
|
589
589
|
[
|
|
590
590
|
{ key: "url", label: "URL", render: (row) => link(row.url) },
|
|
591
|
-
{ key: "status", label: "
|
|
592
|
-
{ key: "ms", label: "
|
|
591
|
+
{ key: "status", label: "Status", render: (row) => `<span class="status ${row.status >= 500 ? "bad" : row.status >= 400 ? "warn" : ""}">${row.status}</span>` },
|
|
592
|
+
{ key: "ms", label: "Time", value: (row) => row.ms, render: (row) => `<span class="${tone(row.ms, [150, 500])}">${ms(row.ms)}</span>` },
|
|
593
593
|
{ key: "cache", label: "Cache", render: (row) => (row.cache ? `<span class="tag">${escapeHtml(row.cache)}</span>` : "—") },
|
|
594
|
-
{ key: "at", label: "
|
|
594
|
+
{ key: "at", label: "Time", value: (row) => row.at, render: (row) => new Date(row.at).toLocaleTimeString() },
|
|
595
595
|
],
|
|
596
596
|
data.requests ?? [],
|
|
597
597
|
{ search: true },
|
|
@@ -621,12 +621,12 @@ function render() {
|
|
|
621
621
|
const main = document.querySelector("[data-part='main']");
|
|
622
622
|
main.innerHTML = data
|
|
623
623
|
? RENDERERS[activeTab]()
|
|
624
|
-
: `<div class="empty">
|
|
624
|
+
: `<div class="empty">Loading data…</div>`;
|
|
625
625
|
|
|
626
626
|
const status = document.querySelector("[data-part='status']");
|
|
627
627
|
if (data) {
|
|
628
|
-
const time = new Date(data.generatedAt).toLocaleTimeString(
|
|
629
|
-
status.textContent = `${data.pages.length}
|
|
628
|
+
const time = new Date(data.generatedAt).toLocaleTimeString();
|
|
629
|
+
status.textContent = `${data.pages.length} pages · ${time}`;
|
|
630
630
|
}
|
|
631
631
|
|
|
632
632
|
document.querySelector("[data-action='auto']").setAttribute("aria-pressed", String(auto));
|
|
@@ -637,7 +637,7 @@ async function load() {
|
|
|
637
637
|
const response = await fetch(`${BASE}/report/data`, { cache: "no-store" });
|
|
638
638
|
data = await response.json();
|
|
639
639
|
} catch {
|
|
640
|
-
document.querySelector("[data-part='status']").textContent = "
|
|
640
|
+
document.querySelector("[data-part='status']").textContent = "server unreachable";
|
|
641
641
|
return;
|
|
642
642
|
}
|
|
643
643
|
render();
|