jskelet 0.2.2 → 0.2.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.
- package/CHANGELOG.md +368 -315
- package/docs/03-routing.md +4 -0
- package/docs/06-cache.md +147 -10
- package/docs/07-yapilandirma.md +63 -2
- package/docs/en/03-routing.md +4 -0
- package/docs/en/06-caching.md +147 -11
- package/docs/en/07-configuration.md +65 -2
- package/package.json +1 -1
- package/src/client/cache-panel/login.html +9 -1
- package/src/client/cache-panel/panel.css +298 -6
- package/src/client/cache-panel/panel.html +179 -5
- package/src/client/cache-panel/panel.js +472 -3
- 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 +246 -13
- 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
|
@@ -120,6 +120,9 @@ async function act(body) {
|
|
|
120
120
|
const result = await response.json().catch(() => ({ ok: false, message: "Failed" }));
|
|
121
121
|
toast(result.message ?? (result.ok ? "Done" : "Failed"), result.ok !== false);
|
|
122
122
|
await load();
|
|
123
|
+
|
|
124
|
+
// Cloudflare ayarları değişmiş olabilir; genel bakış önbelleği atlanır.
|
|
125
|
+
if (String(body.type ?? "").startsWith("cf:")) await loadCloudflare(true);
|
|
123
126
|
}
|
|
124
127
|
|
|
125
128
|
/* ------------------------------------------------------------------- çizim */
|
|
@@ -127,7 +130,17 @@ async function act(body) {
|
|
|
127
130
|
function render() {
|
|
128
131
|
if (!latest) return;
|
|
129
132
|
|
|
130
|
-
const { process: proc, html, data, redis, prewarm } = latest;
|
|
133
|
+
const { process: proc, release, host, html, data, redis, prewarm } = latest;
|
|
134
|
+
|
|
135
|
+
$("version").textContent = `v${release.version}`;
|
|
136
|
+
$("foot-release").textContent = [
|
|
137
|
+
`v${release.version}`,
|
|
138
|
+
release.license,
|
|
139
|
+
release.node ? `Node ${release.node}` : null,
|
|
140
|
+
]
|
|
141
|
+
.filter(Boolean)
|
|
142
|
+
.join(" · ");
|
|
143
|
+
if (release.homepage) $("foot-repo").href = release.homepage;
|
|
131
144
|
|
|
132
145
|
$("env").textContent = proc.env;
|
|
133
146
|
$("pid").textContent = `pid ${proc.pid}`;
|
|
@@ -152,9 +165,12 @@ function render() {
|
|
|
152
165
|
|
|
153
166
|
$("redis-state").textContent = redisState;
|
|
154
167
|
$("redis-sub").textContent = redis.enabled
|
|
155
|
-
? `${redis.
|
|
168
|
+
? `${redis.address} · ${redis.errors} errors`
|
|
156
169
|
: "cache.redis.enabled is false";
|
|
157
170
|
|
|
171
|
+
renderRedis(redis, redisState);
|
|
172
|
+
renderHost(host, proc, redis);
|
|
173
|
+
|
|
158
174
|
const done = prewarm.done ?? 0;
|
|
159
175
|
const total = prewarm.total ?? 0;
|
|
160
176
|
$("prewarm-state").textContent = prewarm.active ? `${done}/${total}` : "idle";
|
|
@@ -167,6 +183,338 @@ function render() {
|
|
|
167
183
|
renderTable();
|
|
168
184
|
}
|
|
169
185
|
|
|
186
|
+
/* ------------------------------------------------------------- cloudflare */
|
|
187
|
+
|
|
188
|
+
/** @type {any} */
|
|
189
|
+
let cloudflare = null;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Cloudflare genel bakışı ayrı bir uçtan gelir: zone bilgisi ve ayarlar ağa
|
|
193
|
+
* çıkıyor, döküm turu ise saniyeler mertebesinde yenileniyor. Bu yüzden
|
|
194
|
+
* yalnızca açılışta ve bir işlemden sonra istenir.
|
|
195
|
+
*
|
|
196
|
+
* @param {boolean} [force]
|
|
197
|
+
*/
|
|
198
|
+
async function loadCloudflare(force = false) {
|
|
199
|
+
const response = await fetch(`cloudflare${force ? "?force=1" : ""}`);
|
|
200
|
+
if (!alive(response)) return;
|
|
201
|
+
|
|
202
|
+
cloudflare = await response.json().catch(() => null);
|
|
203
|
+
renderCloudflare();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function renderCloudflare() {
|
|
207
|
+
if (!cloudflare) return;
|
|
208
|
+
|
|
209
|
+
const ok = cloudflare.ok === true;
|
|
210
|
+
const status = cloudflare.status ?? {};
|
|
211
|
+
const settings = cloudflare.settings ?? {};
|
|
212
|
+
const zone = cloudflare.zone ?? null;
|
|
213
|
+
|
|
214
|
+
$("cf-heading").textContent = ok
|
|
215
|
+
? [zone?.name, zone?.plan].filter(Boolean).join(" · ") || "connected"
|
|
216
|
+
: status.configured
|
|
217
|
+
? `error: ${cloudflare.error}`
|
|
218
|
+
: "not connected";
|
|
219
|
+
|
|
220
|
+
$("cf-tools").hidden = !ok;
|
|
221
|
+
$("cf-suggest").hidden = ok || status.configured;
|
|
222
|
+
$("cf-refresh").hidden = !status.configured;
|
|
223
|
+
|
|
224
|
+
if (!ok) {
|
|
225
|
+
$("cf-kv").replaceChildren();
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const development = settings.developmentMode === "on";
|
|
230
|
+
$("cf-dev").checked = development;
|
|
231
|
+
|
|
232
|
+
/** @type {[string, string, string?][]} */
|
|
233
|
+
const rows = [
|
|
234
|
+
["Zone", `${zone?.name ?? "—"} (${status.zoneId ?? "?"})`],
|
|
235
|
+
["Plan", zone?.plan ?? "—"],
|
|
236
|
+
["Token", status.tokenSource === "env" ? "from environment" : "from config", "on"],
|
|
237
|
+
[
|
|
238
|
+
"Development mode",
|
|
239
|
+
development
|
|
240
|
+
? `on · ${formatDuration(settings.developmentModeRemaining)} left`
|
|
241
|
+
: "off",
|
|
242
|
+
development ? "bad" : "on",
|
|
243
|
+
],
|
|
244
|
+
["Cache level", String(settings.cacheLevel ?? "—")],
|
|
245
|
+
["Browser cache TTL", ttl(settings.browserCacheTtl)],
|
|
246
|
+
["Edge cache TTL", ttl(settings.edgeCacheTtl)],
|
|
247
|
+
["Query string sorting", String(settings.sortQueryString ?? "—")],
|
|
248
|
+
["Always Online", String(settings.alwaysOnline ?? "—")],
|
|
249
|
+
...feature("Tiered Cache", settings.tieredCaching),
|
|
250
|
+
...feature("Regional Tiered Cache", settings.regionalTieredCache),
|
|
251
|
+
...feature("Cache Reserve", settings.cacheReserve),
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
$("cf-kv").replaceChildren(...rows.map(kvRow));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Plan dışı kalan bir özellik `null` döner. "off" yazmak yanıltıcı olurdu:
|
|
259
|
+
* kapatılmış değil, o planda hiç yok.
|
|
260
|
+
*
|
|
261
|
+
* @param {string} label
|
|
262
|
+
* @param {string | null} value
|
|
263
|
+
* @returns {[string, string, string?][]}
|
|
264
|
+
*/
|
|
265
|
+
function feature(label, value) {
|
|
266
|
+
if (value === null || value === undefined) {
|
|
267
|
+
return [[label, "unavailable on this plan", "off"]];
|
|
268
|
+
}
|
|
269
|
+
return [[label, value, value === "on" ? "on" : "off"]];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* @param {unknown} seconds
|
|
274
|
+
* @returns {string}
|
|
275
|
+
*/
|
|
276
|
+
function ttl(seconds) {
|
|
277
|
+
const value = Number(seconds);
|
|
278
|
+
if (!Number.isFinite(value)) return "—";
|
|
279
|
+
// Cloudflare 0'ı "Respect Existing Headers" olarak kullanıyor.
|
|
280
|
+
if (value === 0) return "respect origin headers";
|
|
281
|
+
return formatDuration(value);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* @param {[string, string, string?]} row
|
|
286
|
+
* @returns {HTMLDivElement}
|
|
287
|
+
*/
|
|
288
|
+
function kvRow([label, value, tone]) {
|
|
289
|
+
const wrap = document.createElement("div");
|
|
290
|
+
const dt = document.createElement("dt");
|
|
291
|
+
dt.textContent = label;
|
|
292
|
+
const dd = document.createElement("dd");
|
|
293
|
+
if (tone) dd.className = tone;
|
|
294
|
+
dd.textContent = value;
|
|
295
|
+
wrap.append(dt, dd);
|
|
296
|
+
return wrap;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Analitik dökümü. İki soru tek uçtan cevaplanıyor: yol verilirse kolo
|
|
301
|
+
* kırılımı, verilmezse zone genelinde cache durumu dağılımı.
|
|
302
|
+
*/
|
|
303
|
+
async function loadCloudflareAnalytics() {
|
|
304
|
+
const path = $("cf-path").value.trim();
|
|
305
|
+
const target = $("cf-report");
|
|
306
|
+
|
|
307
|
+
target.hidden = false;
|
|
308
|
+
target.textContent = "Querying Cloudflare…";
|
|
309
|
+
|
|
310
|
+
const response = await fetch("cloudflare/analytics", {
|
|
311
|
+
method: "POST",
|
|
312
|
+
headers: {
|
|
313
|
+
"Content-Type": "application/json",
|
|
314
|
+
"X-JSkelet-Cache-Panel": "1",
|
|
315
|
+
},
|
|
316
|
+
body: JSON.stringify({ path }),
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
if (!alive(response)) return;
|
|
320
|
+
|
|
321
|
+
const result = await response.json().catch(() => null);
|
|
322
|
+
|
|
323
|
+
if (!result?.ok) {
|
|
324
|
+
target.textContent = `Cloudflare: ${result?.error ?? "query failed"}`;
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
target.replaceChildren(
|
|
329
|
+
result.colos ? edgeTable(result) : statusTable(result),
|
|
330
|
+
note(
|
|
331
|
+
result.colos
|
|
332
|
+
? `${result.colos.length} colos served \`${result.path}\` in the last ` +
|
|
333
|
+
`${result.hours}h — ${result.hits} from cache, ${result.misses} not. ` +
|
|
334
|
+
"Edges that received no request do not appear, even if they hold a copy."
|
|
335
|
+
: `Zone-wide cache status over the last ${result.hours}h. ` +
|
|
336
|
+
"Numbers come from Cloudflare's sampled dataset: ratios are reliable, " +
|
|
337
|
+
"absolute counts are estimates.",
|
|
338
|
+
),
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* @param {any} result
|
|
344
|
+
* @returns {HTMLTableElement}
|
|
345
|
+
*/
|
|
346
|
+
function edgeTable(result) {
|
|
347
|
+
return buildTable(
|
|
348
|
+
["Colo", "From cache", "To origin"],
|
|
349
|
+
result.colos.map((/** @type {any} */ row) => [
|
|
350
|
+
row.colo,
|
|
351
|
+
String(row.hits),
|
|
352
|
+
String(row.misses),
|
|
353
|
+
]),
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* @param {any} result
|
|
359
|
+
* @returns {HTMLTableElement}
|
|
360
|
+
*/
|
|
361
|
+
function statusTable(result) {
|
|
362
|
+
const total = result.rows.reduce((sum, /** @type {any} */ row) => sum + row.requests, 0) || 1;
|
|
363
|
+
|
|
364
|
+
return buildTable(
|
|
365
|
+
["Cache status", "Requests", "Share", "Edge bytes"],
|
|
366
|
+
result.rows.map((/** @type {any} */ row) => [
|
|
367
|
+
row.status,
|
|
368
|
+
row.requests.toLocaleString(),
|
|
369
|
+
`${Math.round((row.requests / total) * 100)}%`,
|
|
370
|
+
formatBytes(row.bytes),
|
|
371
|
+
]),
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* @param {string[]} headers
|
|
377
|
+
* @param {string[][]} rows
|
|
378
|
+
* @returns {HTMLTableElement}
|
|
379
|
+
*/
|
|
380
|
+
function buildTable(headers, rows) {
|
|
381
|
+
const table = document.createElement("table");
|
|
382
|
+
const thead = document.createElement("thead");
|
|
383
|
+
const headRow = document.createElement("tr");
|
|
384
|
+
|
|
385
|
+
headers.forEach((label, index) => {
|
|
386
|
+
const th = document.createElement("th");
|
|
387
|
+
th.textContent = label;
|
|
388
|
+
if (index > 0) th.className = "num";
|
|
389
|
+
headRow.append(th);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
thead.append(headRow);
|
|
393
|
+
|
|
394
|
+
const tbody = document.createElement("tbody");
|
|
395
|
+
for (const row of rows) {
|
|
396
|
+
const tr = document.createElement("tr");
|
|
397
|
+
row.forEach((value, index) => tr.append(cell(value, index > 0 ? "num" : undefined)));
|
|
398
|
+
tbody.append(tr);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
table.append(thead, tbody);
|
|
402
|
+
return table;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* @param {string} text
|
|
407
|
+
* @returns {HTMLParagraphElement}
|
|
408
|
+
*/
|
|
409
|
+
function note(text) {
|
|
410
|
+
const paragraph = document.createElement("p");
|
|
411
|
+
paragraph.className = "note m0";
|
|
412
|
+
paragraph.textContent = text;
|
|
413
|
+
return paragraph;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Paylaşımlı kademenin künyesi.
|
|
418
|
+
*
|
|
419
|
+
* "Bağlı mı" tek başına yetmiyor: yanlış `namespace` ya da beklenmeyen bir
|
|
420
|
+
* `buildId` ile bağlı bir Redis de hiçbir şey paylaşmıyor gibi görünür. Bu
|
|
421
|
+
* yüzden anahtarın **nerede** durduğu ve hangi türlerin paylaşıldığı da
|
|
422
|
+
* yazılır.
|
|
423
|
+
*
|
|
424
|
+
* @param {any} redis
|
|
425
|
+
* @param {string} state
|
|
426
|
+
*/
|
|
427
|
+
function renderRedis(redis, state) {
|
|
428
|
+
$("redis-heading").textContent = redis.enabled ? state : "not configured";
|
|
429
|
+
$("redis-actions").hidden = !redis.enabled;
|
|
430
|
+
$("redis-suggest").hidden = redis.enabled;
|
|
431
|
+
|
|
432
|
+
/** @type {[string, string, string?][]} */
|
|
433
|
+
const rows = redis.enabled
|
|
434
|
+
? [
|
|
435
|
+
["Connection", state, redis.connected && !redis.bypassed ? "on" : "bad"],
|
|
436
|
+
["Address", `${redis.address}${redis.secure ? " (TLS)" : ""}`],
|
|
437
|
+
["Database", redis.db ?? "default"],
|
|
438
|
+
["Key prefix", `${redis.keyPrefix}:${redis.namespace}`],
|
|
439
|
+
["Build id", redis.buildId || "dev"],
|
|
440
|
+
["Shares HTML", redis.html ? "yes" : "no", redis.html ? "on" : "off"],
|
|
441
|
+
["Shares data", redis.data ? "yes" : "no", redis.data ? "on" : "off"],
|
|
442
|
+
[
|
|
443
|
+
"Compressed bodies",
|
|
444
|
+
redis.storeEncoded ? "shared" : "local only",
|
|
445
|
+
redis.storeEncoded ? "on" : "off",
|
|
446
|
+
],
|
|
447
|
+
[
|
|
448
|
+
"Purge broadcast",
|
|
449
|
+
redis.events ? (redis.subscribed ? "subscribed" : "publish only") : "off",
|
|
450
|
+
redis.events && redis.subscribed ? "on" : "off",
|
|
451
|
+
],
|
|
452
|
+
["Command timeout", `${redis.commandTimeoutMs} ms`],
|
|
453
|
+
["Command errors", String(redis.errors), redis.errors ? "bad" : "on"],
|
|
454
|
+
]
|
|
455
|
+
: [
|
|
456
|
+
["Tier", "in-process only"],
|
|
457
|
+
["Replicas sharing this cache", "none"],
|
|
458
|
+
["Purge broadcast", "local only", "off"],
|
|
459
|
+
];
|
|
460
|
+
|
|
461
|
+
$("redis-kv").replaceChildren(...rows.map(kvRow));
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Makinenin RAM ve disk durumu. Paylaşımlı kademe kapalıyken önbelleğin
|
|
466
|
+
* tamamı bu sürecin belleğinde yaşıyor; `maxEntries` ile RAM arasındaki
|
|
467
|
+
* ilişkiyi görmeden ayar yapmak körlemesine oluyor.
|
|
468
|
+
*
|
|
469
|
+
* @param {any} host
|
|
470
|
+
* @param {any} proc
|
|
471
|
+
* @param {any} redis
|
|
472
|
+
*/
|
|
473
|
+
function renderHost(host, proc, redis) {
|
|
474
|
+
$("host-heading").textContent = [
|
|
475
|
+
host.platform,
|
|
476
|
+
`${host.cpus} cpu`,
|
|
477
|
+
host.load ? `load ${host.load.toFixed(2)}` : null,
|
|
478
|
+
]
|
|
479
|
+
.filter(Boolean)
|
|
480
|
+
.join(" · ");
|
|
481
|
+
|
|
482
|
+
const ramUsed = host.memory.used / host.memory.total;
|
|
483
|
+
$("ram-text").textContent =
|
|
484
|
+
`${formatBytes(host.memory.used)} / ${formatBytes(host.memory.total)}`;
|
|
485
|
+
meter("ram-bar", ramUsed);
|
|
486
|
+
$("ram-note").textContent = redis.connected
|
|
487
|
+
? `This process: ${formatBytes(proc.memory.rss)} RSS.`
|
|
488
|
+
: `This process holds the whole cache: ${formatBytes(proc.memory.rss)} RSS, ` +
|
|
489
|
+
`${formatBytes(latest.html.bytes)} of it HTML.`;
|
|
490
|
+
|
|
491
|
+
if (!host.disk) {
|
|
492
|
+
$("disk-text").textContent = "unavailable";
|
|
493
|
+
meter("disk-bar", 0);
|
|
494
|
+
$("disk-note").textContent = "This platform does not report filesystem stats.";
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const diskUsed = (host.disk.total - host.disk.free) / host.disk.total;
|
|
499
|
+
$("disk-text").textContent =
|
|
500
|
+
`${formatBytes(host.disk.total - host.disk.free)} / ${formatBytes(host.disk.total)}`;
|
|
501
|
+
meter("disk-bar", diskUsed);
|
|
502
|
+
// Önbellek diske yazılmıyor; disk build çıktısı ve log için önemli.
|
|
503
|
+
$("disk-note").textContent =
|
|
504
|
+
`${formatBytes(host.disk.free)} free on ${host.disk.path} — build output and logs live here.`;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* @param {string} id
|
|
509
|
+
* @param {number} ratio 0–1
|
|
510
|
+
*/
|
|
511
|
+
function meter(id, ratio) {
|
|
512
|
+
const bar = $(id);
|
|
513
|
+
const percent = Math.max(0, Math.min(100, ratio * 100));
|
|
514
|
+
bar.style.width = `${percent}%`;
|
|
515
|
+
bar.className = percent >= 90 ? "bad" : percent >= 75 ? "warn" : "";
|
|
516
|
+
}
|
|
517
|
+
|
|
170
518
|
function renderTable() {
|
|
171
519
|
const source = tab === "html" ? latest.html : latest.data;
|
|
172
520
|
const thead = $("thead");
|
|
@@ -198,7 +546,29 @@ function row(entry) {
|
|
|
198
546
|
|
|
199
547
|
const key = document.createElement("td");
|
|
200
548
|
key.className = "key mono";
|
|
201
|
-
|
|
549
|
+
|
|
550
|
+
if (tab === "html") {
|
|
551
|
+
// HTML anahtarı gezilebilir bir yol: yeni sekmede açmak, bir sayfanın
|
|
552
|
+
// neden bayat olduğunu anlamanın en kısa yolu. `noreferrer` panelin
|
|
553
|
+
// yolunu `Referer` başlığıyla sızdırmamak için.
|
|
554
|
+
const link = document.createElement("a");
|
|
555
|
+
link.href = entry.url;
|
|
556
|
+
link.target = "_blank";
|
|
557
|
+
link.rel = "noreferrer noopener";
|
|
558
|
+
link.textContent = entry.url;
|
|
559
|
+
key.append(link);
|
|
560
|
+
} else {
|
|
561
|
+
// Veri anahtarı bir URL değil; tıklamanın işe yarar karşılığı onu
|
|
562
|
+
// `clearDataCache()` çağrısına yapıştırabilmek.
|
|
563
|
+
const copy = document.createElement("button");
|
|
564
|
+
copy.className = "as-text";
|
|
565
|
+
copy.type = "button";
|
|
566
|
+
copy.title = "Copy key";
|
|
567
|
+
copy.textContent = entry.key;
|
|
568
|
+
copy.addEventListener("click", () => void copyKey(entry.key));
|
|
569
|
+
key.append(copy);
|
|
570
|
+
}
|
|
571
|
+
|
|
202
572
|
tr.append(key);
|
|
203
573
|
|
|
204
574
|
const state = document.createElement("td");
|
|
@@ -223,6 +593,19 @@ function row(entry) {
|
|
|
223
593
|
const actions = document.createElement("td");
|
|
224
594
|
actions.className = "actions";
|
|
225
595
|
|
|
596
|
+
// Tek satırı hem origin'den hem edge'den düşürmek en sık istenen çift
|
|
597
|
+
// işlem; Cloudflare bağlı değilse düğme hiç çıkmaz.
|
|
598
|
+
if (tab === "html" && cloudflare?.ok) {
|
|
599
|
+
const purge = document.createElement("button");
|
|
600
|
+
purge.className = "tiny";
|
|
601
|
+
purge.textContent = "cf purge";
|
|
602
|
+
purge.title = "Purge this URL at Cloudflare";
|
|
603
|
+
purge.addEventListener("click", () => {
|
|
604
|
+
void act({ type: "cf:purge-urls", paths: [entry.url] });
|
|
605
|
+
});
|
|
606
|
+
actions.append(purge);
|
|
607
|
+
}
|
|
608
|
+
|
|
226
609
|
const drop = document.createElement("button");
|
|
227
610
|
drop.className = "tiny danger";
|
|
228
611
|
drop.textContent = "drop";
|
|
@@ -236,6 +619,23 @@ function row(entry) {
|
|
|
236
619
|
return tr;
|
|
237
620
|
}
|
|
238
621
|
|
|
622
|
+
/**
|
|
623
|
+
* Panel `http://localhost` dışında da açılabiliyor ve tarayıcı güvensiz
|
|
624
|
+
* kaynaklarda pano API'sini vermiyor; o durumda anahtar seçili bırakılır.
|
|
625
|
+
*
|
|
626
|
+
* @param {string} key
|
|
627
|
+
*/
|
|
628
|
+
async function copyKey(key) {
|
|
629
|
+
try {
|
|
630
|
+
await navigator.clipboard.writeText(key);
|
|
631
|
+
toast("Key copied");
|
|
632
|
+
} catch {
|
|
633
|
+
$("prefix").value = key;
|
|
634
|
+
$("prefix").focus();
|
|
635
|
+
toast("Clipboard is unavailable — key moved to the prefix field", false);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
239
639
|
/**
|
|
240
640
|
* @param {string} text
|
|
241
641
|
* @param {string} [className]
|
|
@@ -317,6 +717,10 @@ $("clear-data").addEventListener("click", () => {
|
|
|
317
717
|
|
|
318
718
|
$("prewarm").addEventListener("click", () => void act({ type: "prewarm" }));
|
|
319
719
|
|
|
720
|
+
// Sayım ayrı bir düğme: her döküm turunda tüm keyspace'i taramak Redis'i
|
|
721
|
+
// teşhis uğruna yormak olurdu.
|
|
722
|
+
$("redis-inspect").addEventListener("click", () => void act({ type: "redis:inspect" }));
|
|
723
|
+
|
|
320
724
|
$("redis-html").addEventListener("click", () =>
|
|
321
725
|
void act({ type: "redis:drop", kind: "html" }),
|
|
322
726
|
);
|
|
@@ -325,6 +729,70 @@ $("redis-data").addEventListener("click", () =>
|
|
|
325
729
|
void act({ type: "redis:drop", kind: "data" }),
|
|
326
730
|
);
|
|
327
731
|
|
|
732
|
+
/* ------------------------------------------------- cloudflare kontrolleri */
|
|
733
|
+
|
|
734
|
+
$("cf-refresh").addEventListener("click", () => void loadCloudflare(true));
|
|
735
|
+
|
|
736
|
+
$("cf-purge-all").addEventListener("click", () => {
|
|
737
|
+
if (!confirm("Purge the entire Cloudflare cache for this zone?")) return;
|
|
738
|
+
void act({ type: "cf:purge-everything" });
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
// Bu kurulumda sıcak olan yollar zaten biliniyor; "her şeyi düşür" yerine
|
|
742
|
+
// yalnızca gerçekten servis edilen sayfaları düşürmek edge'i boşa soğutmaz.
|
|
743
|
+
$("cf-purge-cached").addEventListener("click", () => {
|
|
744
|
+
const paths = (latest?.html.entries ?? []).map((/** @type {any} */ entry) => entry.url);
|
|
745
|
+
if (!paths.length) {
|
|
746
|
+
toast("No cached pages to purge.", false);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (!confirm(`Purge ${paths.length} URLs at Cloudflare?`)) return;
|
|
751
|
+
void act({ type: "cf:purge-urls", paths });
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
$("cf-purge-keys").addEventListener("click", () => {
|
|
755
|
+
const values = $("cf-values").value.trim();
|
|
756
|
+
if (!values) {
|
|
757
|
+
toast("Enter at least one value.", false);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
void act({ type: "cf:purge-keys", kind: $("cf-kind").value, values });
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
$("cf-dev").addEventListener("change", (event) => {
|
|
765
|
+
const on = /** @type {HTMLInputElement} */ (event.target).checked;
|
|
766
|
+
void act({ type: "cf:setting", id: "development_mode", value: on ? "on" : "off" });
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
$("cf-tiered").addEventListener("click", () => {
|
|
770
|
+
const current = cloudflare?.settings?.tieredCaching;
|
|
771
|
+
void act({ type: "cf:feature", feature: "tiered_caching", value: flip(current) });
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
$("cf-reserve").addEventListener("click", () => {
|
|
775
|
+
const current = cloudflare?.settings?.cacheReserve;
|
|
776
|
+
void act({ type: "cf:feature", feature: "cache_reserve", value: flip(current) });
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
$("cf-reserve-clear").addEventListener("click", () => {
|
|
780
|
+
if (!confirm("Clear Cache Reserve? Cloudflare will refetch everything from the origin.")) {
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
void act({ type: "cf:clear-reserve" });
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
$("cf-analytics").addEventListener("click", () => void loadCloudflareAnalytics());
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* @param {string | null | undefined} value
|
|
790
|
+
* @returns {"on" | "off"}
|
|
791
|
+
*/
|
|
792
|
+
function flip(value) {
|
|
793
|
+
return value === "on" ? "off" : "on";
|
|
794
|
+
}
|
|
795
|
+
|
|
328
796
|
// Sekme arkaya alındığında yoklama durur: panel açık kalmış bir sekmede
|
|
329
797
|
// dakikalarca boşa istek atmasın.
|
|
330
798
|
document.addEventListener("visibilitychange", () => {
|
|
@@ -336,4 +804,5 @@ document.addEventListener("visibilitychange", () => {
|
|
|
336
804
|
});
|
|
337
805
|
|
|
338
806
|
void load();
|
|
807
|
+
void loadCloudflare();
|
|
339
808
|
startPolling();
|
package/src/config/defaults.js
CHANGED
|
@@ -215,6 +215,37 @@ export const DEFAULT_CACHE_PANEL = {
|
|
|
215
215
|
sessionHours: 12,
|
|
216
216
|
};
|
|
217
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Cloudflare cache yüzeyi.
|
|
220
|
+
*
|
|
221
|
+
* JSkelet'in önbelleği origin önbelleği; ziyaretçinin gördüğü kopya CDN'de.
|
|
222
|
+
* Bu bölüm ikisini aynı panelden yönetilebilir kılar (bkz.
|
|
223
|
+
* `src/server/cloudflare.js`).
|
|
224
|
+
*
|
|
225
|
+
* Token **config'e yazılmamalı**: `JSKELET_CLOUDFLARE_KEY` env'i önceliklidir
|
|
226
|
+
* ve önerilen yol odur. Zone kimliği sır değil, ama o da env'den okunabilir
|
|
227
|
+
* (`JSKELET_CLOUDFLARE_ZONE_ID`).
|
|
228
|
+
*
|
|
229
|
+
* Gereken token izinleri: purge için `Zone.Cache Purge`, ayarlar için
|
|
230
|
+
* `Zone.Zone Settings`, analitik için `Zone.Analytics` (salt okunur).
|
|
231
|
+
*/
|
|
232
|
+
export const DEFAULT_CLOUDFLARE = {
|
|
233
|
+
/** `false` verilirse env'de token olsa bile yüzey kapalı kalır. */
|
|
234
|
+
enabled: true,
|
|
235
|
+
/** @type {string | null} */
|
|
236
|
+
zoneId: null,
|
|
237
|
+
/** @type {string | null} Env tercih edilir; burada tutmak sırrı repoya sokar. */
|
|
238
|
+
apiToken: null,
|
|
239
|
+
/**
|
|
240
|
+
* Purge, tam URL istiyor; panel elinde yalnızca yol tutuyor. Site adı
|
|
241
|
+
* verilmezse purge isteğinin geldiği istek origin'i kullanılır.
|
|
242
|
+
* @type {string | null}
|
|
243
|
+
*/
|
|
244
|
+
hostname: null,
|
|
245
|
+
/** Analitik penceresi (saat). Cloudflare'in izin verdiği aralıkla sınırlı. */
|
|
246
|
+
analyticsHours: 24,
|
|
247
|
+
};
|
|
248
|
+
|
|
218
249
|
/** Oturuma bağlı sayfalar ısıtılmaz; uygulama kendi listesini verebilir. */
|
|
219
250
|
export const DEFAULT_PREWARM_SKIP = [
|
|
220
251
|
"/api/",
|