jskelet 0.1.1 → 0.1.3

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 (66) hide show
  1. package/AGENTS.md +5 -0
  2. package/CHANGELOG.md +129 -2
  3. package/README.md +21 -7
  4. package/bin/jskelet.mjs +6 -6
  5. package/docs/03-routing.md +48 -9
  6. package/docs/04-render-ve-sablonlar.md +2 -2
  7. package/docs/05-islands.md +59 -6
  8. package/docs/06-cache.md +240 -26
  9. package/docs/07-yapilandirma.md +108 -7
  10. package/docs/08-build.md +4 -4
  11. package/docs/09-dev-araclari.md +5 -0
  12. package/docs/12-panel-ve-oturum.md +384 -0
  13. package/docs/README.md +25 -2
  14. package/docs/en/01-getting-started.md +292 -0
  15. package/docs/en/02-architecture.md +305 -0
  16. package/docs/en/03-routing.md +493 -0
  17. package/docs/en/04-rendering.md +504 -0
  18. package/docs/en/05-islands.md +492 -0
  19. package/docs/en/06-caching.md +640 -0
  20. package/docs/en/07-configuration.md +789 -0
  21. package/docs/en/08-build.md +383 -0
  22. package/docs/en/09-dev-tools.md +314 -0
  23. package/docs/en/10-deployment.md +332 -0
  24. package/docs/en/11-migration.md +360 -0
  25. package/docs/en/12-dashboards-and-sessions.md +392 -0
  26. package/docs/en/README.md +112 -0
  27. package/package.json +4 -2
  28. package/src/build/build.mjs +1 -1
  29. package/src/build/tasks/client.mjs +2 -2
  30. package/src/build/tasks/fonts.mjs +3 -3
  31. package/src/build/tasks/icons.mjs +1 -1
  32. package/src/build/tasks/images.mjs +2 -2
  33. package/src/client/devtools/overlay.js +196 -164
  34. package/src/client/devtools/report.js +96 -96
  35. package/src/client/form.js +192 -0
  36. package/src/client/index.js +10 -1
  37. package/src/client/registry.js +78 -4
  38. package/src/client/swap.js +188 -0
  39. package/src/config/defaults.js +83 -0
  40. package/src/config/index.js +129 -18
  41. package/src/config/pattern.js +1 -1
  42. package/src/dev-server.mjs +1 -1
  43. package/src/http/control-flow.js +16 -1
  44. package/src/http/cookies.js +257 -0
  45. package/src/http/request-context.js +162 -0
  46. package/src/index.js +26 -2
  47. package/src/init.mjs +32 -31
  48. package/src/log.mjs +8 -2
  49. package/src/logo.png +0 -0
  50. package/src/runtime/alias-hooks.mjs +1 -1
  51. package/src/server/assets.js +1 -1
  52. package/src/server/create-app.js +12 -4
  53. package/src/server/data-cache.js +244 -0
  54. package/src/server/dev/devtools.js +6 -2
  55. package/src/server/dev/report.js +8 -1
  56. package/src/server/dev/version-check.mjs +139 -0
  57. package/src/server/head-hints.js +1 -1
  58. package/src/server/html-cache.js +32 -6
  59. package/src/server/middleware/csrf.js +134 -0
  60. package/src/server/prewarm.js +164 -19
  61. package/src/server/render.js +256 -20
  62. package/src/server/router.js +14 -7
  63. package/src/server/status-page.js +1 -1
  64. package/src/version.mjs +9 -4
  65. package/src/views/components/loader.js +1 -1
  66. package/src/views/helpers/tags.js +53 -1
@@ -1,5 +1,5 @@
1
1
  /**
2
- * "Detaylı İnceleme" sayfasının arayüzü.
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: "Genel bakış" },
29
- { id: "pages", label: "Sayfalar" },
30
- { id: "chunks", label: "Chunk analizi" },
31
- { id: "assets", label: "Varlıklar" },
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 istekleri" },
34
- { id: "errors", label: "Hatalar" },
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 ?? ""), "tr");
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="Filtrele — ${visible.length}/${rows.length} satır" />
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">Kayıt yok.</div>`;
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 geneli<span class="note">${pages.length} sayfa biliniyor, ${measured.length} tanesi tarayıcıda ölçüldü</span></h2>
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: "Ortalama LCP",
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: `en kötü ${ms(worst[0]?.metrics?.lcp)}`,
201
+ sub: `worst ${ms(worst[0]?.metrics?.lcp)}`,
202
202
  },
203
203
  {
204
- label: "Ölçülen sayfa",
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: "gez ki ölçülsün",
208
+ sub: "browse to measure",
209
209
  },
210
- { label: "JS çıktısı", value: size(jsBytes), sub: `${js.length} dosya` },
211
- { label: "CSS çıktısı", value: size(cssBytes), sub: `${css.length} dosya` },
212
- { label: "SSR HTML", value: size(ssrBytes), sub: "bilinen sayfaların toplamı" },
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: "bağlanan / işaretlenen",
218
+ sub: "mounted / marked",
219
219
  },
220
220
  {
221
- label: "HTML önbelleği",
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))} bellekte`,
223
+ sub: `${size((data.cache?.entries ?? []).reduce((sum, item) => sum + item.bytes, 0))} in memory`,
224
224
  },
225
225
  {
226
- label: "Sunucu API çağrısı",
226
+ label: "Server API calls",
227
227
  value: String((data.serverApi ?? []).length),
228
228
  tone: apiErrors ? "bad" : "good",
229
- sub: apiErrors ? `${apiErrors} hata` : "hata yok",
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: "Süreç",
240
+ label: "Process",
241
241
  value: size(data.process?.memory?.rss),
242
- sub: `${data.process?.node} · ${Math.round(data.process?.uptime ?? 0)} sn`,
242
+ sub: `${data.process?.node} · ${Math.round(data.process?.uptime ?? 0)} s`,
243
243
  },
244
244
  ])}
245
245
 
246
- <h2>En yavaş sayfalar<span class="note">LCP'ye göre</span></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: "Sayfa", render: (row) => link(row.url) },
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">Henüz tarayıcı ölçümü yok. Sitede gezindikçe sayfalar buraya düşer.</div>`
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 dağılımı</h2>
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()} — medyan ${ms(p(0.5))}, p75 ${ms(p(0.75))}, p95 ${ms(p(0.95))}</span>
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>Sayfalar<span class="note">tarayıcı ölçümleri gezdikçe birikir; SSR sütunları ısıtma turundan gelir</span></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: "Sayfa", render: (row) => link(row.url) },
295
- { key: "visits", label: "Ziyaret", value: (row) => row.visits },
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: "İstek", value: (row) => row.resources?.count ?? 0 },
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 analizi için <code>build/generated/metafile.json</code> gerekiyor. <code>npm run dev</code> veya <code>npm run build</code> bir tur döndüğünde oluşur.</div>`;
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>Çıktılar<span class="note">${outputs.length} dosya · ${size(totalBytes)} ham</span></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: "Dosya",
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: "Ham", value: (row) => row.bytes, render: (row) => size(row.bytes) },
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: "Modül", value: (row) => row.inputCount },
346
- { key: "imports", label: "Import", value: (row) => row.imports.length },
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: "Pay",
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">Seçili çıktı</h2>
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)} ham · ${size(selected.gzip)} gzip · ${selected.inputCount} modül
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>Import ettiği chunk'lar</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">Soldan bir çıktı seç.</div>`
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>Kaynak grupları<span class="note">tüm çıktılarda paket / klasör payı</span></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 varlıkları<span class="note">${assets.length} girdi · ${size(total)}</span></h2>
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: "Ad" },
423
- { key: "url", label: "Yol", render: (row) => link(row.url) },
424
- { key: "kind", label: "Tür", render: (row) => `<span class="tag">${escapeHtml(row.kind)}</span>` },
425
- { key: "bytes", label: "Ham", value: (row) => row.bytes ?? 0, render: (row) => size(row.bytes) },
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: "Kazanç",
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>Sunucu render & istemci hidrasyonu</h2>
462
+ <h2>Server render & client hydration</h2>
463
463
  ${metrics([
464
- { label: "SSR sayfa", value: String(pages.filter((page) => page.html?.bytes).length), sub: "HTML boyutu bilinen" },
465
- { label: "Island'lı sayfa", value: `${csr.length}/${pages.length}`, ratio: pages.length ? csr.length / pages.length : null, tone: "good" },
466
- { label: "Eksik hidrasyon", value: String(partial.length), tone: partial.length ? "mid" : "good", sub: "bazı island'lar bağlanmadı" },
467
- { label: "Farklı island", value: String(islands.size) },
468
- { label: "Önbellek girdisi", value: String(data.cache?.size ?? 0) },
469
- { label: "Önbellek boyutu", value: size(cache.reduce((sum, item) => sum + item.bytes, 0)) },
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 envanteri<span class="note">gezilen sayfalarda görülen island'lar</span></h2>
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: "Sayfa sayısı", value: (row) => row.pages },
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 önbelleği<span class="note">bellekteki girdiler, bayatlama süresiyle</span></h2>
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: "Anahtar" },
488
- { key: "status", label: "Durum", render: (row) => `<span class="status ${row.status >= 400 ? "bad" : ""}">${row.status}</span>` },
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: "Tazelik", value: (row) => row.expiresIn, render: (row) => (row.stale ? `<span class="mid">bayat</span>` : `${row.expiresIn} sn`) },
491
- { key: "encodings", label: "Sıkıştırma", render: (row) => (row.encodings.length ? row.encodings.map((item) => `<span class="tag">${escapeHtml(item)}</span>`).join(" ") : "—") },
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>Sunucu tarafı çağrılar<span class="note">SSR sırasında yapılan dış istekler, yola göre gruplu</span></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: "Yol" },
529
+ { key: "key", label: "Path" },
530
530
  { key: "host", label: "Host" },
531
- { key: "calls", label: "Çağrı", value: (row) => row.calls },
532
- { key: "avg", label: "Ort. süre", 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: "Toplam süre", value: (row) => row.ms, render: (row) => ms(row.ms) },
534
- { key: "bytes", label: "Gövde", value: (row) => row.bytes, render: (row) => size(row.bytes) },
535
- { key: "errors", label: "Hata", value: (row) => row.errors, render: (row) => (row.errors ? `<span class="bad">${row.errors}</span>` : "0") },
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>Son çağrılar</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: "Metot" },
547
- { key: "status", label: "Durum", render: (row) => `<span class="status ${row.error ? "bad" : ""}">${row.status || "—"}</span>` },
548
- { key: "ms", label: "Süre", value: (row) => row.ms, render: (row) => `<span class="${tone(row.ms, [200, 800])}">${ms(row.ms)}</span>` },
549
- { key: "bytes", label: "Gövde", value: (row) => row.bytes, render: (row) => size(row.bytes) },
550
- { key: "error", label: "Hata", render: (row) => (row.error ? `<span class="bad">${escapeHtml(row.error)}</span>` : "—") },
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>Tarayıcı tarafı çağrılar<span class="note">island'ların yaptığı fetch istekleri</span></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: "Sayfa", render: (row) => link(row.page) },
562
- { key: "status", label: "Durum", render: (row) => `<span class="status ${row.status >= 400 || !row.status ? "bad" : ""}">${row.status || "—"}</span>` },
563
- { key: "ms", label: "Süre", value: (row) => row.ms, render: (row) => `<span class="${tone(row.ms, [200, 800])}">${ms(row.ms)}</span>` },
564
- { key: "bytes", label: "Gövde", value: (row) => row.bytes, render: (row) => size(row.bytes) },
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>Hatalar<span class="note">sunucu günlüğü</span></h2>
574
+ <h2>Errors<span class="note">server log</span></h2>
575
575
  ${table(
576
576
  "errors",
577
577
  [
578
- { key: "message", label: "Mesaj" },
579
- { key: "level", label: "Seviye", render: (row) => `<span class="tag">${escapeHtml(row.level)}</span>` },
580
- { key: "at", label: "Zaman", value: (row) => row.at, render: (row) => new Date(row.at).toLocaleTimeString("tr-TR") },
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>Son istekler</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: "Durum", render: (row) => `<span class="status ${row.status >= 500 ? "bad" : row.status >= 400 ? "warn" : ""}">${row.status}</span>` },
592
- { key: "ms", label: "Süre", value: (row) => row.ms, render: (row) => `<span class="${tone(row.ms, [150, 500])}">${ms(row.ms)}</span>` },
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: "Zaman", value: (row) => row.at, render: (row) => new Date(row.at).toLocaleTimeString("tr-TR") },
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">Veri yükleniyor…</div>`;
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("tr-TR");
629
- status.textContent = `${data.pages.length} sayfa · ${time}`;
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 = "sunucuya ulaşılamıyor";
640
+ document.querySelector("[data-part='status']").textContent = "server unreachable";
641
641
  return;
642
642
  }
643
643
  render();