jskelet 0.2.2 → 0.2.4
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/CHANGELOG.md +375 -315
- package/docs/03-routing.md +4 -0
- package/docs/06-cache.md +154 -10
- package/docs/07-yapilandirma.md +63 -2
- package/docs/en/03-routing.md +4 -0
- package/docs/en/06-caching.md +154 -11
- package/docs/en/07-configuration.md +65 -2
- package/package.json +1 -1
- package/src/client/cache-panel/i18n.js +670 -0
- package/src/client/cache-panel/login.html +23 -12
- package/src/client/cache-panel/panel.css +314 -6
- package/src/client/cache-panel/panel.html +205 -30
- package/src/client/cache-panel/panel.js +605 -29
- package/src/config/defaults.js +31 -0
- package/src/config/index.js +87 -2
- package/src/index.js +16 -1
- package/src/log.mjs +25 -0
- package/src/server/cache-panel.js +294 -40
- package/src/server/cloudflare.js +595 -0
- package/src/server/redis.js +108 -0
- package/src/server/render.js +74 -2
- package/src/version.mjs +9 -0
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Yenileme WebSocket değil, kısa aralıklı `fetch` ile: panel nadiren ve kısa
|
|
9
9
|
* süre açık kalıyor, kalıcı bir kanal açmanın karşılığı yok.
|
|
10
|
+
*
|
|
11
|
+
* Bütün görünen metin `i18n.js` üzerinden geçiyor; sunucudan gelen cevaplar da
|
|
12
|
+
* metin değil `code` taşıyor, yani dil değişimi tek yerden hallediliyor.
|
|
10
13
|
*/
|
|
14
|
+
import { applyTranslations, languageSelect, t } from "./i18n.js";
|
|
11
15
|
|
|
12
16
|
const REFRESH_MS = 3000;
|
|
13
17
|
|
|
@@ -42,10 +46,14 @@ function formatBytes(bytes) {
|
|
|
42
46
|
*/
|
|
43
47
|
function formatDuration(seconds) {
|
|
44
48
|
const total = Math.max(0, Math.round(seconds));
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (total <
|
|
48
|
-
return `${Math.floor(total /
|
|
49
|
+
const [s, m, h, d] = [t("unit.second"), t("unit.minute"), t("unit.hour"), t("unit.day")];
|
|
50
|
+
|
|
51
|
+
if (total < 60) return `${total}${s}`;
|
|
52
|
+
if (total < 3600) return `${Math.floor(total / 60)}${m} ${total % 60}${s}`;
|
|
53
|
+
if (total < 86_400) {
|
|
54
|
+
return `${Math.floor(total / 3600)}${h} ${Math.floor((total % 3600) / 60)}${m}`;
|
|
55
|
+
}
|
|
56
|
+
return `${Math.floor(total / 86_400)}${d} ${Math.floor((total % 86_400) / 3600)}${h}`;
|
|
49
57
|
}
|
|
50
58
|
|
|
51
59
|
/**
|
|
@@ -117,9 +125,37 @@ async function act(body) {
|
|
|
117
125
|
|
|
118
126
|
if (!alive(response)) return;
|
|
119
127
|
|
|
120
|
-
const result = await response.json().catch(() =>
|
|
121
|
-
toast(
|
|
128
|
+
const result = await response.json().catch(() => null);
|
|
129
|
+
toast(describe(result), result?.ok !== false);
|
|
122
130
|
await load();
|
|
131
|
+
|
|
132
|
+
// Cloudflare ayarları değişmiş olabilir; genel bakış önbelleği atlanır.
|
|
133
|
+
if (String(body.type ?? "").startsWith("cf:")) await loadCloudflare(true);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Sunucu cevabını metne çevirir.
|
|
138
|
+
*
|
|
139
|
+
* `/action` dil taşımıyor: `{ ok, code, params }` dönüyor ve cümle burada
|
|
140
|
+
* kuruluyor. Redis sayımı gibi parçalı cevaplar `parts` ile geliyor, çünkü
|
|
141
|
+
* "veritabanında kaç anahtar var" bilgisi her zaman okunamıyor ve eksik parçayı
|
|
142
|
+
* cümleden düşürmek tek düzgün yol.
|
|
143
|
+
*
|
|
144
|
+
* @param {any} result
|
|
145
|
+
* @returns {string}
|
|
146
|
+
*/
|
|
147
|
+
function describe(result) {
|
|
148
|
+
if (!result) return t("toast.failed");
|
|
149
|
+
|
|
150
|
+
if (Array.isArray(result.parts)) {
|
|
151
|
+
const parts = result.parts
|
|
152
|
+
.map((/** @type {any} */ part) => t(`msg.${part.code}`, part.params))
|
|
153
|
+
.filter(Boolean);
|
|
154
|
+
if (parts.length) return parts.join(" · ");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (result.code) return t(`msg.${result.code}`, result.params);
|
|
158
|
+
return t(result.ok ? "toast.done" : "toast.failed");
|
|
123
159
|
}
|
|
124
160
|
|
|
125
161
|
/* ------------------------------------------------------------------- çizim */
|
|
@@ -127,20 +163,33 @@ async function act(body) {
|
|
|
127
163
|
function render() {
|
|
128
164
|
if (!latest) return;
|
|
129
165
|
|
|
130
|
-
const { process: proc, html, data, redis, prewarm } = latest;
|
|
166
|
+
const { process: proc, release, host, html, data, redis, prewarm } = latest;
|
|
167
|
+
|
|
168
|
+
$("version").textContent = `v${release.version}`;
|
|
169
|
+
$("foot-release").textContent = [
|
|
170
|
+
`v${release.version}`,
|
|
171
|
+
release.license,
|
|
172
|
+
release.node ? `Node ${release.node}` : null,
|
|
173
|
+
]
|
|
174
|
+
.filter(Boolean)
|
|
175
|
+
.join(" · ");
|
|
176
|
+
if (release.homepage) $("foot-repo").href = release.homepage;
|
|
131
177
|
|
|
132
178
|
$("env").textContent = proc.env;
|
|
133
179
|
$("pid").textContent = `pid ${proc.pid}`;
|
|
134
|
-
$("uptime").textContent =
|
|
180
|
+
$("uptime").textContent = t("chip.up", { value: formatDuration(proc.uptime) });
|
|
135
181
|
$("memory").textContent = `rss ${formatBytes(proc.memory.rss)}`;
|
|
136
182
|
|
|
137
183
|
$("html-size").textContent = html.size.toLocaleString();
|
|
138
|
-
$("html-sub").textContent =
|
|
184
|
+
$("html-sub").textContent = t("card.limit", { count: html.maxEntries.toLocaleString() });
|
|
139
185
|
$("html-bytes").textContent = formatBytes(html.bytes);
|
|
140
|
-
$("html-stale").textContent =
|
|
186
|
+
$("html-stale").textContent = t("card.stale", { count: html.stale });
|
|
141
187
|
|
|
142
188
|
$("data-size").textContent = data.size.toLocaleString();
|
|
143
|
-
$("data-sub").textContent =
|
|
189
|
+
$("data-sub").textContent = t("card.staleAndLimit", {
|
|
190
|
+
stale: data.stale,
|
|
191
|
+
limit: data.maxEntries.toLocaleString(),
|
|
192
|
+
});
|
|
144
193
|
|
|
145
194
|
const redisState = !redis.enabled
|
|
146
195
|
? "off"
|
|
@@ -150,23 +199,398 @@ function render() {
|
|
|
150
199
|
? "connected"
|
|
151
200
|
: "disconnected";
|
|
152
201
|
|
|
153
|
-
$("redis-state").textContent = redisState;
|
|
202
|
+
$("redis-state").textContent = t(`state.${redisState}`);
|
|
154
203
|
$("redis-sub").textContent = redis.enabled
|
|
155
|
-
?
|
|
156
|
-
: "
|
|
204
|
+
? t("redis.sub", { address: redis.address, errors: redis.errors })
|
|
205
|
+
: t("redis.disabled");
|
|
206
|
+
|
|
207
|
+
renderRedis(redis, redisState);
|
|
208
|
+
renderHost(host, proc, redis);
|
|
157
209
|
|
|
158
210
|
const done = prewarm.done ?? 0;
|
|
159
211
|
const total = prewarm.total ?? 0;
|
|
160
|
-
$("prewarm-state").textContent = prewarm.active ? `${done}/${total}` : "idle";
|
|
212
|
+
$("prewarm-state").textContent = prewarm.active ? `${done}/${total}` : t("state.idle");
|
|
161
213
|
$("prewarm-sub").textContent = prewarm.active
|
|
162
|
-
? "
|
|
214
|
+
? t("prewarm.inProgress")
|
|
163
215
|
: total
|
|
164
|
-
?
|
|
165
|
-
: "
|
|
216
|
+
? t("prewarm.lastRound", { done, total })
|
|
217
|
+
: t("prewarm.never");
|
|
166
218
|
|
|
167
219
|
renderTable();
|
|
168
220
|
}
|
|
169
221
|
|
|
222
|
+
/* ------------------------------------------------------------- cloudflare */
|
|
223
|
+
|
|
224
|
+
/** @type {any} */
|
|
225
|
+
let cloudflare = null;
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Cloudflare genel bakışı ayrı bir uçtan gelir: zone bilgisi ve ayarlar ağa
|
|
229
|
+
* çıkıyor, döküm turu ise saniyeler mertebesinde yenileniyor. Bu yüzden
|
|
230
|
+
* yalnızca açılışta ve bir işlemden sonra istenir.
|
|
231
|
+
*
|
|
232
|
+
* @param {boolean} [force]
|
|
233
|
+
*/
|
|
234
|
+
async function loadCloudflare(force = false) {
|
|
235
|
+
const response = await fetch(`cloudflare${force ? "?force=1" : ""}`);
|
|
236
|
+
if (!alive(response)) return;
|
|
237
|
+
|
|
238
|
+
cloudflare = await response.json().catch(() => null);
|
|
239
|
+
renderCloudflare();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function renderCloudflare() {
|
|
243
|
+
if (!cloudflare) return;
|
|
244
|
+
|
|
245
|
+
const ok = cloudflare.ok === true;
|
|
246
|
+
const status = cloudflare.status ?? {};
|
|
247
|
+
const settings = cloudflare.settings ?? {};
|
|
248
|
+
const zone = cloudflare.zone ?? null;
|
|
249
|
+
|
|
250
|
+
$("cf-heading").textContent = ok
|
|
251
|
+
? [zone?.name, zone?.plan].filter(Boolean).join(" · ") || t("state.connected")
|
|
252
|
+
: status.configured
|
|
253
|
+
? t("cf.error", { error: cloudflare.error })
|
|
254
|
+
: t("cf.notConnected");
|
|
255
|
+
|
|
256
|
+
$("cf-tools").hidden = !ok;
|
|
257
|
+
$("cf-suggest").hidden = ok || status.configured;
|
|
258
|
+
$("cf-refresh").hidden = !status.configured;
|
|
259
|
+
|
|
260
|
+
if (!ok) {
|
|
261
|
+
$("cf-kv").replaceChildren();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const development = settings.developmentMode === "on";
|
|
266
|
+
$("cf-dev").checked = development;
|
|
267
|
+
|
|
268
|
+
/** @type {[string, string, string?][]} */
|
|
269
|
+
const rows = [
|
|
270
|
+
[t("cf.zone"), `${zone?.name ?? "—"} (${status.zoneId ?? "?"})`],
|
|
271
|
+
[t("cf.plan"), zone?.plan ?? "—"],
|
|
272
|
+
[
|
|
273
|
+
t("cf.token"),
|
|
274
|
+
status.tokenSource === "env" ? t("cf.token.env") : t("cf.token.config"),
|
|
275
|
+
"on",
|
|
276
|
+
],
|
|
277
|
+
[
|
|
278
|
+
t("cf.devModeLabel"),
|
|
279
|
+
development
|
|
280
|
+
? t("cf.devMode.on", { time: formatDuration(settings.developmentModeRemaining) })
|
|
281
|
+
: t("state.off"),
|
|
282
|
+
development ? "bad" : "on",
|
|
283
|
+
],
|
|
284
|
+
[t("cf.cacheLevel"), String(settings.cacheLevel ?? "—")],
|
|
285
|
+
[t("cf.browserTtl"), ttl(settings.browserCacheTtl)],
|
|
286
|
+
[t("cf.edgeTtl"), ttl(settings.edgeCacheTtl)],
|
|
287
|
+
[t("cf.sortQuery"), String(settings.sortQueryString ?? "—")],
|
|
288
|
+
[t("cf.alwaysOnline"), String(settings.alwaysOnline ?? "—")],
|
|
289
|
+
...feature(t("cf.tiered"), settings.tieredCaching),
|
|
290
|
+
...feature(t("cf.regionalTiered"), settings.regionalTieredCache),
|
|
291
|
+
...feature(t("cf.reserve"), settings.cacheReserve),
|
|
292
|
+
];
|
|
293
|
+
|
|
294
|
+
$("cf-kv").replaceChildren(...rows.map(kvRow));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Plan dışı kalan bir özellik `null` döner. "off" yazmak yanıltıcı olurdu:
|
|
299
|
+
* kapatılmış değil, o planda hiç yok.
|
|
300
|
+
*
|
|
301
|
+
* @param {string} label
|
|
302
|
+
* @param {string | null} value
|
|
303
|
+
* @returns {[string, string, string?][]}
|
|
304
|
+
*/
|
|
305
|
+
function feature(label, value) {
|
|
306
|
+
if (value === null || value === undefined) {
|
|
307
|
+
return [[label, t("cf.unavailablePlan"), "off"]];
|
|
308
|
+
}
|
|
309
|
+
const on = value === "on";
|
|
310
|
+
return [[label, on ? t("state.on") : t("state.off"), on ? "on" : "off"]];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* @param {unknown} seconds
|
|
315
|
+
* @returns {string}
|
|
316
|
+
*/
|
|
317
|
+
function ttl(seconds) {
|
|
318
|
+
const value = Number(seconds);
|
|
319
|
+
if (!Number.isFinite(value)) return "—";
|
|
320
|
+
// Cloudflare 0'ı "Respect Existing Headers" olarak kullanıyor.
|
|
321
|
+
if (value === 0) return t("cf.respectOrigin");
|
|
322
|
+
return formatDuration(value);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* @param {[string, string, string?]} row
|
|
327
|
+
* @returns {HTMLDivElement}
|
|
328
|
+
*/
|
|
329
|
+
function kvRow([label, value, tone]) {
|
|
330
|
+
const wrap = document.createElement("div");
|
|
331
|
+
const dt = document.createElement("dt");
|
|
332
|
+
dt.textContent = label;
|
|
333
|
+
const dd = document.createElement("dd");
|
|
334
|
+
if (tone) dd.className = tone;
|
|
335
|
+
dd.textContent = value;
|
|
336
|
+
wrap.append(dt, dd);
|
|
337
|
+
return wrap;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Analitik dökümü. İki soru tek uçtan cevaplanıyor: yol verilirse kolo
|
|
342
|
+
* kırılımı, verilmezse zone genelinde cache durumu dağılımı.
|
|
343
|
+
*/
|
|
344
|
+
async function loadCloudflareAnalytics() {
|
|
345
|
+
const path = $("cf-path").value.trim();
|
|
346
|
+
const target = $("cf-report");
|
|
347
|
+
|
|
348
|
+
target.hidden = false;
|
|
349
|
+
target.textContent = t("cf.querying");
|
|
350
|
+
|
|
351
|
+
const response = await fetch("cloudflare/analytics", {
|
|
352
|
+
method: "POST",
|
|
353
|
+
headers: {
|
|
354
|
+
"Content-Type": "application/json",
|
|
355
|
+
"X-JSkelet-Cache-Panel": "1",
|
|
356
|
+
},
|
|
357
|
+
body: JSON.stringify({ path }),
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
if (!alive(response)) return;
|
|
361
|
+
|
|
362
|
+
const result = await response.json().catch(() => null);
|
|
363
|
+
|
|
364
|
+
if (!result?.ok) {
|
|
365
|
+
target.textContent = t("msg.cf.failed", { error: result?.error ?? t("cf.queryFailed") });
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Son dökümü dil değişiminde yeniden kurmak için saklanır: tablo ağdan
|
|
370
|
+
// geliyor ve dil değişti diye Cloudflare'e yeniden gitmek gereksiz.
|
|
371
|
+
lastReport = result;
|
|
372
|
+
renderReport();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** @type {any} */
|
|
376
|
+
let lastReport = null;
|
|
377
|
+
|
|
378
|
+
function renderReport() {
|
|
379
|
+
if (!lastReport) return;
|
|
380
|
+
const result = lastReport;
|
|
381
|
+
|
|
382
|
+
$("cf-report").replaceChildren(
|
|
383
|
+
result.colos ? edgeTable(result) : statusTable(result),
|
|
384
|
+
note(
|
|
385
|
+
result.colos
|
|
386
|
+
? t("cf.edgeNote", {
|
|
387
|
+
colos: result.colos.length,
|
|
388
|
+
path: result.path,
|
|
389
|
+
hours: result.hours,
|
|
390
|
+
hits: result.hits,
|
|
391
|
+
misses: result.misses,
|
|
392
|
+
})
|
|
393
|
+
: t("cf.zoneNote", { hours: result.hours }),
|
|
394
|
+
),
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* @param {any} result
|
|
400
|
+
* @returns {HTMLTableElement}
|
|
401
|
+
*/
|
|
402
|
+
function edgeTable(result) {
|
|
403
|
+
return buildTable(
|
|
404
|
+
[t("cf.colo"), t("cf.fromCache"), t("cf.toOrigin")],
|
|
405
|
+
result.colos.map((/** @type {any} */ row) => [
|
|
406
|
+
row.colo,
|
|
407
|
+
String(row.hits),
|
|
408
|
+
String(row.misses),
|
|
409
|
+
]),
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* @param {any} result
|
|
415
|
+
* @returns {HTMLTableElement}
|
|
416
|
+
*/
|
|
417
|
+
function statusTable(result) {
|
|
418
|
+
const total = result.rows.reduce((sum, /** @type {any} */ row) => sum + row.requests, 0) || 1;
|
|
419
|
+
|
|
420
|
+
return buildTable(
|
|
421
|
+
[t("cf.cacheStatus"), t("cf.requests"), t("cf.share"), t("cf.edgeBytes")],
|
|
422
|
+
result.rows.map((/** @type {any} */ row) => [
|
|
423
|
+
row.status,
|
|
424
|
+
row.requests.toLocaleString(),
|
|
425
|
+
`${Math.round((row.requests / total) * 100)}%`,
|
|
426
|
+
formatBytes(row.bytes),
|
|
427
|
+
]),
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* @param {string[]} headers
|
|
433
|
+
* @param {string[][]} rows
|
|
434
|
+
* @returns {HTMLTableElement}
|
|
435
|
+
*/
|
|
436
|
+
function buildTable(headers, rows) {
|
|
437
|
+
const table = document.createElement("table");
|
|
438
|
+
const thead = document.createElement("thead");
|
|
439
|
+
const headRow = document.createElement("tr");
|
|
440
|
+
|
|
441
|
+
headers.forEach((label, index) => {
|
|
442
|
+
const th = document.createElement("th");
|
|
443
|
+
th.textContent = label;
|
|
444
|
+
if (index > 0) th.className = "num";
|
|
445
|
+
headRow.append(th);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
thead.append(headRow);
|
|
449
|
+
|
|
450
|
+
const tbody = document.createElement("tbody");
|
|
451
|
+
for (const row of rows) {
|
|
452
|
+
const tr = document.createElement("tr");
|
|
453
|
+
row.forEach((value, index) => tr.append(cell(value, index > 0 ? "num" : undefined)));
|
|
454
|
+
tbody.append(tr);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
table.append(thead, tbody);
|
|
458
|
+
return table;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* @param {string} text
|
|
463
|
+
* @returns {HTMLParagraphElement}
|
|
464
|
+
*/
|
|
465
|
+
function note(text) {
|
|
466
|
+
const paragraph = document.createElement("p");
|
|
467
|
+
paragraph.className = "note m0";
|
|
468
|
+
paragraph.textContent = text;
|
|
469
|
+
return paragraph;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Paylaşımlı kademenin künyesi.
|
|
474
|
+
*
|
|
475
|
+
* "Bağlı mı" tek başına yetmiyor: yanlış `namespace` ya da beklenmeyen bir
|
|
476
|
+
* `buildId` ile bağlı bir Redis de hiçbir şey paylaşmıyor gibi görünür. Bu
|
|
477
|
+
* yüzden anahtarın **nerede** durduğu ve hangi türlerin paylaşıldığı da
|
|
478
|
+
* yazılır.
|
|
479
|
+
*
|
|
480
|
+
* @param {any} redis
|
|
481
|
+
* @param {string} state
|
|
482
|
+
*/
|
|
483
|
+
function renderRedis(redis, state) {
|
|
484
|
+
$("redis-heading").textContent = redis.enabled ? t(`state.${state}`) : t("state.notConfigured");
|
|
485
|
+
$("redis-actions").hidden = !redis.enabled;
|
|
486
|
+
$("redis-suggest").hidden = redis.enabled;
|
|
487
|
+
|
|
488
|
+
/** @type {[string, string, string?][]} */
|
|
489
|
+
const rows = redis.enabled
|
|
490
|
+
? [
|
|
491
|
+
[
|
|
492
|
+
t("redis.connection"),
|
|
493
|
+
t(`state.${state}`),
|
|
494
|
+
redis.connected && !redis.bypassed ? "on" : "bad",
|
|
495
|
+
],
|
|
496
|
+
[t("redis.address"), `${redis.address}${redis.secure ? " (TLS)" : ""}`],
|
|
497
|
+
[t("redis.database"), redis.db ?? t("state.default")],
|
|
498
|
+
[t("redis.keyPrefix"), `${redis.keyPrefix}:${redis.namespace}`],
|
|
499
|
+
[t("redis.buildId"), redis.buildId || "dev"],
|
|
500
|
+
[
|
|
501
|
+
t("redis.sharesHtml"),
|
|
502
|
+
redis.html ? t("state.yes") : t("state.no"),
|
|
503
|
+
redis.html ? "on" : "off",
|
|
504
|
+
],
|
|
505
|
+
[
|
|
506
|
+
t("redis.sharesData"),
|
|
507
|
+
redis.data ? t("state.yes") : t("state.no"),
|
|
508
|
+
redis.data ? "on" : "off",
|
|
509
|
+
],
|
|
510
|
+
[
|
|
511
|
+
t("redis.compressed"),
|
|
512
|
+
redis.storeEncoded ? t("redis.compressed.shared") : t("redis.compressed.local"),
|
|
513
|
+
redis.storeEncoded ? "on" : "off",
|
|
514
|
+
],
|
|
515
|
+
[
|
|
516
|
+
t("redis.broadcast"),
|
|
517
|
+
redis.events
|
|
518
|
+
? redis.subscribed
|
|
519
|
+
? t("redis.broadcast.subscribed")
|
|
520
|
+
: t("redis.broadcast.publish")
|
|
521
|
+
: t("state.off"),
|
|
522
|
+
redis.events && redis.subscribed ? "on" : "off",
|
|
523
|
+
],
|
|
524
|
+
[t("redis.timeout"), `${redis.commandTimeoutMs} ${t("unit.ms")}`],
|
|
525
|
+
[t("redis.errors"), String(redis.errors), redis.errors ? "bad" : "on"],
|
|
526
|
+
]
|
|
527
|
+
: [
|
|
528
|
+
[t("redis.tier"), t("redis.tier.inProcess")],
|
|
529
|
+
[t("redis.replicas"), t("state.none")],
|
|
530
|
+
[t("redis.broadcast"), t("redis.broadcast.local"), "off"],
|
|
531
|
+
];
|
|
532
|
+
|
|
533
|
+
$("redis-kv").replaceChildren(...rows.map(kvRow));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Makinenin RAM ve disk durumu. Paylaşımlı kademe kapalıyken önbelleğin
|
|
538
|
+
* tamamı bu sürecin belleğinde yaşıyor; `maxEntries` ile RAM arasındaki
|
|
539
|
+
* ilişkiyi görmeden ayar yapmak körlemesine oluyor.
|
|
540
|
+
*
|
|
541
|
+
* @param {any} host
|
|
542
|
+
* @param {any} proc
|
|
543
|
+
* @param {any} redis
|
|
544
|
+
*/
|
|
545
|
+
function renderHost(host, proc, redis) {
|
|
546
|
+
$("host-heading").textContent = [
|
|
547
|
+
host.platform,
|
|
548
|
+
`${host.cpus} cpu`,
|
|
549
|
+
host.load ? `load ${host.load.toFixed(2)}` : null,
|
|
550
|
+
]
|
|
551
|
+
.filter(Boolean)
|
|
552
|
+
.join(" · ");
|
|
553
|
+
|
|
554
|
+
const ramUsed = host.memory.used / host.memory.total;
|
|
555
|
+
$("ram-text").textContent =
|
|
556
|
+
`${formatBytes(host.memory.used)} / ${formatBytes(host.memory.total)}`;
|
|
557
|
+
meter("ram-bar", ramUsed);
|
|
558
|
+
$("ram-note").textContent = redis.connected
|
|
559
|
+
? t("host.ramShared", { rss: formatBytes(proc.memory.rss) })
|
|
560
|
+
: t("host.ramOnly", {
|
|
561
|
+
rss: formatBytes(proc.memory.rss),
|
|
562
|
+
html: formatBytes(latest.html.bytes),
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
if (!host.disk) {
|
|
566
|
+
$("disk-text").textContent = t("state.unavailable");
|
|
567
|
+
meter("disk-bar", 0);
|
|
568
|
+
$("disk-note").textContent = t("host.noStats");
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const diskUsed = (host.disk.total - host.disk.free) / host.disk.total;
|
|
573
|
+
$("disk-text").textContent =
|
|
574
|
+
`${formatBytes(host.disk.total - host.disk.free)} / ${formatBytes(host.disk.total)}`;
|
|
575
|
+
meter("disk-bar", diskUsed);
|
|
576
|
+
// Önbellek diske yazılmıyor; disk build çıktısı ve log için önemli.
|
|
577
|
+
$("disk-note").textContent = t("host.diskNote", {
|
|
578
|
+
free: formatBytes(host.disk.free),
|
|
579
|
+
path: host.disk.path,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* @param {string} id
|
|
585
|
+
* @param {number} ratio 0–1
|
|
586
|
+
*/
|
|
587
|
+
function meter(id, ratio) {
|
|
588
|
+
const bar = $(id);
|
|
589
|
+
const percent = Math.max(0, Math.min(100, ratio * 100));
|
|
590
|
+
bar.style.width = `${percent}%`;
|
|
591
|
+
bar.className = percent >= 90 ? "bad" : percent >= 75 ? "warn" : "";
|
|
592
|
+
}
|
|
593
|
+
|
|
170
594
|
function renderTable() {
|
|
171
595
|
const source = tab === "html" ? latest.html : latest.data;
|
|
172
596
|
const thead = $("thead");
|
|
@@ -174,13 +598,31 @@ function renderTable() {
|
|
|
174
598
|
|
|
175
599
|
$("listed").textContent =
|
|
176
600
|
source.matched > source.entries.length
|
|
177
|
-
?
|
|
178
|
-
:
|
|
601
|
+
? t("entries.shownPartial", { shown: source.entries.length, total: source.matched })
|
|
602
|
+
: t("entries.shown", { total: source.matched });
|
|
179
603
|
|
|
180
|
-
|
|
604
|
+
const columns =
|
|
181
605
|
tab === "html"
|
|
182
|
-
?
|
|
183
|
-
|
|
606
|
+
? [
|
|
607
|
+
["entries.key"],
|
|
608
|
+
["entries.state"],
|
|
609
|
+
["entries.size", "num"],
|
|
610
|
+
["entries.status", "num"],
|
|
611
|
+
["entries.expires", "num"],
|
|
612
|
+
["entries.deps", "num"],
|
|
613
|
+
["entries.encodings"],
|
|
614
|
+
[null],
|
|
615
|
+
]
|
|
616
|
+
: [["entries.key"], ["entries.state"], ["entries.expires", "num"], [null]];
|
|
617
|
+
|
|
618
|
+
const headRow = document.createElement("tr");
|
|
619
|
+
for (const [key, className] of columns) {
|
|
620
|
+
const th = document.createElement("th");
|
|
621
|
+
if (className) th.className = className;
|
|
622
|
+
if (key) th.textContent = t(key);
|
|
623
|
+
headRow.append(th);
|
|
624
|
+
}
|
|
625
|
+
thead.replaceChildren(headRow);
|
|
184
626
|
|
|
185
627
|
tbody.replaceChildren(
|
|
186
628
|
...source.entries.map((/** @type {any} */ entry) => row(entry)),
|
|
@@ -198,13 +640,35 @@ function row(entry) {
|
|
|
198
640
|
|
|
199
641
|
const key = document.createElement("td");
|
|
200
642
|
key.className = "key mono";
|
|
201
|
-
|
|
643
|
+
|
|
644
|
+
if (tab === "html") {
|
|
645
|
+
// HTML anahtarı gezilebilir bir yol: yeni sekmede açmak, bir sayfanın
|
|
646
|
+
// neden bayat olduğunu anlamanın en kısa yolu. `noreferrer` panelin
|
|
647
|
+
// yolunu `Referer` başlığıyla sızdırmamak için.
|
|
648
|
+
const link = document.createElement("a");
|
|
649
|
+
link.href = entry.url;
|
|
650
|
+
link.target = "_blank";
|
|
651
|
+
link.rel = "noreferrer noopener";
|
|
652
|
+
link.textContent = entry.url;
|
|
653
|
+
key.append(link);
|
|
654
|
+
} else {
|
|
655
|
+
// Veri anahtarı bir URL değil; tıklamanın işe yarar karşılığı onu
|
|
656
|
+
// `clearDataCache()` çağrısına yapıştırabilmek.
|
|
657
|
+
const copy = document.createElement("button");
|
|
658
|
+
copy.className = "as-text";
|
|
659
|
+
copy.type = "button";
|
|
660
|
+
copy.title = t("entries.copyKey");
|
|
661
|
+
copy.textContent = entry.key;
|
|
662
|
+
copy.addEventListener("click", () => void copyKey(entry.key));
|
|
663
|
+
key.append(copy);
|
|
664
|
+
}
|
|
665
|
+
|
|
202
666
|
tr.append(key);
|
|
203
667
|
|
|
204
668
|
const state = document.createElement("td");
|
|
205
669
|
const tag = document.createElement("span");
|
|
206
670
|
tag.className = entry.stale ? "tag stale" : "tag fresh";
|
|
207
|
-
tag.textContent = entry.stale ? "stale" : "fresh";
|
|
671
|
+
tag.textContent = entry.stale ? t("entries.stale") : t("entries.fresh");
|
|
208
672
|
state.append(tag);
|
|
209
673
|
tr.append(state);
|
|
210
674
|
|
|
@@ -223,9 +687,22 @@ function row(entry) {
|
|
|
223
687
|
const actions = document.createElement("td");
|
|
224
688
|
actions.className = "actions";
|
|
225
689
|
|
|
690
|
+
// Tek satırı hem origin'den hem edge'den düşürmek en sık istenen çift
|
|
691
|
+
// işlem; Cloudflare bağlı değilse düğme hiç çıkmaz.
|
|
692
|
+
if (tab === "html" && cloudflare?.ok) {
|
|
693
|
+
const purge = document.createElement("button");
|
|
694
|
+
purge.className = "tiny";
|
|
695
|
+
purge.textContent = t("cf.rowPurge");
|
|
696
|
+
purge.title = t("cf.rowPurgeTitle");
|
|
697
|
+
purge.addEventListener("click", () => {
|
|
698
|
+
void act({ type: "cf:purge-urls", paths: [entry.url] });
|
|
699
|
+
});
|
|
700
|
+
actions.append(purge);
|
|
701
|
+
}
|
|
702
|
+
|
|
226
703
|
const drop = document.createElement("button");
|
|
227
704
|
drop.className = "tiny danger";
|
|
228
|
-
drop.textContent = "drop";
|
|
705
|
+
drop.textContent = t("entries.drop");
|
|
229
706
|
drop.addEventListener("click", () => {
|
|
230
707
|
void act({ type: tab === "html" ? "html:drop" : "data:drop", key: entry.key });
|
|
231
708
|
});
|
|
@@ -236,6 +713,23 @@ function row(entry) {
|
|
|
236
713
|
return tr;
|
|
237
714
|
}
|
|
238
715
|
|
|
716
|
+
/**
|
|
717
|
+
* Panel `http://localhost` dışında da açılabiliyor ve tarayıcı güvensiz
|
|
718
|
+
* kaynaklarda pano API'sini vermiyor; o durumda anahtar seçili bırakılır.
|
|
719
|
+
*
|
|
720
|
+
* @param {string} key
|
|
721
|
+
*/
|
|
722
|
+
async function copyKey(key) {
|
|
723
|
+
try {
|
|
724
|
+
await navigator.clipboard.writeText(key);
|
|
725
|
+
toast(t("toast.keyCopied"));
|
|
726
|
+
} catch {
|
|
727
|
+
$("prefix").value = key;
|
|
728
|
+
$("prefix").focus();
|
|
729
|
+
toast(t("toast.clipboard"), false);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
239
733
|
/**
|
|
240
734
|
* @param {string} text
|
|
241
735
|
* @param {string} [className]
|
|
@@ -294,7 +788,7 @@ for (const name of /** @type {const} */ (["html", "data"])) {
|
|
|
294
788
|
$("invalidate").addEventListener("click", () => {
|
|
295
789
|
const target = $("target").value.trim();
|
|
296
790
|
if (!target) {
|
|
297
|
-
toast("
|
|
791
|
+
toast(t("toast.needTarget"), false);
|
|
298
792
|
return;
|
|
299
793
|
}
|
|
300
794
|
|
|
@@ -302,13 +796,13 @@ $("invalidate").addEventListener("click", () => {
|
|
|
302
796
|
});
|
|
303
797
|
|
|
304
798
|
$("clear-html").addEventListener("click", () => {
|
|
305
|
-
if (!confirm("
|
|
799
|
+
if (!confirm(t("confirm.clearHtml"))) return;
|
|
306
800
|
void act({ type: "html:clear" });
|
|
307
801
|
});
|
|
308
802
|
|
|
309
803
|
$("clear-data").addEventListener("click", () => {
|
|
310
804
|
const prefix = $("prefix").value.trim();
|
|
311
|
-
if (!prefix && !confirm("
|
|
805
|
+
if (!prefix && !confirm(t("confirm.clearData"))) {
|
|
312
806
|
return;
|
|
313
807
|
}
|
|
314
808
|
|
|
@@ -317,6 +811,10 @@ $("clear-data").addEventListener("click", () => {
|
|
|
317
811
|
|
|
318
812
|
$("prewarm").addEventListener("click", () => void act({ type: "prewarm" }));
|
|
319
813
|
|
|
814
|
+
// Sayım ayrı bir düğme: her döküm turunda tüm keyspace'i taramak Redis'i
|
|
815
|
+
// teşhis uğruna yormak olurdu.
|
|
816
|
+
$("redis-inspect").addEventListener("click", () => void act({ type: "redis:inspect" }));
|
|
817
|
+
|
|
320
818
|
$("redis-html").addEventListener("click", () =>
|
|
321
819
|
void act({ type: "redis:drop", kind: "html" }),
|
|
322
820
|
);
|
|
@@ -325,6 +823,68 @@ $("redis-data").addEventListener("click", () =>
|
|
|
325
823
|
void act({ type: "redis:drop", kind: "data" }),
|
|
326
824
|
);
|
|
327
825
|
|
|
826
|
+
/* ------------------------------------------------- cloudflare kontrolleri */
|
|
827
|
+
|
|
828
|
+
$("cf-refresh").addEventListener("click", () => void loadCloudflare(true));
|
|
829
|
+
|
|
830
|
+
$("cf-purge-all").addEventListener("click", () => {
|
|
831
|
+
if (!confirm(t("confirm.purgeAll"))) return;
|
|
832
|
+
void act({ type: "cf:purge-everything" });
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
// Bu kurulumda sıcak olan yollar zaten biliniyor; "her şeyi düşür" yerine
|
|
836
|
+
// yalnızca gerçekten servis edilen sayfaları düşürmek edge'i boşa soğutmaz.
|
|
837
|
+
$("cf-purge-cached").addEventListener("click", () => {
|
|
838
|
+
const paths = (latest?.html.entries ?? []).map((/** @type {any} */ entry) => entry.url);
|
|
839
|
+
if (!paths.length) {
|
|
840
|
+
toast(t("toast.noCachedPages"), false);
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
if (!confirm(t("confirm.purgeUrls", { count: paths.length }))) return;
|
|
845
|
+
void act({ type: "cf:purge-urls", paths });
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
$("cf-purge-keys").addEventListener("click", () => {
|
|
849
|
+
const values = $("cf-values").value.trim();
|
|
850
|
+
if (!values) {
|
|
851
|
+
toast(t("toast.needValue"), false);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
void act({ type: "cf:purge-keys", kind: $("cf-kind").value, values });
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
$("cf-dev").addEventListener("change", (event) => {
|
|
859
|
+
const on = /** @type {HTMLInputElement} */ (event.target).checked;
|
|
860
|
+
void act({ type: "cf:setting", id: "development_mode", value: on ? "on" : "off" });
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
$("cf-tiered").addEventListener("click", () => {
|
|
864
|
+
const current = cloudflare?.settings?.tieredCaching;
|
|
865
|
+
void act({ type: "cf:feature", feature: "tiered_caching", value: flip(current) });
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
$("cf-reserve").addEventListener("click", () => {
|
|
869
|
+
const current = cloudflare?.settings?.cacheReserve;
|
|
870
|
+
void act({ type: "cf:feature", feature: "cache_reserve", value: flip(current) });
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
$("cf-reserve-clear").addEventListener("click", () => {
|
|
874
|
+
if (!confirm(t("confirm.clearReserve"))) return;
|
|
875
|
+
void act({ type: "cf:clear-reserve" });
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
$("cf-analytics").addEventListener("click", () => void loadCloudflareAnalytics());
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* @param {string | null | undefined} value
|
|
882
|
+
* @returns {"on" | "off"}
|
|
883
|
+
*/
|
|
884
|
+
function flip(value) {
|
|
885
|
+
return value === "on" ? "off" : "on";
|
|
886
|
+
}
|
|
887
|
+
|
|
328
888
|
// Sekme arkaya alındığında yoklama durur: panel açık kalmış bir sekmede
|
|
329
889
|
// dakikalarca boşa istek atmasın.
|
|
330
890
|
document.addEventListener("visibilitychange", () => {
|
|
@@ -335,5 +895,21 @@ document.addEventListener("visibilitychange", () => {
|
|
|
335
895
|
}
|
|
336
896
|
});
|
|
337
897
|
|
|
898
|
+
/* -------------------------------------------------------------------- dil */
|
|
899
|
+
|
|
900
|
+
// Dil değişimi hiçbir isteğe yol açmaz: statik metinler sözlükten yeniden
|
|
901
|
+
// yazılır, dinamik olanlar elde duran son dökümle tekrar çizilir.
|
|
902
|
+
$("language").append(
|
|
903
|
+
languageSelect(() => {
|
|
904
|
+
applyTranslations();
|
|
905
|
+
if (latest) render();
|
|
906
|
+
renderCloudflare();
|
|
907
|
+
renderReport();
|
|
908
|
+
}),
|
|
909
|
+
);
|
|
910
|
+
|
|
911
|
+
applyTranslations();
|
|
912
|
+
|
|
338
913
|
void load();
|
|
914
|
+
void loadCloudflare();
|
|
339
915
|
startPolling();
|