jskelet 0.2.3 → 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 +7 -0
- package/docs/06-cache.md +8 -1
- package/docs/en/06-caching.md +8 -1
- package/package.json +1 -1
- package/src/client/cache-panel/i18n.js +670 -0
- package/src/client/cache-panel/login.html +17 -14
- package/src/client/cache-panel/panel.css +16 -0
- package/src/client/cache-panel/panel.html +80 -79
- package/src/client/cache-panel/panel.js +201 -94
- package/src/server/cache-panel.js +104 -83
|
@@ -338,7 +338,11 @@ async function hostStatus() {
|
|
|
338
338
|
/* --------------------------------------------------------------- aksiyonlar */
|
|
339
339
|
|
|
340
340
|
/**
|
|
341
|
-
* Panelden gelen işlemi uygular ve
|
|
341
|
+
* Panelden gelen işlemi uygular ve sonucu **metin değil kod** olarak döner.
|
|
342
|
+
*
|
|
343
|
+
* Cümleyi panel kuruyor (`i18n.js`): panel iki dilde konuşuyor, sunucunun
|
|
344
|
+
* arayüz dilini bilmesi için bir sebep yok. `params` yer tutucuları besler,
|
|
345
|
+
* `parts` ise eksik kalabilen parçalı cevaplar için.
|
|
342
346
|
*
|
|
343
347
|
* Her işlem yerel kademeyi düşürüp paylaşımlı kademeye de yayılıyor (bkz.
|
|
344
348
|
* `html-cache.js`, `data-cache.js`): tek node'un önbelleğini boşaltmak,
|
|
@@ -346,7 +350,9 @@ async function hostStatus() {
|
|
|
346
350
|
*
|
|
347
351
|
* @param {Record<string, any>} body
|
|
348
352
|
* @param {import('express').Request} req
|
|
349
|
-
* @returns {Promise<{ ok: boolean,
|
|
353
|
+
* @returns {Promise<{ ok: boolean, code?: string,
|
|
354
|
+
* params?: Record<string, string | number>,
|
|
355
|
+
* parts?: ({ code: string, params?: Record<string, string | number> } | null)[] }>}
|
|
350
356
|
*/
|
|
351
357
|
async function runAction(body, req) {
|
|
352
358
|
const type = String(body?.type ?? "");
|
|
@@ -355,89 +361,88 @@ async function runAction(body, req) {
|
|
|
355
361
|
case "html:clear": {
|
|
356
362
|
const size = getHtmlCacheSize();
|
|
357
363
|
clearHtmlCache();
|
|
358
|
-
return { ok: true,
|
|
364
|
+
return { ok: true, code: "html.cleared", params: { count: size } };
|
|
359
365
|
}
|
|
360
366
|
|
|
361
367
|
case "data:clear": {
|
|
362
368
|
const prefix = typeof body.prefix === "string" && body.prefix ? body.prefix : undefined;
|
|
363
369
|
const removed = clearDataCache(prefix);
|
|
364
|
-
return
|
|
365
|
-
ok: true,
|
|
366
|
-
|
|
367
|
-
? `${removed} data entries dropped under \`${prefix}\``
|
|
368
|
-
: `Data cache cleared (${removed} entries)`,
|
|
369
|
-
};
|
|
370
|
+
return prefix
|
|
371
|
+
? { ok: true, code: "data.clearedPrefix", params: { count: removed, prefix } }
|
|
372
|
+
: { ok: true, code: "data.cleared", params: { count: removed } };
|
|
370
373
|
}
|
|
371
374
|
|
|
372
375
|
case "html:invalidate": {
|
|
373
376
|
const target = String(body.target ?? "");
|
|
374
377
|
if (!target.startsWith("/")) {
|
|
375
|
-
return { ok: false,
|
|
378
|
+
return { ok: false, code: "target.invalid" };
|
|
376
379
|
}
|
|
377
380
|
|
|
378
381
|
const hard = body.hard === true;
|
|
379
382
|
const count = invalidateHtmlCache(target, { hard });
|
|
380
383
|
return {
|
|
381
384
|
ok: true,
|
|
382
|
-
|
|
385
|
+
code: hard ? "html.dropped" : "html.marked",
|
|
386
|
+
params: { count, target },
|
|
383
387
|
};
|
|
384
388
|
}
|
|
385
389
|
|
|
386
390
|
case "html:drop": {
|
|
387
391
|
const key = String(body.key ?? "");
|
|
388
|
-
if (!key) return { ok: false,
|
|
392
|
+
if (!key) return { ok: false, code: "key.missing" };
|
|
389
393
|
const existed = dropHtmlCacheKey(key);
|
|
390
|
-
return {
|
|
391
|
-
ok: true,
|
|
392
|
-
message: existed ? `Dropped \`${key}\`` : `\`${key}\` was not cached`,
|
|
393
|
-
};
|
|
394
|
+
return { ok: true, code: existed ? "entry.dropped" : "entry.absent", params: { key } };
|
|
394
395
|
}
|
|
395
396
|
|
|
396
397
|
case "data:drop": {
|
|
397
398
|
const key = String(body.key ?? "");
|
|
398
|
-
if (!key) return { ok: false,
|
|
399
|
+
if (!key) return { ok: false, code: "key.missing" };
|
|
399
400
|
const existed = dropDataCacheKey(key);
|
|
400
|
-
return {
|
|
401
|
-
ok: true,
|
|
402
|
-
message: existed ? `Dropped \`${key}\`` : `\`${key}\` was not cached`,
|
|
403
|
-
};
|
|
401
|
+
return { ok: true, code: existed ? "entry.dropped" : "entry.absent", params: { key } };
|
|
404
402
|
}
|
|
405
403
|
|
|
406
404
|
case "redis:inspect": {
|
|
407
405
|
const result = await inspectRedis();
|
|
408
|
-
if (!result.ok) return { ok: false,
|
|
409
|
-
|
|
410
|
-
const parts = [
|
|
411
|
-
`${result.html} html keys`,
|
|
412
|
-
`${result.data} data keys`,
|
|
413
|
-
result.totalKeys !== null ? `${result.totalKeys} keys in db` : null,
|
|
414
|
-
result.usedMemory ? `${result.usedMemory} used` : null,
|
|
415
|
-
].filter(Boolean);
|
|
406
|
+
if (!result.ok) return { ok: false, code: "redis.unreachable" };
|
|
416
407
|
|
|
417
|
-
|
|
408
|
+
// Sayım tek cümle değil parçalar hâlinde dönüyor: `DBSIZE` ve
|
|
409
|
+
// `used_memory` komut kısıtlı bir Redis'te okunamıyor ve eksik parçayı
|
|
410
|
+
// cümleden düşürme kararı panelin.
|
|
411
|
+
return {
|
|
412
|
+
ok: true,
|
|
413
|
+
parts: [
|
|
414
|
+
{ code: "redis.htmlKeys", params: { count: result.html } },
|
|
415
|
+
{ code: "redis.dataKeys", params: { count: result.data } },
|
|
416
|
+
result.totalKeys !== null
|
|
417
|
+
? { code: "redis.dbKeys", params: { count: result.totalKeys } }
|
|
418
|
+
: null,
|
|
419
|
+
result.usedMemory ? { code: "redis.memory", params: { value: result.usedMemory } } : null,
|
|
420
|
+
].filter(Boolean),
|
|
421
|
+
};
|
|
418
422
|
}
|
|
419
423
|
|
|
420
424
|
case "redis:drop": {
|
|
421
425
|
const kind = body.kind === "data" ? "data" : "html";
|
|
422
426
|
const status = getRedisStatus();
|
|
423
427
|
if (!status.connected) {
|
|
424
|
-
return { ok: false,
|
|
428
|
+
return { ok: false, code: "redis.notConnected" };
|
|
425
429
|
}
|
|
426
430
|
|
|
427
431
|
const dropped = await redisDropMatching(kind);
|
|
428
|
-
return {
|
|
432
|
+
return {
|
|
433
|
+
ok: true,
|
|
434
|
+
code: kind === "data" ? "redis.droppedData" : "redis.droppedHtml",
|
|
435
|
+
params: { count: dropped },
|
|
436
|
+
};
|
|
429
437
|
}
|
|
430
438
|
|
|
431
439
|
/* ------------------------------------------------------- cloudflare */
|
|
432
440
|
|
|
433
441
|
case "cf:purge-everything": {
|
|
434
442
|
const result = await purgeCloudflare({ everything: true });
|
|
435
|
-
return
|
|
436
|
-
ok:
|
|
437
|
-
|
|
438
|
-
? "Cloudflare cache purged (everything)"
|
|
439
|
-
: `Cloudflare: ${result.error}`,
|
|
440
|
-
};
|
|
443
|
+
return result.ok
|
|
444
|
+
? { ok: true, code: "cf.purgedEverything" }
|
|
445
|
+
: cloudflareFailure(result.error);
|
|
441
446
|
}
|
|
442
447
|
|
|
443
448
|
case "cf:purge-urls": {
|
|
@@ -445,23 +450,19 @@ async function runAction(body, req) {
|
|
|
445
450
|
// Origin, isteğin geldiği host'tan türetilir ki tek zone'lu kurulumda
|
|
446
451
|
// ayrıca ayar gerekmesin.
|
|
447
452
|
const paths = Array.isArray(body.paths) ? body.paths.map(String) : [];
|
|
448
|
-
if (!paths.length) return { ok: false,
|
|
453
|
+
if (!paths.length) return { ok: false, code: "cf.noPaths" };
|
|
449
454
|
|
|
450
455
|
const urls = toCloudflareUrls(paths, originOf(req));
|
|
451
|
-
if (!urls.length) {
|
|
452
|
-
return {
|
|
453
|
-
ok: false,
|
|
454
|
-
message: "Could not build absolute URLs — set cache().cloudflare.hostname",
|
|
455
|
-
};
|
|
456
|
-
}
|
|
456
|
+
if (!urls.length) return { ok: false, code: "cf.noHostname" };
|
|
457
457
|
|
|
458
458
|
const result = await purgeCloudflare({ files: urls });
|
|
459
|
-
return
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
459
|
+
return result.ok
|
|
460
|
+
? {
|
|
461
|
+
ok: true,
|
|
462
|
+
code: "cf.purgedUrls",
|
|
463
|
+
params: { count: result.purged, batches: result.batches },
|
|
464
|
+
}
|
|
465
|
+
: cloudflareFailure(result.error);
|
|
465
466
|
}
|
|
466
467
|
|
|
467
468
|
case "cf:purge-keys": {
|
|
@@ -473,51 +474,47 @@ async function runAction(body, req) {
|
|
|
473
474
|
.split(/[\s,]+/)
|
|
474
475
|
.filter(Boolean);
|
|
475
476
|
|
|
476
|
-
if (!values.length) return { ok: false,
|
|
477
|
+
if (!values.length) return { ok: false, code: "cf.nothingToPurge" };
|
|
477
478
|
|
|
478
479
|
const result = await purgeCloudflare({ [kind]: values });
|
|
479
|
-
return
|
|
480
|
-
ok: result.
|
|
481
|
-
|
|
482
|
-
? `Purged ${result.purged} ${kind} at Cloudflare`
|
|
483
|
-
: `Cloudflare: ${result.error}`,
|
|
484
|
-
};
|
|
480
|
+
return result.ok
|
|
481
|
+
? { ok: true, code: "cf.purgedKeys", params: { count: result.purged, kind } }
|
|
482
|
+
: cloudflareFailure(result.error);
|
|
485
483
|
}
|
|
486
484
|
|
|
487
485
|
case "cf:setting": {
|
|
488
486
|
const result = await setCloudflareSetting(body.id, body.value);
|
|
489
|
-
return
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
487
|
+
return result.ok
|
|
488
|
+
? {
|
|
489
|
+
ok: true,
|
|
490
|
+
code: "cf.settingChanged",
|
|
491
|
+
params: { id: String(body.id), value: String(result.value) },
|
|
492
|
+
}
|
|
493
|
+
: cloudflareFailure(result.error);
|
|
495
494
|
}
|
|
496
495
|
|
|
497
496
|
case "cf:feature": {
|
|
498
497
|
const value = body.value === "on" ? "on" : "off";
|
|
499
498
|
const result = await setCloudflareFeature(body.feature, value);
|
|
500
|
-
return
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
499
|
+
return result.ok
|
|
500
|
+
? {
|
|
501
|
+
ok: true,
|
|
502
|
+
code: "cf.featureChanged",
|
|
503
|
+
params: { feature: String(body.feature), value },
|
|
504
|
+
}
|
|
505
|
+
: cloudflareFailure(result.error);
|
|
506
506
|
}
|
|
507
507
|
|
|
508
508
|
case "cf:clear-reserve": {
|
|
509
509
|
const result = await clearCloudflareCacheReserve();
|
|
510
|
-
return
|
|
511
|
-
ok:
|
|
512
|
-
|
|
513
|
-
? "Cache Reserve clear started — it runs asynchronously at Cloudflare"
|
|
514
|
-
: `Cloudflare: ${result.error}`,
|
|
515
|
-
};
|
|
510
|
+
return result.ok
|
|
511
|
+
? { ok: true, code: "cf.reserveClearing" }
|
|
512
|
+
: cloudflareFailure(result.error);
|
|
516
513
|
}
|
|
517
514
|
|
|
518
515
|
case "prewarm": {
|
|
519
516
|
if (prewarmProgress.active) {
|
|
520
|
-
return { ok: false,
|
|
517
|
+
return { ok: false, code: "prewarm.busy" };
|
|
521
518
|
}
|
|
522
519
|
|
|
523
520
|
const requested = Array.isArray(body.paths) ? body.paths : [];
|
|
@@ -535,17 +532,27 @@ async function runAction(body, req) {
|
|
|
535
532
|
console.error("[cache-panel] prewarm failed", error);
|
|
536
533
|
});
|
|
537
534
|
|
|
538
|
-
return
|
|
539
|
-
ok: true,
|
|
540
|
-
|
|
541
|
-
};
|
|
535
|
+
return paths.length
|
|
536
|
+
? { ok: true, code: "prewarm.paths", params: { count: paths.length } }
|
|
537
|
+
: { ok: true, code: "prewarm.all" };
|
|
542
538
|
}
|
|
543
539
|
|
|
544
540
|
default:
|
|
545
|
-
return { ok: false,
|
|
541
|
+
return { ok: false, code: "action.unknown", params: { type } };
|
|
546
542
|
}
|
|
547
543
|
}
|
|
548
544
|
|
|
545
|
+
/**
|
|
546
|
+
* Cloudflare hatası tek bir kodla döner: hata metni Cloudflare'den geliyor ve
|
|
547
|
+
* çevrilemez, ama etrafındaki cümle çevrilebilir.
|
|
548
|
+
*
|
|
549
|
+
* @param {string | undefined} error
|
|
550
|
+
* @returns {{ ok: false, code: string, params: { error: string } }}
|
|
551
|
+
*/
|
|
552
|
+
function cloudflareFailure(error) {
|
|
553
|
+
return { ok: false, code: "cf.failed", params: { error: error ?? "unknown error" } };
|
|
554
|
+
}
|
|
555
|
+
|
|
549
556
|
/* ------------------------------------------------------------------ router */
|
|
550
557
|
|
|
551
558
|
/**
|
|
@@ -591,6 +598,12 @@ function router() {
|
|
|
591
598
|
sendFile(res, "panel.css");
|
|
592
599
|
});
|
|
593
600
|
|
|
601
|
+
// Sözlük giriş sayfasında da gerekiyor: dil seçimi oturumdan önce yapılıyor.
|
|
602
|
+
api.get("/i18n.js", (req, res) => {
|
|
603
|
+
res.type("application/javascript");
|
|
604
|
+
sendFile(res, "i18n.js");
|
|
605
|
+
});
|
|
606
|
+
|
|
594
607
|
api.get("/logo.png", (req, res) => {
|
|
595
608
|
res.type("image/png");
|
|
596
609
|
fs.createReadStream(path.join(FRAMEWORK_ROOT, "src", "logo.png")).pipe(res);
|
|
@@ -634,7 +647,15 @@ function router() {
|
|
|
634
647
|
api.use((req, res, next) => {
|
|
635
648
|
if (authenticated(req)) return next();
|
|
636
649
|
|
|
637
|
-
|
|
650
|
+
// Yetkisiz **okuma** sayaca yazılmaz. Süreç yeniden başladığında eski
|
|
651
|
+
// oturumlar ölüyor ve açık kalmış bir panel sekmesi yoklamaya devam
|
|
652
|
+
// ediyor: sayarsak operatör kendi IP'sini üç saniyede yasaklıyor. Okuma
|
|
653
|
+
// zaten `404` dönüyor, yani saymamanın bir bedeli yok — korunması gereken
|
|
654
|
+
// şey şifre denemesi ve durumu değiştiren istek.
|
|
655
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
656
|
+
noteFailure(req, `unauthenticated ${req.method} ${req.path}`);
|
|
657
|
+
}
|
|
658
|
+
|
|
638
659
|
notFound(res);
|
|
639
660
|
});
|
|
640
661
|
|