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
|
@@ -117,7 +117,7 @@ function captureErrors() {
|
|
|
117
117
|
const target = /** @type {HTMLElement | null} */ (event.target);
|
|
118
118
|
if (target && target !== /** @type {any} */ (window)) {
|
|
119
119
|
const url = target.getAttribute?.("src") ?? target.getAttribute?.("href");
|
|
120
|
-
pushError("error", `
|
|
120
|
+
pushError("error", `Resource failed to load: ${url ?? target.tagName}`, {
|
|
121
121
|
source: "resource",
|
|
122
122
|
});
|
|
123
123
|
return;
|
|
@@ -799,26 +799,26 @@ const TRASH_ICON = `<svg viewBox="0 0 16 16"><path d="M2.5 4.5h11M6 4.5V3h4v1.5M
|
|
|
799
799
|
const SKELETON = `
|
|
800
800
|
<div class="root">
|
|
801
801
|
<div class="backdrop" data-action="backdrop" hidden>
|
|
802
|
-
<div class="panel" role="dialog" aria-label="JSkelet dev
|
|
802
|
+
<div class="panel" role="dialog" aria-label="JSkelet dev tools">
|
|
803
803
|
<div class="head">
|
|
804
804
|
<img src="${BASE}/logo.png" alt="">
|
|
805
805
|
<span class="title">JSkelet Dev</span>
|
|
806
806
|
<span class="pill"><span class="dot"></span><span data-part="pill"></span></span>
|
|
807
807
|
<span class="spacer"></span>
|
|
808
808
|
<span class="kbd">Alt+D</span>
|
|
809
|
-
<button class="btn icon" data-action="toggle" title="
|
|
809
|
+
<button class="btn icon" data-action="toggle" title="Close (Esc)">✕</button>
|
|
810
810
|
</div>
|
|
811
811
|
<div class="rail">
|
|
812
|
-
<button class="tab" data-tab="errors">${TAB_ICONS.errors}
|
|
813
|
-
<button class="tab" data-tab="perf">${TAB_ICONS.perf}
|
|
814
|
-
<button class="tab" data-tab="server">${TAB_ICONS.server}
|
|
812
|
+
<button class="tab" data-tab="errors">${TAB_ICONS.errors} Errors<span class="chip" data-part="chip" hidden></span></button>
|
|
813
|
+
<button class="tab" data-tab="perf">${TAB_ICONS.perf} Performance</button>
|
|
814
|
+
<button class="tab" data-tab="server">${TAB_ICONS.server} Server<span class="chip neutral" data-part="version-chip" hidden>update</span></button>
|
|
815
815
|
<button class="tab" data-tab="warm">${TAB_ICONS.warm} Prewarming<span class="chip neutral" data-part="warm-chip" hidden></span></button>
|
|
816
816
|
<div class="rail-foot">
|
|
817
|
-
<button class="tab" data-tab="about">${TAB_ICONS.about}
|
|
817
|
+
<button class="tab" data-tab="about">${TAB_ICONS.about} About</button>
|
|
818
818
|
<div class="divider"></div>
|
|
819
|
-
<button class="btn wide" data-action="reload">${REFRESH_ICON}
|
|
820
|
-
<a class="btn wide" href="${BASE}/report" target="_blank" rel="noreferrer">${SCOPE_ICON}
|
|
821
|
-
<button class="btn wide danger" data-action="clear">${TRASH_ICON}
|
|
819
|
+
<button class="btn wide" data-action="reload">${REFRESH_ICON} Reload page</button>
|
|
820
|
+
<a class="btn wide" href="${BASE}/report" target="_blank" rel="noreferrer">${SCOPE_ICON} Inspect in detail</a>
|
|
821
|
+
<button class="btn wide danger" data-action="clear">${TRASH_ICON} Clear records</button>
|
|
822
822
|
</div>
|
|
823
823
|
</div>
|
|
824
824
|
<div class="body"></div>
|
|
@@ -830,7 +830,7 @@ const SKELETON = `
|
|
|
830
830
|
<span data-part="warm-text"></span>
|
|
831
831
|
</span>
|
|
832
832
|
<button class="bubble" data-action="toggle">
|
|
833
|
-
<img src="${BASE}/logo.png" alt="JSkelet dev
|
|
833
|
+
<img src="${BASE}/logo.png" alt="JSkelet dev tools">
|
|
834
834
|
<span class="badge" hidden></span>
|
|
835
835
|
</button>
|
|
836
836
|
</div>
|
|
@@ -892,7 +892,7 @@ function pathLink(value) {
|
|
|
892
892
|
const text = escapeHtml(value);
|
|
893
893
|
if (!value?.startsWith("/")) return text;
|
|
894
894
|
|
|
895
|
-
return `<a href="${escapeHtml(value)}" target="_blank" rel="noreferrer" title="
|
|
895
|
+
return `<a href="${escapeHtml(value)}" target="_blank" rel="noreferrer" title="Open in new tab: ${text}">${text}</a>`;
|
|
896
896
|
}
|
|
897
897
|
|
|
898
898
|
/** @param {string} value */
|
|
@@ -938,7 +938,7 @@ function ratio(value, ceiling) {
|
|
|
938
938
|
*/
|
|
939
939
|
const clipboard = new Map();
|
|
940
940
|
|
|
941
|
-
/** Kısa süreli "
|
|
941
|
+
/** Kısa süreli "Copied" geri bildirimi için. */
|
|
942
942
|
let copiedKey = null;
|
|
943
943
|
|
|
944
944
|
/**
|
|
@@ -958,7 +958,7 @@ function asText(item) {
|
|
|
958
958
|
|
|
959
959
|
/** @param {{ id?: number, level: string, message: string, stack?: string | null, source?: string, at: number }[]} list */
|
|
960
960
|
function errorList(list, scope) {
|
|
961
|
-
if (!list.length) return `<div class="empty">
|
|
961
|
+
if (!list.length) return `<div class="empty">No records.</div>`;
|
|
962
962
|
return list
|
|
963
963
|
.map((item) => {
|
|
964
964
|
const key = `${scope}:${item.id ?? item.at}`;
|
|
@@ -968,14 +968,14 @@ function errorList(list, scope) {
|
|
|
968
968
|
return `<div class="item ${item.level === "warn" ? "warn" : ""}">
|
|
969
969
|
<div class="meta">
|
|
970
970
|
<span class="tag">${escapeHtml(item.source ?? "server")}</span>
|
|
971
|
-
<span>${new Date(item.at).toLocaleTimeString(
|
|
971
|
+
<span>${new Date(item.at).toLocaleTimeString()}</span>
|
|
972
972
|
<span class="spacer"></span>
|
|
973
973
|
${copyButton(key)}
|
|
974
974
|
</div>
|
|
975
975
|
<div class="msg">${escapeHtml(item.message)}</div>
|
|
976
976
|
${
|
|
977
977
|
item.stack
|
|
978
|
-
? `<button class="link" data-action="stack" data-key="${escapeHtml(key)}">${shown ? "▾
|
|
978
|
+
? `<button class="link" data-action="stack" data-key="${escapeHtml(key)}">${shown ? "▾ hide stack" : "▸ show stack"}</button>
|
|
979
979
|
${shown ? `<pre>${escapeHtml(item.stack)}</pre>` : ""}`
|
|
980
980
|
: ""
|
|
981
981
|
}
|
|
@@ -994,7 +994,7 @@ const CHECK_ICON = `<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 8.5l
|
|
|
994
994
|
function copyButton(key, label = "") {
|
|
995
995
|
const done = copiedKey === key;
|
|
996
996
|
return `<button class="copy ${done ? "done" : ""}" data-action="copy" data-key="${escapeHtml(key)}"
|
|
997
|
-
title="${done ? "
|
|
997
|
+
title="${done ? "Copied" : "Copy"}">${done ? CHECK_ICON : COPY_ICON}${
|
|
998
998
|
label ? `<span>${escapeHtml(label)}</span>` : ""
|
|
999
999
|
}</button>`;
|
|
1000
1000
|
}
|
|
@@ -1028,16 +1028,16 @@ function errorsTab() {
|
|
|
1028
1028
|
${lede(
|
|
1029
1029
|
errorCount ? "bad" : total ? "mid" : "good",
|
|
1030
1030
|
errorCount
|
|
1031
|
-
? `<strong>${errorCount}
|
|
1031
|
+
? `<strong>${errorCount} errors</strong> captured${total - errorCount ? `, plus ${total - errorCount} warnings` : ""}. Newest first; expand the stack trace and copy a record with one click.`
|
|
1032
1032
|
: total
|
|
1033
|
-
? `
|
|
1034
|
-
: "
|
|
1033
|
+
? `No errors, <strong>${total} warnings</strong>. Warnings usually come from missing data or islands that were never ported.`
|
|
1034
|
+
: "No browser or server side errors were seen in this session.",
|
|
1035
1035
|
)}
|
|
1036
|
-
<h4>
|
|
1037
|
-
total ? `<span class="head-action">${copyButton("all", "
|
|
1036
|
+
<h4>Browser · ${clientErrors.length}${
|
|
1037
|
+
total ? `<span class="head-action">${copyButton("all", "copy all")}</span>` : ""
|
|
1038
1038
|
}</h4>
|
|
1039
1039
|
${errorList(clientErrors, "client")}
|
|
1040
|
-
<h4>
|
|
1040
|
+
<h4>Server · ${server.length}</h4>
|
|
1041
1041
|
${errorList(
|
|
1042
1042
|
server.map((item) => ({ ...item, source: "server" })),
|
|
1043
1043
|
"server",
|
|
@@ -1063,7 +1063,7 @@ function perfTab() {
|
|
|
1063
1063
|
`LCP: ${ms(metrics.lcp)} CLS: ${metrics.cls.toFixed(3)} INP: ${ms(metrics.inp)}`,
|
|
1064
1064
|
`FCP: ${ms(metrics.fcp)} TTFB: ${ms(metrics.ttfb)}`,
|
|
1065
1065
|
`Long task: ${metrics.longTasks} Blocking: ${ms(metrics.blocking)}`,
|
|
1066
|
-
|
|
1066
|
+
`Requests: ${resources.count} Transfer: ${kb(resources.bytes)}`,
|
|
1067
1067
|
`Island: ${islands.ready}/${islands.total}`,
|
|
1068
1068
|
].join("\n"),
|
|
1069
1069
|
);
|
|
@@ -1078,11 +1078,11 @@ function perfTab() {
|
|
|
1078
1078
|
return `
|
|
1079
1079
|
${lede(
|
|
1080
1080
|
tone,
|
|
1081
|
-
`
|
|
1082
|
-
`
|
|
1083
|
-
|
|
1081
|
+
`The page painted its largest content at <strong>${ms(metrics.lcp)}</strong>, layout shift is <strong>${metrics.cls.toFixed(3)}</strong>. ` +
|
|
1082
|
+
`The main thread was blocked <strong>${ms(metrics.blocking)}</strong> by ${metrics.longTasks} long tasks; ` +
|
|
1083
|
+
`<strong>${kb(resources.bytes)}</strong> was downloaded over ${resources.count} requests and ${islands.ready} of ${islands.total} islands mounted.`,
|
|
1084
1084
|
)}
|
|
1085
|
-
<h4>Core Web Vitals<span class="head-action">${copyButton("perf", "
|
|
1085
|
+
<h4>Core Web Vitals<span class="head-action">${copyButton("perf", "copy summary")}</span></h4>
|
|
1086
1086
|
${metricGrid([
|
|
1087
1087
|
{ label: "LCP", value: ms(metrics.lcp), tone: grade(metrics.lcp, [2500, 4000]), ratio: ratio(metrics.lcp, 4000) },
|
|
1088
1088
|
{ label: "CLS", value: metrics.cls.toFixed(3), tone: grade(metrics.cls * 1000, [100, 250]), ratio: metrics.cls / 0.25 },
|
|
@@ -1090,7 +1090,7 @@ function perfTab() {
|
|
|
1090
1090
|
{ label: "FCP", value: ms(metrics.fcp), tone: grade(metrics.fcp, [1800, 3000]), ratio: ratio(metrics.fcp, 3000) },
|
|
1091
1091
|
{ label: "TTFB", value: ms(metrics.ttfb), tone: grade(metrics.ttfb, [800, 1800]), ratio: ratio(metrics.ttfb, 1800) },
|
|
1092
1092
|
])}
|
|
1093
|
-
<h4>
|
|
1093
|
+
<h4>Loading</h4>
|
|
1094
1094
|
${metricGrid([
|
|
1095
1095
|
{ label: "DOMContentLoaded", value: ms(metrics.dcl) },
|
|
1096
1096
|
{ label: "Load", value: ms(metrics.load) },
|
|
@@ -1098,9 +1098,9 @@ function perfTab() {
|
|
|
1098
1098
|
{ label: "Blocking", value: ms(metrics.blocking), tone: grade(metrics.blocking, [200, 600]), ratio: metrics.blocking / 600 },
|
|
1099
1099
|
...(memory ? [{ label: "JS heap", value: kb(memory) }] : []),
|
|
1100
1100
|
])}
|
|
1101
|
-
<h4>
|
|
1101
|
+
<h4>Resources</h4>
|
|
1102
1102
|
${metricGrid([
|
|
1103
|
-
{ label: "
|
|
1103
|
+
{ label: "Requests", value: String(resources.count) },
|
|
1104
1104
|
{ label: "Transfer", value: kb(resources.bytes) },
|
|
1105
1105
|
{ label: "Island", value: `${islands.ready}/${islands.total}`, ratio: islands.total ? islands.ready / islands.total : null, tone: "good" },
|
|
1106
1106
|
])}
|
|
@@ -1109,7 +1109,7 @@ function perfTab() {
|
|
|
1109
1109
|
.map(
|
|
1110
1110
|
([kind, info]) => `<div class="row">
|
|
1111
1111
|
<span class="tag">${escapeHtml(kind)}</span>
|
|
1112
|
-
<span class="path">${info.count}
|
|
1112
|
+
<span class="path">${info.count} requests</span>
|
|
1113
1113
|
<span class="hint">${Math.round((info.bytes / heaviest) * 100)}%</span>
|
|
1114
1114
|
<span>${kb(info.bytes)}</span>
|
|
1115
1115
|
</div>`,
|
|
@@ -1119,9 +1119,30 @@ function perfTab() {
|
|
|
1119
1119
|
`;
|
|
1120
1120
|
}
|
|
1121
1121
|
|
|
1122
|
+
/**
|
|
1123
|
+
* Yeni bir sürüm varsa yükseltme satırı. Kayıt defterine ulaşılamadıysa
|
|
1124
|
+
* (`latest` boş) hiçbir şey basılmaz: dev panelinde "bilinmiyor" satırı
|
|
1125
|
+
* gürültüden başka bir şey değil.
|
|
1126
|
+
*/
|
|
1127
|
+
function versionNotice() {
|
|
1128
|
+
const version = serverStats?.version;
|
|
1129
|
+
if (!version?.outdated) return "";
|
|
1130
|
+
|
|
1131
|
+
clipboard.set("upgrade", `npm install jskelet@${version.latest}`);
|
|
1132
|
+
|
|
1133
|
+
return lede(
|
|
1134
|
+
"mid",
|
|
1135
|
+
`JSkelet <strong>${escapeHtml(version.latest)}</strong> is out; this project runs ` +
|
|
1136
|
+
`<strong>${escapeHtml(version.current)}</strong>. Upgrade with ` +
|
|
1137
|
+
`<code>npm install jskelet@latest</code> and skim the changelog for breaking changes. ` +
|
|
1138
|
+
copyButton("upgrade", "copy command"),
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1122
1142
|
function serverTab() {
|
|
1123
|
-
if (!serverStats) return `<div class="empty">
|
|
1143
|
+
if (!serverStats) return `<div class="empty">Waiting for server stats…</div>`;
|
|
1124
1144
|
|
|
1145
|
+
const version = serverStats.version;
|
|
1125
1146
|
const requests = serverStats.requests ?? [];
|
|
1126
1147
|
const slowest = requests.reduce((max, item) => Math.max(max, item.ms), 0);
|
|
1127
1148
|
const average = requests.length
|
|
@@ -1144,24 +1165,30 @@ function serverTab() {
|
|
|
1144
1165
|
${lede(
|
|
1145
1166
|
offline ? "mid" : grade(average, [150, 500]) || "good",
|
|
1146
1167
|
offline
|
|
1147
|
-
? "
|
|
1148
|
-
: `
|
|
1149
|
-
`
|
|
1150
|
-
`(
|
|
1151
|
-
`${restarts ? `
|
|
1168
|
+
? "The server is unreachable right now; we are waiting for it to restart. Existing records are kept."
|
|
1169
|
+
: `The server has been up <strong>${Math.round(serverStats.uptime ?? 0)} s</strong>, ` +
|
|
1170
|
+
`answering the last ${requests.length} requests in <strong>${ms(average)}</strong> on average ` +
|
|
1171
|
+
`(slowest ${ms(slowest)}). The process uses ${kb(serverStats.memory?.rss ?? 0)} of memory` +
|
|
1172
|
+
`${restarts ? ` and restarted ${restarts} times in this session` : ""}.`,
|
|
1152
1173
|
)}
|
|
1153
|
-
|
|
1174
|
+
${versionNotice()}
|
|
1175
|
+
<h4>Process${
|
|
1154
1176
|
requests.length
|
|
1155
|
-
? `<span class="head-action">${copyButton("requests", "
|
|
1177
|
+
? `<span class="head-action">${copyButton("requests", "copy requests")}</span>`
|
|
1156
1178
|
: ""
|
|
1157
1179
|
}</h4>
|
|
1158
1180
|
${metricGrid([
|
|
1181
|
+
{
|
|
1182
|
+
label: "JSkelet",
|
|
1183
|
+
value: version?.current ?? "—",
|
|
1184
|
+
tone: version?.outdated ? "mid" : "",
|
|
1185
|
+
},
|
|
1159
1186
|
{ label: "Node", value: serverStats.node ?? "—" },
|
|
1160
|
-
{ label: "Uptime", value: `${Math.round(serverStats.uptime ?? 0)}
|
|
1187
|
+
{ label: "Uptime", value: `${Math.round(serverStats.uptime ?? 0)} s` },
|
|
1161
1188
|
{ label: "RSS", value: kb(serverStats.memory?.rss ?? 0) },
|
|
1162
1189
|
{ label: "Heap", value: kb(serverStats.memory?.heapUsed ?? 0) },
|
|
1163
|
-
{ label: "
|
|
1164
|
-
{ label: "
|
|
1190
|
+
{ label: "Avg. render", value: ms(average), tone: grade(average, [150, 500]) },
|
|
1191
|
+
{ label: "Slowest", value: ms(slowest), tone: grade(slowest, [300, 1000]) },
|
|
1165
1192
|
{ label: "Restart", value: String(restarts), tone: offline ? "mid" : "" },
|
|
1166
1193
|
...(serverStats.prewarm?.total
|
|
1167
1194
|
? [
|
|
@@ -1174,7 +1201,7 @@ function serverTab() {
|
|
|
1174
1201
|
]
|
|
1175
1202
|
: []),
|
|
1176
1203
|
])}
|
|
1177
|
-
<h4>
|
|
1204
|
+
<h4>Recent requests</h4>
|
|
1178
1205
|
${
|
|
1179
1206
|
requests.length
|
|
1180
1207
|
? `<div class="rows">${requests
|
|
@@ -1187,7 +1214,7 @@ function serverTab() {
|
|
|
1187
1214
|
</div>`,
|
|
1188
1215
|
)
|
|
1189
1216
|
.join("")}</div>`
|
|
1190
|
-
: `<div class="empty">
|
|
1217
|
+
: `<div class="empty">No requests yet.</div>`
|
|
1191
1218
|
}
|
|
1192
1219
|
`;
|
|
1193
1220
|
}
|
|
@@ -1198,7 +1225,7 @@ function warmTab() {
|
|
|
1198
1225
|
if (!warm?.total) {
|
|
1199
1226
|
return `
|
|
1200
1227
|
${warmToolbar(false)}
|
|
1201
|
-
<div class="empty"
|
|
1228
|
+
<div class="empty">Cache prewarming has not started yet. In dev it triggers once the server stays idle for a while; disable it with <code>PREWARM=0</code>.</div>
|
|
1202
1229
|
`;
|
|
1203
1230
|
}
|
|
1204
1231
|
|
|
@@ -1220,7 +1247,7 @@ function warmTab() {
|
|
|
1220
1247
|
clipboard.set(
|
|
1221
1248
|
"warm",
|
|
1222
1249
|
[
|
|
1223
|
-
`Prewarm ${warm.ok}/${warm.total} · ${warm.failed}
|
|
1250
|
+
`Prewarm ${warm.ok}/${warm.total} · ${warm.failed} errors · ${(elapsed / 1000).toFixed(1)}s · ${kb(bytes)}`,
|
|
1224
1251
|
...entries.map(
|
|
1225
1252
|
(item) =>
|
|
1226
1253
|
`${String(item.status).padStart(3)} ${String(item.ms).padStart(5)}ms ${String(
|
|
@@ -1235,38 +1262,38 @@ function warmTab() {
|
|
|
1235
1262
|
${lede(
|
|
1236
1263
|
warm.active ? "mid" : warm.failed ? "bad" : "good",
|
|
1237
1264
|
warm.active
|
|
1238
|
-
? `
|
|
1265
|
+
? `Prewarming in progress: <strong>${warm.done}/${warm.total}</strong> pages tried, ${kb(bytes)} rendered so far.`
|
|
1239
1266
|
: warm.failed
|
|
1240
|
-
? `<strong>${warm.ok}/${warm.total}</strong>
|
|
1241
|
-
: `
|
|
1267
|
+
? `<strong>${warm.ok}/${warm.total}</strong> pages warmed, <strong>${warm.failed} pages failed</strong>. The failing paths are listed at the top below.`
|
|
1268
|
+
: `All <strong>${warm.total}</strong> pages were warmed in ${(elapsed / 1000).toFixed(1)} seconds, producing <strong>${kb(bytes)}</strong> of HTML in total. The first visitor will not wait for a cold render.`,
|
|
1242
1269
|
)}
|
|
1243
|
-
<h4
|
|
1270
|
+
<h4>Summary<span class="head-action">${copyButton("warm", "copy list")}</span></h4>
|
|
1244
1271
|
${metricGrid([
|
|
1245
|
-
{ label: "
|
|
1246
|
-
{ label: "
|
|
1247
|
-
{ label: "
|
|
1248
|
-
{ label: "
|
|
1249
|
-
{ label: "
|
|
1250
|
-
{ label: "
|
|
1251
|
-
{ label: "
|
|
1252
|
-
{ label: "
|
|
1272
|
+
{ label: "Tried", value: `${warm.done}/${warm.total}`, ratio: warm.done / warm.total, tone: "good" },
|
|
1273
|
+
{ label: "Succeeded", value: String(warm.ok), tone: "good" },
|
|
1274
|
+
{ label: "Failed", value: String(warm.failed), tone: warm.failed ? "bad" : "" },
|
|
1275
|
+
{ label: "Duration", value: `${(elapsed / 1000).toFixed(1)} s` },
|
|
1276
|
+
{ label: "Total HTML", value: kb(bytes) },
|
|
1277
|
+
{ label: "Avg. render", value: ms(average), tone: grade(average, [150, 500]) },
|
|
1278
|
+
{ label: "Slowest", value: ms(slowest?.ms ?? null), tone: grade(slowest?.ms ?? null, [300, 1000]) },
|
|
1279
|
+
{ label: "From cache", value: `${cached}/${entries.length}` },
|
|
1253
1280
|
])}
|
|
1254
1281
|
${
|
|
1255
1282
|
failures.length
|
|
1256
|
-
? `<h4>
|
|
1257
|
-
<button class="btn mini" data-action="warm-retry" ${warm.active ? "disabled" : ""}>${REFRESH_ICON}
|
|
1283
|
+
? `<h4>Errors · ${failures.length}<span class="head-action">
|
|
1284
|
+
<button class="btn mini" data-action="warm-retry" ${warm.active ? "disabled" : ""}>${REFRESH_ICON} Retry</button>
|
|
1258
1285
|
</span></h4>
|
|
1259
1286
|
<div class="rows">${failures.map(warmRow).join("")}</div>`
|
|
1260
1287
|
: ""
|
|
1261
1288
|
}
|
|
1262
|
-
<h4>
|
|
1289
|
+
<h4>Pages tried · ${entries.length}</h4>
|
|
1263
1290
|
${
|
|
1264
1291
|
entries.length
|
|
1265
1292
|
? `<div class="rows">${[...entries]
|
|
1266
1293
|
.sort((a, b) => b.ms - a.ms)
|
|
1267
1294
|
.map(warmRow)
|
|
1268
1295
|
.join("")}</div>`
|
|
1269
|
-
: `<div class="empty">
|
|
1296
|
+
: `<div class="empty">No results yet.</div>`
|
|
1270
1297
|
}
|
|
1271
1298
|
`;
|
|
1272
1299
|
}
|
|
@@ -1278,7 +1305,7 @@ function warmTab() {
|
|
|
1278
1305
|
function warmToolbar(active) {
|
|
1279
1306
|
return `<div class="toolbar">
|
|
1280
1307
|
<button class="btn mini" data-action="warm-run" ${active ? "disabled" : ""}>
|
|
1281
|
-
${REFRESH_ICON} ${active ? "
|
|
1308
|
+
${REFRESH_ICON} ${active ? "Prewarming…" : "Start prewarm from scratch"}
|
|
1282
1309
|
</button>
|
|
1283
1310
|
</div>`;
|
|
1284
1311
|
}
|
|
@@ -1305,145 +1332,145 @@ const ABOUT = {
|
|
|
1305
1332
|
framework: {
|
|
1306
1333
|
label: "Framework",
|
|
1307
1334
|
html: `
|
|
1308
|
-
<p><strong>JSkelet</strong
|
|
1309
|
-
|
|
1310
|
-
|
|
1335
|
+
<p><strong>JSkelet</strong> is a thin layer written without any off-the-shelf
|
|
1336
|
+
meta-framework: server side rendering with EJS on top of Express, island based
|
|
1337
|
+
client code via esbuild and a token driven theme system.</p>
|
|
1311
1338
|
<div class="steps">
|
|
1312
|
-
<div class="step"><span class="n">1</span><span><b>
|
|
1313
|
-
<div class="step"><span class="n">2</span><span><b>
|
|
1314
|
-
<div class="step"><span class="n">3</span><span><b>Build
|
|
1315
|
-
<div class="step"><span class="n">4</span><span><b>Dev
|
|
1316
|
-
<div class="step"><span class="n">5</span><span><b>
|
|
1339
|
+
<div class="step"><span class="n">1</span><span><b>Server render</b><span>Express + EJS produce the page entirely on the server; the result is kept in the HTML cache, so repeat requests return in under a millisecond.</span></span></div>
|
|
1340
|
+
<div class="step"><span class="n">2</span><span><b>Islands</b><span>Every interactive piece is marked with <code>data-island</code> and its module loads once it enters the viewport. The whole page is never hydrated.</span></span></div>
|
|
1341
|
+
<div class="step"><span class="n">3</span><span><b>Build pipeline</b><span>esbuild compiles JS, Tailwind + LightningCSS the styles, sharp the images, Phosphor the icons; the output is hashed and written to <code>manifest.json</code>.</span></span></div>
|
|
1342
|
+
<div class="step"><span class="n">4</span><span><b>Dev loop</b><span>Sources are watched: on a CSS change the stylesheet is swapped, on a JS or server change a single full reload happens.</span></span></div>
|
|
1343
|
+
<div class="step"><span class="n">5</span><span><b>Observability</b><span>This panel collects errors, Web Vitals measurements and server requests; records survive a server restart.</span></span></div>
|
|
1317
1344
|
</div>
|
|
1318
1345
|
<div class="cards">
|
|
1319
|
-
<div class="card"><b
|
|
1320
|
-
<div class="card"><b>
|
|
1321
|
-
<div class="card"><b>
|
|
1346
|
+
<div class="card"><b>Runtime</b><span>Node + Express 5, ESM. Page templates are EJS, the data layer is the services under <code>lib/</code>.</span></div>
|
|
1347
|
+
<div class="card"><b>Target browsers</b><span>Chrome/Edge/Firefox 111+, Safari 16.4+. Modern CSS and ESM are used directly, with no transpile overhead.</span></div>
|
|
1348
|
+
<div class="card"><b>This panel</b><span>Loaded only under <code>npm run dev</code>; the layout prints the script tag in development only, so it never reaches the production output.</span></div>
|
|
1322
1349
|
</div>
|
|
1323
1350
|
`,
|
|
1324
1351
|
},
|
|
1325
1352
|
request: {
|
|
1326
|
-
label: "
|
|
1353
|
+
label: "Request flow",
|
|
1327
1354
|
html: `
|
|
1328
|
-
<p>
|
|
1329
|
-
|
|
1355
|
+
<p>The path a page request follows on the server. Every step lives in its own file
|
|
1356
|
+
under <code>server/</code>; none of them is buried inside another.</p>
|
|
1330
1357
|
<div class="steps">
|
|
1331
|
-
<div class="step"><span class="n">1</span><span><b>Route</b><span><code>server/routes/</code> URL
|
|
1332
|
-
<div class="step"><span class="n">2</span><span><b>Controller</b><span><code>server/controllers/</code>
|
|
1333
|
-
<div class="step"><span class="n">3</span><span><b>Render</b><span><code>server/render.js</code> EJS
|
|
1334
|
-
<div class="step"><span class="n">4</span><span><b>HTML
|
|
1335
|
-
<div class="step"><span class="n">5</span><span><b>
|
|
1358
|
+
<div class="step"><span class="n">1</span><span><b>Route</b><span><code>server/routes/</code> matches the URL and hands the request to the right controller.</span></span></div>
|
|
1359
|
+
<div class="step"><span class="n">2</span><span><b>Controller</b><span><code>server/controllers/</code> gathers the data it needs from the <code>lib/</code> services and prepares the model for the template.</span></span></div>
|
|
1360
|
+
<div class="step"><span class="n">3</span><span><b>Render</b><span><code>server/render.js</code> runs the EJS template and prints manifest entries and head hints into the layout.</span></span></div>
|
|
1361
|
+
<div class="step"><span class="n">4</span><span><b>HTML cache</b><span><code>server/html-cache.js</code> stores the output; if the same URL is requested again the template never runs. The <code>cached</code> label on the Server tab shows this.</span></span></div>
|
|
1362
|
+
<div class="step"><span class="n">5</span><span><b>Response</b><span>Compression and cache headers are decided in <code>server/middleware/</code>; responses like event streams skip compression.</span></span></div>
|
|
1336
1363
|
</div>
|
|
1337
|
-
<p><strong
|
|
1338
|
-
|
|
1339
|
-
|
|
1364
|
+
<p><strong>Prewarming:</strong> <code>server/prewarm.js</code> visits the most travelled
|
|
1365
|
+
routes by itself at startup, so the first real visitor lands on a warm cache.
|
|
1366
|
+
To do the same by hand, use <code>npm run warm</code>.</p>
|
|
1340
1367
|
`,
|
|
1341
1368
|
},
|
|
1342
1369
|
islands: {
|
|
1343
|
-
label: "
|
|
1370
|
+
label: "Islands",
|
|
1344
1371
|
html: `
|
|
1345
|
-
<p
|
|
1346
|
-
<strong>
|
|
1347
|
-
|
|
1372
|
+
<p>Client code is not one huge bundle per page; it is made of
|
|
1373
|
+
<strong>islands</strong> loaded piece by piece. The template partial prints the HTML,
|
|
1374
|
+
the island only adds behaviour.</p>
|
|
1348
1375
|
<div class="steps">
|
|
1349
|
-
<div class="step"><span class="n">1</span><span><b
|
|
1350
|
-
<div class="step"><span class="n">2</span><span><b>
|
|
1351
|
-
<div class="step"><span class="n">3</span><span><b>
|
|
1352
|
-
<div class="step"><span class="n">4</span><span><b>
|
|
1376
|
+
<div class="step"><span class="n">1</span><span><b>Marking</b><span>The EJS partial puts <code>data-island="name"</code> on the root element.</span></span></div>
|
|
1377
|
+
<div class="step"><span class="n">2</span><span><b>Module</b><span>A file with the same name lives under <code>client/islands/</code> and exports the mount function.</span></span></div>
|
|
1378
|
+
<div class="step"><span class="n">3</span><span><b>Loading</b><span><code>client/core/registry.js</code> dynamically imports the module once an IntersectionObserver sees the element approach the viewport.</span></span></div>
|
|
1379
|
+
<div class="step"><span class="n">4</span><span><b>Mounting</b><span>If the mount succeeds, <code>data-island-ready</code> is written on the element. The <b>Island</b> card on the Performance tab shows this ratio.</span></span></div>
|
|
1353
1380
|
</div>
|
|
1354
1381
|
<div class="cards">
|
|
1355
|
-
<div class="card"><b>
|
|
1356
|
-
<div class="card"><b>
|
|
1357
|
-
<div class="card"><b>
|
|
1382
|
+
<div class="card"><b>Why</b><span>There is no need to download JS at first paint for a chart, map or comment block below the fold.</span></div>
|
|
1383
|
+
<div class="card"><b>Error trail</b><span>If an island blows up, the error lands on the <b>Errors</b> tab of this panel with the <code>client</code> source, and the rest of the page keeps working.</span></div>
|
|
1384
|
+
<div class="card"><b>Stubs</b><span><code>build/tasks/island-stubs.mjs</code> catches missing island files; name mismatches show up at build time.</span></div>
|
|
1358
1385
|
</div>
|
|
1359
1386
|
`,
|
|
1360
1387
|
},
|
|
1361
1388
|
build: {
|
|
1362
1389
|
label: "Build",
|
|
1363
1390
|
html: `
|
|
1364
|
-
<p><code>npm run build</code>
|
|
1365
|
-
|
|
1366
|
-
|
|
1391
|
+
<p><code>npm run build</code> produces every asset under <code>build/generated/</code>
|
|
1392
|
+
and writes the hashed file names into <code>manifest.json</code>. Templates never write
|
|
1393
|
+
a file name by hand; they always read it from the manifest.</p>
|
|
1367
1394
|
<div class="rows">
|
|
1368
|
-
<div class="row"><span class="tag">client</span><span class="path">
|
|
1369
|
-
<div class="row"><span class="tag">css</span><span class="path">Tailwind + PostCSS
|
|
1370
|
-
<div class="row"><span class="tag">icons</span><span class="path">Phosphor
|
|
1371
|
-
<div class="row"><span class="tag">fonts</span><span class="path">Font
|
|
1372
|
-
<div class="row"><span class="tag">images</span><span class="path">
|
|
1373
|
-
<div class="row"><span class="tag">precompress</span><span class="path">
|
|
1395
|
+
<div class="row"><span class="tag">client</span><span class="path">Islands and core JS via esbuild; incremental rebundling in watch mode.</span><span class="hint"></span><span></span></div>
|
|
1396
|
+
<div class="row"><span class="tag">css</span><span class="path">Tailwind + PostCSS compilation, then minification and browser downleveling with LightningCSS.</span><span class="hint"></span><span></span></div>
|
|
1397
|
+
<div class="row"><span class="tag">icons</span><span class="path">Only the icons actually used are collected from the Phosphor set and written into a single sprite.</span><span class="hint"></span><span></span></div>
|
|
1398
|
+
<div class="row"><span class="tag">fonts</span><span class="path">Font files are subset and copied, and preload hints are generated.</span><span class="hint"></span><span></span></div>
|
|
1399
|
+
<div class="row"><span class="tag">images</span><span class="path">Size variants and modern formats via sharp.</span><span class="hint"></span><span></span></div>
|
|
1400
|
+
<div class="row"><span class="tag">precompress</span><span class="path">gzip/brotli versions of the static output are prepared ahead of time; the server never compresses per request.</span><span class="hint"></span><span></span></div>
|
|
1374
1401
|
</div>
|
|
1375
|
-
<p><code>
|
|
1376
|
-
|
|
1402
|
+
<p><code>build/ensure-build.mjs</code> runs before <code>npm start</code>: if the output
|
|
1403
|
+
is missing the build triggers itself, so an uncompiled server never comes up.</p>
|
|
1377
1404
|
`,
|
|
1378
1405
|
},
|
|
1379
1406
|
dev: {
|
|
1380
|
-
label: "Dev
|
|
1407
|
+
label: "Dev flow",
|
|
1381
1408
|
html: `
|
|
1382
|
-
<p><code>npm run dev</code>
|
|
1383
|
-
watch
|
|
1384
|
-
|
|
1409
|
+
<p><code>npm run dev</code> is not a single process: <code>scripts/dev.mjs</code> starts
|
|
1410
|
+
the build in watch mode, brings the server up and merges the output of both into one
|
|
1411
|
+
readable stream.</p>
|
|
1385
1412
|
<div class="steps">
|
|
1386
|
-
<div class="step"><span class="n">1</span><span><b
|
|
1387
|
-
<div class="step"><span class="n">2</span><span><b>CSS
|
|
1388
|
-
<div class="step"><span class="n">3</span><span><b>
|
|
1389
|
-
<div class="step"><span class="n">4</span><span><b>
|
|
1413
|
+
<div class="step"><span class="n">1</span><span><b>Watching</b><span>Only <code>server/</code>, <code>lib/</code> and <code>views/</code> are watched. The build output is not watched, so no self-triggering restart loop can form.</span></span></div>
|
|
1414
|
+
<div class="step"><span class="n">2</span><span><b>CSS swap</b><span>A style change does not reload the page; the new stylesheet loads and the old one is removed, keeping scroll position and open panels.</span></span></div>
|
|
1415
|
+
<div class="step"><span class="n">3</span><span><b>Full reload</b><span>A JS or server code change causes a single reload. The same path is taken when the server process id changes.</span></span></div>
|
|
1416
|
+
<div class="step"><span class="n">4</span><span><b>State retention</b><span>The panel's open state and tab live in session storage; server logs are kept outside the process. A restart does not wipe the panel's contents.</span></span></div>
|
|
1390
1417
|
</div>
|
|
1391
1418
|
<div class="cards">
|
|
1392
|
-
<div class="card"><b>Alt+D</b><span>
|
|
1393
|
-
<div class="card"><b>
|
|
1394
|
-
<div class="card"><b>
|
|
1419
|
+
<div class="card"><b>Alt+D</b><span>Opens/closes the panel. <code>Esc</code> closes it, and so does clicking the backdrop.</span></div>
|
|
1420
|
+
<div class="card"><b>Reload page</b><span>Reloads while the panel is open; afterwards the panel comes back on the same tab.</span></div>
|
|
1421
|
+
<div class="card"><b>Clear records</b><span>Resets the browser and server side error/request logs without closing the panel.</span></div>
|
|
1395
1422
|
</div>
|
|
1396
1423
|
`,
|
|
1397
1424
|
},
|
|
1398
1425
|
theme: {
|
|
1399
|
-
label: "
|
|
1426
|
+
label: "Theme & UI",
|
|
1400
1427
|
html: `
|
|
1401
|
-
<p>
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
<code>@theme inline</code>
|
|
1428
|
+
<p>Style decisions come from a single source. Colour, spacing, radius and typography
|
|
1429
|
+
tokens are defined in the <code>:root</code> variables inside
|
|
1430
|
+
<code>styles/theme.css</code>; Tailwind classes are bound to those tokens by the
|
|
1431
|
+
<code>@theme inline</code> block in <code>styles/JSkelet.css</code>.</p>
|
|
1405
1432
|
<div class="rows">
|
|
1406
|
-
<div class="row"><span class="tag">theme.css</span><span class="path">
|
|
1407
|
-
<div class="row"><span class="tag">JSkelet.css</span><span class="path">Tailwind utility
|
|
1408
|
-
<div class="row"><span class="tag">components/ui</span><span class="path">
|
|
1433
|
+
<div class="row"><span class="tag">theme.css</span><span class="path">Tokens and ready-made classes such as <code>.wrapper</code> and <code>.clamp-2</code>.</span><span class="hint"></span><span></span></div>
|
|
1434
|
+
<div class="row"><span class="tag">JSkelet.css</span><span class="path">Tailwind utility mappings and composite classes such as <code>.JSkelet-type-*</code>.</span><span class="hint"></span><span></span></div>
|
|
1435
|
+
<div class="row"><span class="tag">components/ui</span><span class="path">The single home for reusable pieces like buttons, cards and modals.</span><span class="hint"></span><span></span></div>
|
|
1409
1436
|
</div>
|
|
1410
|
-
<p><strong>
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
<code>views/components/ui/</code
|
|
1437
|
+
<p><strong>Rule:</strong> when a new colour or measure is needed, never hardcode a
|
|
1438
|
+
hex/pixel value; add a token first, map it on the Tailwind side if necessary, then use
|
|
1439
|
+
it in the component. A new general purpose UI piece also never leaves
|
|
1440
|
+
<code>views/components/ui/</code>.</p>
|
|
1414
1441
|
`,
|
|
1415
1442
|
},
|
|
1416
1443
|
commands: {
|
|
1417
|
-
label: "
|
|
1444
|
+
label: "Commands",
|
|
1418
1445
|
html: `
|
|
1419
|
-
<p>
|
|
1420
|
-
|
|
1446
|
+
<p>Commonly used npm scripts. The verification scripts check against real output
|
|
1447
|
+
whether specific pages contain the pieces they are expected to.</p>
|
|
1421
1448
|
<div class="rows">
|
|
1422
|
-
<div class="row"><span class="tag">dev</span><span class="path">Build watch +
|
|
1423
|
-
<div class="row"><span class="tag">build</span><span class="path"
|
|
1424
|
-
<div class="row"><span class="tag">start</span><span class="path"
|
|
1425
|
-
<div class="row"><span class="tag">lint</span><span class="path">ESLint.
|
|
1426
|
-
<div class="row"><span class="tag">warm</span><span class="path"
|
|
1427
|
-
<div class="row"><span class="tag">perf</span><span class="path">
|
|
1428
|
-
<div class="row"><span class="tag">verify:*</span><span class="path">
|
|
1449
|
+
<div class="row"><span class="tag">dev</span><span class="path">Build watch + server, together with this panel.</span><span class="hint"></span><span></span></div>
|
|
1450
|
+
<div class="row"><span class="tag">build</span><span class="path">Production output: JS, CSS, icons, fonts, images, precompression.</span><span class="hint"></span><span></span></div>
|
|
1451
|
+
<div class="row"><span class="tag">start</span><span class="path">Production server; a missing build is completed first.</span><span class="hint"></span><span></span></div>
|
|
1452
|
+
<div class="row"><span class="tag">lint</span><span class="path">ESLint. This is the default verification path after a change; a full build is not required.</span><span class="hint"></span><span></span></div>
|
|
1453
|
+
<div class="row"><span class="tag">warm</span><span class="path">Warms the cache by hand.</span><span class="hint"></span><span></span></div>
|
|
1454
|
+
<div class="row"><span class="tag">perf</span><span class="path">Produces a performance audit report.</span><span class="hint"></span><span></span></div>
|
|
1455
|
+
<div class="row"><span class="tag">verify:*</span><span class="path">Targeted checks for route smoke, head tags, charts, comments, stock tabs, the news feed and home page widgets.</span><span class="hint"></span><span></span></div>
|
|
1429
1456
|
</div>
|
|
1430
1457
|
`,
|
|
1431
1458
|
},
|
|
1432
1459
|
layout: {
|
|
1433
|
-
label: "
|
|
1460
|
+
label: "Folders",
|
|
1434
1461
|
html: `
|
|
1435
|
-
<p>
|
|
1462
|
+
<p>A short map so you know where to look for something.</p>
|
|
1436
1463
|
<div class="rows">
|
|
1437
|
-
<div class="row"><span class="tag">server/</span><span class="path">Express
|
|
1438
|
-
<div class="row"><span class="tag">views/</span><span class="path">EJS layout
|
|
1439
|
-
<div class="row"><span class="tag">client/</span><span class="path">
|
|
1440
|
-
<div class="row"><span class="tag">lib/</span><span class="path">
|
|
1441
|
-
<div class="row"><span class="tag">app/</span><span class="path">
|
|
1442
|
-
<div class="row"><span class="tag">build/</span><span class="path">
|
|
1443
|
-
<div class="row"><span class="tag">framework/</span><span class="path">
|
|
1444
|
-
<div class="row"><span class="tag">scripts/</span><span class="path">Dev
|
|
1445
|
-
<div class="row"><span class="tag">public/</span><span class="path">
|
|
1446
|
-
<div class="row"><span class="tag">docs/</span><span class="path">
|
|
1464
|
+
<div class="row"><span class="tag">server/</span><span class="path">The Express application: routes, controllers, middleware, render and cache.</span><span class="hint"></span><span></span></div>
|
|
1465
|
+
<div class="row"><span class="tag">views/</span><span class="path">The EJS layout, pages and partials. The single source of HTML.</span><span class="hint"></span><span></span></div>
|
|
1466
|
+
<div class="row"><span class="tag">client/</span><span class="path">Browser code: <code>core/</code> island infrastructure, <code>islands/</code> behaviours, <code>devtools/</code> this panel.</span><span class="hint"></span><span></span></div>
|
|
1467
|
+
<div class="row"><span class="tag">lib/</span><span class="path">Data services and domain logic; API calls, transforms, formatters.</span><span class="hint"></span><span></span></div>
|
|
1468
|
+
<div class="row"><span class="tag">app/</span><span class="path">Theme files and shared UI components.</span><span class="hint"></span><span></span></div>
|
|
1469
|
+
<div class="row"><span class="tag">build/</span><span class="path">Build scripts (<code>tasks/</code>) and the generated output (<code>generated/</code>).</span><span class="hint"></span><span></span></div>
|
|
1470
|
+
<div class="row"><span class="tag">framework/</span><span class="path">Helpers shared by the build and dev processes, including the terminal logger.</span><span class="hint"></span><span></span></div>
|
|
1471
|
+
<div class="row"><span class="tag">scripts/</span><span class="path">Dev orchestration and verification/maintenance tools.</span><span class="hint"></span><span></span></div>
|
|
1472
|
+
<div class="row"><span class="tag">public/</span><span class="path">Static files served directly.</span><span class="hint"></span><span></span></div>
|
|
1473
|
+
<div class="row"><span class="tag">docs/</span><span class="path">Long form notes and decision records.</span><span class="hint"></span><span></span></div>
|
|
1447
1474
|
</div>
|
|
1448
1475
|
`,
|
|
1449
1476
|
},
|
|
@@ -1457,8 +1484,8 @@ function aboutTab() {
|
|
|
1457
1484
|
return `
|
|
1458
1485
|
${lede(
|
|
1459
1486
|
"good",
|
|
1460
|
-
`JSkelet
|
|
1461
|
-
`
|
|
1487
|
+
`The JSkelet front end runs on its own thin framework: <strong>HTML on the server</strong>, ` +
|
|
1488
|
+
`only the necessary <strong>islands</strong> in the browser. This tab is the short documentation inside the panel.`,
|
|
1462
1489
|
)}
|
|
1463
1490
|
<div class="subtabs">
|
|
1464
1491
|
${Object.entries(ABOUT)
|
|
@@ -1519,9 +1546,9 @@ function paintPrewarm(root) {
|
|
|
1519
1546
|
|
|
1520
1547
|
chip.hidden = false;
|
|
1521
1548
|
chip.className = `warm ${warm.active ? "" : "done"}`;
|
|
1522
|
-
chip.title =
|
|
1523
|
-
warm.failed ? `, ${warm.failed}
|
|
1524
|
-
} / ${warm.total}
|
|
1549
|
+
chip.title = `Cache prewarming: ${warm.ok} succeeded${
|
|
1550
|
+
warm.failed ? `, ${warm.failed} failed` : ""
|
|
1551
|
+
} / ${warm.total} pages`;
|
|
1525
1552
|
chip.querySelector("[data-part='warm-text']").textContent = warm.active
|
|
1526
1553
|
? `${warm.done}/${warm.total} prewarmed`
|
|
1527
1554
|
: `${warm.ok}/${warm.total} prewarmed`;
|
|
@@ -1541,12 +1568,12 @@ function paint() {
|
|
|
1541
1568
|
|
|
1542
1569
|
const tone = errorCount ? "bad" : warnCount ? "warn" : "";
|
|
1543
1570
|
const badgeCount = errorCount || warnCount;
|
|
1544
|
-
const lcpLabel = metrics.lcp ? `${Math.round(metrics.lcp)} ms` : "
|
|
1571
|
+
const lcpLabel = metrics.lcp ? `${Math.round(metrics.lcp)} ms` : "measuring";
|
|
1545
1572
|
|
|
1546
1573
|
const bubble = root.querySelector(".bubble");
|
|
1547
1574
|
bubble.className = `bubble ${tone}`;
|
|
1548
|
-
bubble.title = `JSkelet dev
|
|
1549
|
-
badgeCount ? `, ${errorCount}
|
|
1575
|
+
bubble.title = `JSkelet dev tools — LCP ${lcpLabel}${
|
|
1576
|
+
badgeCount ? `, ${errorCount} errors, ${warnCount} warnings` : ""
|
|
1550
1577
|
}`;
|
|
1551
1578
|
|
|
1552
1579
|
const badge = root.querySelector(".badge");
|
|
@@ -1563,15 +1590,20 @@ function paint() {
|
|
|
1563
1590
|
const pill = root.querySelector(".pill");
|
|
1564
1591
|
pill.className = `pill ${offline ? "warn" : tone}`;
|
|
1565
1592
|
pill.querySelector("[data-part='pill']").textContent = offline
|
|
1566
|
-
? "
|
|
1593
|
+
? "server restarting…"
|
|
1567
1594
|
: errorCount || warnCount
|
|
1568
|
-
? `${errorCount}
|
|
1569
|
-
: "
|
|
1595
|
+
? `${errorCount} errors · ${warnCount} warnings`
|
|
1596
|
+
: "all good";
|
|
1570
1597
|
|
|
1571
1598
|
const chip = root.querySelector("[data-part='chip']");
|
|
1572
1599
|
chip.hidden = !errorCount;
|
|
1573
1600
|
chip.textContent = String(errorCount);
|
|
1574
1601
|
|
|
1602
|
+
const version = serverStats?.version;
|
|
1603
|
+
const versionChip = root.querySelector("[data-part='version-chip']");
|
|
1604
|
+
versionChip.hidden = !version?.outdated;
|
|
1605
|
+
if (version?.outdated) versionChip.title = `JSkelet ${version.latest} is available`;
|
|
1606
|
+
|
|
1575
1607
|
const warm = serverStats?.prewarm;
|
|
1576
1608
|
const warmChip = root.querySelector("[data-part='warm-chip']");
|
|
1577
1609
|
warmChip.hidden = !warm?.total;
|