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
package/src/config/index.js
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* headers() → [{ source, headers: [{ key, value }] }]
|
|
16
16
|
* redirects() → [{ source, destination, permanent?, statusCode? }]
|
|
17
17
|
* rewrites() → [{ source, destination }] | { beforeFiles?, afterFiles? }
|
|
18
|
-
* cache() → { html?: { [source]: saniye },
|
|
18
|
+
* cache() → { html?: { [source]: saniye },
|
|
19
|
+
* query?: { [source]: string[] | true }, maxEntries?: number,
|
|
19
20
|
* data?: {...}, redis?: {...}, prewarm?: {...},
|
|
20
21
|
* panel?: {...} }
|
|
21
22
|
*
|
|
@@ -30,6 +31,7 @@ import { compilePattern, matchPattern } from "./pattern.js";
|
|
|
30
31
|
import {
|
|
31
32
|
DEFAULT_BRAND,
|
|
32
33
|
DEFAULT_CACHE_PANEL,
|
|
34
|
+
DEFAULT_CLOUDFLARE,
|
|
33
35
|
DEFAULT_DATA_CACHE,
|
|
34
36
|
DEFAULT_DEV_GATE_BYPASS,
|
|
35
37
|
DEFAULT_DIRS,
|
|
@@ -84,6 +86,9 @@ const CONFIG_FILE = "jskelet.config.mjs";
|
|
|
84
86
|
* @property {{ pattern: CompiledPattern, destination: string, statusCode: number }[]} redirects
|
|
85
87
|
* @property {{ phase: "beforeFiles" | "afterFiles", pattern: CompiledPattern, destination: string }[]} rewrites
|
|
86
88
|
* @property {{ pattern: CompiledPattern, seconds: number }[]} html
|
|
89
|
+
* @property {{ pattern: CompiledPattern, allow: true | string[] }[]} cacheQuery
|
|
90
|
+
* Yol deseni başına, HTML cache anahtarına girmesine izin verilen query
|
|
91
|
+
* parametreleri. Eşleşen kural yoksa query'li istek cache'lenmez.
|
|
87
92
|
* @property {number} htmlMaxEntries HTML önbelleğinin girdi sınırı.
|
|
88
93
|
* @property {Record<string, unknown>} data Upstream veri önbelleği ayarları.
|
|
89
94
|
* @property {boolean} trackUpstream `fetch` sarılıp geçici hatalar otomatik bildirilsin mi.
|
|
@@ -92,6 +97,7 @@ const CONFIG_FILE = "jskelet.config.mjs";
|
|
|
92
97
|
* @property {RedisConfig} redis Opsiyonel Redis ikinci kademesi.
|
|
93
98
|
* @property {typeof DEFAULT_UPSTREAM_LIMIT} upstream Upstream hız freni.
|
|
94
99
|
* @property {typeof DEFAULT_CACHE_PANEL} cachePanel Önbellek yönetim paneli.
|
|
100
|
+
* @property {typeof DEFAULT_CLOUDFLARE} cloudflare Cloudflare cache yüzeyi.
|
|
95
101
|
* @property {Record<string, unknown>} prewarm
|
|
96
102
|
* @property {{ source: string, test: (pathname: string) => boolean }[]} prewarmPriority
|
|
97
103
|
* @property {Record<string, unknown>} brand
|
|
@@ -338,15 +344,87 @@ function normalizeCachePanel(raw) {
|
|
|
338
344
|
};
|
|
339
345
|
}
|
|
340
346
|
|
|
347
|
+
/**
|
|
348
|
+
* Cloudflare bölümü. Token burada da verilebiliyor ama önerilen yol env;
|
|
349
|
+
* normalizasyon sadece tipleri sabitler, sırrı okumak `cloudflare.js`'in işi.
|
|
350
|
+
*
|
|
351
|
+
* @param {unknown} raw
|
|
352
|
+
* @returns {typeof DEFAULT_CLOUDFLARE}
|
|
353
|
+
*/
|
|
354
|
+
function normalizeCloudflare(raw) {
|
|
355
|
+
const source = /** @type {Record<string, any>} */ (raw ?? {});
|
|
356
|
+
const hours = Number(source.analyticsHours);
|
|
357
|
+
|
|
358
|
+
/** @param {unknown} value */
|
|
359
|
+
const text = (value) => (typeof value === "string" && value ? value : null);
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
enabled: source.enabled !== false,
|
|
363
|
+
zoneId: text(source.zoneId),
|
|
364
|
+
apiToken: text(source.apiToken),
|
|
365
|
+
// Şema yazılırsa purge URL'i `https://https://…` olur; baştaki şema atılır.
|
|
366
|
+
hostname: text(source.hostname)?.replace(/^https?:\/\//, "") ?? null,
|
|
367
|
+
analyticsHours:
|
|
368
|
+
Number.isFinite(hours) && hours > 0
|
|
369
|
+
? Math.min(72, Math.floor(hours))
|
|
370
|
+
: DEFAULT_CLOUDFLARE.analyticsHours,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* `cache().query` → yol deseni başına, cache anahtarına girmesine izin verilen
|
|
376
|
+
* query parametreleri.
|
|
377
|
+
*
|
|
378
|
+
* Varsayılan bilinçli olarak "query varsa sayfa dinamik": bir yolun bütün
|
|
379
|
+
* query varyantlarını cache'lemek, `?utm_source=…` gibi sonsuz sayıda anahtar
|
|
380
|
+
* üretip LRU'daki gerçek sayfaları dışarı atıyor. Hangi parametrenin çıktıyı
|
|
381
|
+
* gerçekten değiştirdiğini yalnızca uygulama bilir, o yüzden izin listesi
|
|
382
|
+
* config'ten gelir.
|
|
383
|
+
*
|
|
384
|
+
* Bir desen `true` ile eşlenirse bütün parametreler anahtara girer (eski
|
|
385
|
+
* davranış), `[]` ile eşlenirse hiçbiri girmez — yani query yok sayılır ve
|
|
386
|
+
* bütün varyantlar query'siz sürümün HTML'ini paylaşır.
|
|
387
|
+
*
|
|
388
|
+
* @param {unknown} raw
|
|
389
|
+
* @returns {ResolvedConfig["cacheQuery"]}
|
|
390
|
+
*/
|
|
391
|
+
function normalizeQueryRules(raw) {
|
|
392
|
+
/** @type {ResolvedConfig["cacheQuery"]} */
|
|
393
|
+
const out = [];
|
|
394
|
+
|
|
395
|
+
for (const [source, value] of Object.entries(raw ?? {})) {
|
|
396
|
+
const pattern = compilePattern(source);
|
|
397
|
+
if (!pattern) continue;
|
|
398
|
+
|
|
399
|
+
if (value === true) {
|
|
400
|
+
out.push({ pattern, allow: true });
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (value === false) continue;
|
|
404
|
+
|
|
405
|
+
const allow = asArray(
|
|
406
|
+
typeof value === "string" ? [value] : value,
|
|
407
|
+
`cache().query["${source}"]`,
|
|
408
|
+
)
|
|
409
|
+
.filter((name) => typeof name === "string" && name)
|
|
410
|
+
.map(String);
|
|
411
|
+
out.push({ pattern, allow });
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return out;
|
|
415
|
+
}
|
|
416
|
+
|
|
341
417
|
/**
|
|
342
418
|
* @param {unknown} raw
|
|
343
|
-
* @returns {{ html: ResolvedConfig["html"],
|
|
419
|
+
* @returns {{ html: ResolvedConfig["html"],
|
|
420
|
+
* cacheQuery: ResolvedConfig["cacheQuery"], htmlMaxEntries: number,
|
|
344
421
|
* data: Record<string, unknown>, trackUpstream: boolean,
|
|
345
422
|
* trackDependencies: boolean,
|
|
346
423
|
* transientRetry: { attempts: number, delayMs: number },
|
|
347
424
|
* redis: RedisConfig,
|
|
348
425
|
* upstream: typeof DEFAULT_UPSTREAM_LIMIT,
|
|
349
426
|
* cachePanel: typeof DEFAULT_CACHE_PANEL,
|
|
427
|
+
* cloudflare: typeof DEFAULT_CLOUDFLARE,
|
|
350
428
|
* prewarm: Record<string, unknown>,
|
|
351
429
|
* prewarmPriority: ResolvedConfig["prewarmPriority"] }}
|
|
352
430
|
*/
|
|
@@ -362,10 +440,12 @@ function normalizeCache(raw) {
|
|
|
362
440
|
}
|
|
363
441
|
|
|
364
442
|
const prewarm = { ...DEFAULT_PREWARM, ...(raw?.prewarm ?? {}) };
|
|
443
|
+
const queryRules = normalizeQueryRules(raw?.query);
|
|
365
444
|
const maxEntries = Number(raw?.maxEntries);
|
|
366
445
|
|
|
367
446
|
return {
|
|
368
447
|
html,
|
|
448
|
+
cacheQuery: queryRules,
|
|
369
449
|
htmlMaxEntries:
|
|
370
450
|
Number.isFinite(maxEntries) && maxEntries > 0
|
|
371
451
|
? Math.floor(maxEntries)
|
|
@@ -385,6 +465,7 @@ function normalizeCache(raw) {
|
|
|
385
465
|
redis: normalizeRedis(raw?.redis),
|
|
386
466
|
upstream: normalizeUpstream(raw?.upstream),
|
|
387
467
|
cachePanel: normalizeCachePanel(raw?.panel),
|
|
468
|
+
cloudflare: normalizeCloudflare(raw?.cloudflare),
|
|
388
469
|
// Desenler derlenmiş hâlde ayrı alanda tutulur: `prewarm` sayısal
|
|
389
470
|
// ayarların düz torbası olarak kalsın, her turda yeniden derlenmesin.
|
|
390
471
|
prewarm,
|
|
@@ -597,6 +678,7 @@ export async function loadConfig(options = {}) {
|
|
|
597
678
|
|
|
598
679
|
const {
|
|
599
680
|
html,
|
|
681
|
+
cacheQuery,
|
|
600
682
|
htmlMaxEntries,
|
|
601
683
|
data,
|
|
602
684
|
trackUpstream,
|
|
@@ -605,6 +687,7 @@ export async function loadConfig(options = {}) {
|
|
|
605
687
|
redis,
|
|
606
688
|
upstream,
|
|
607
689
|
cachePanel,
|
|
690
|
+
cloudflare,
|
|
608
691
|
prewarm,
|
|
609
692
|
prewarmPriority,
|
|
610
693
|
} = normalizeCache(cache);
|
|
@@ -619,6 +702,7 @@ export async function loadConfig(options = {}) {
|
|
|
619
702
|
redirects: normalizeRedirects(redirects),
|
|
620
703
|
rewrites: normalizeRewrites(rewrites),
|
|
621
704
|
html,
|
|
705
|
+
cacheQuery,
|
|
622
706
|
htmlMaxEntries,
|
|
623
707
|
data,
|
|
624
708
|
trackUpstream,
|
|
@@ -627,6 +711,7 @@ export async function loadConfig(options = {}) {
|
|
|
627
711
|
redis,
|
|
628
712
|
upstream,
|
|
629
713
|
cachePanel,
|
|
714
|
+
cloudflare,
|
|
630
715
|
prewarm,
|
|
631
716
|
prewarmPriority,
|
|
632
717
|
brand,
|
package/src/index.js
CHANGED
|
@@ -58,7 +58,22 @@ export {
|
|
|
58
58
|
} from "./server/data-cache.js";
|
|
59
59
|
// Paylaşımlı önbellek kademesinin durumu. Bağlantı kurulmamışken de güvenle
|
|
60
60
|
// çağrılır; healthcheck uçları bunu okuyor.
|
|
61
|
-
|
|
61
|
+
// `getRedisDetails()` bağlantının nereye kurulduğunu ve hangi türleri
|
|
62
|
+
// paylaştığını söyler (şifre asla dönmez); `inspectRedis()` tür başına anahtar
|
|
63
|
+
// sayar — bir `SCAN` turu olduğu için istek yolunda çağrılmamalı.
|
|
64
|
+
export { getRedisDetails, getRedisStatus, inspectRedis } from "./server/redis.js";
|
|
65
|
+
// CDN kademesi. JSkelet'in önbelleği origin önbelleği; ziyaretçinin gördüğü
|
|
66
|
+
// kopya edge'de duruyor. Bir içeriği gerçekten tazelemek için ikisini birlikte
|
|
67
|
+
// düşürmek gerekiyor: `invalidateHtmlCache()` + `purgeCloudflare()`.
|
|
68
|
+
export {
|
|
69
|
+
cloudflareConfigured,
|
|
70
|
+
fetchCacheAnalytics,
|
|
71
|
+
fetchCloudflareOverview,
|
|
72
|
+
fetchPathEdges,
|
|
73
|
+
getCloudflareStatus,
|
|
74
|
+
purgeCloudflare,
|
|
75
|
+
toCloudflareUrls,
|
|
76
|
+
} from "./server/cloudflare.js";
|
|
62
77
|
// Upstream hız freninin host başına durumu: healthcheck ve teşhis uçları için.
|
|
63
78
|
export { getUpstreamLimiterStatus } from "./server/upstream-limiter.js";
|
|
64
79
|
export { prewarm, prewarmProgress } from "./server/prewarm.js";
|
package/src/log.mjs
CHANGED
|
@@ -284,6 +284,31 @@ function truncate(text, max) {
|
|
|
284
284
|
/** Kutunun içi sabit genişlikte; uzun satırlar kırpılır. */
|
|
285
285
|
const BOX = 52;
|
|
286
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Çerçeveli bilgi kutusu.
|
|
289
|
+
*
|
|
290
|
+
* Açılış logunda kaybolmaması gereken tek şey sır: önbellek panelinin şifresi
|
|
291
|
+
* her restart'ta değişiyor ve kullanıcı onu bir kez, akışın içinde görüyor.
|
|
292
|
+
* Genişlik içeriğe göre büyür — bir URL'i ya da 32 haneli bir şifreyi
|
|
293
|
+
* kırpmak kutunun bütün amacını bozar.
|
|
294
|
+
*
|
|
295
|
+
* @param {{ title: string, lines: string[],
|
|
296
|
+
* tint?: (value: string) => string }} info `lines` içinde boş string ayırıcı
|
|
297
|
+
* satır olur. Satırlar **düz metin** olmalı: bir ANSI dizisi `length`e
|
|
298
|
+
* sayıldığı için hizalamayı bozar, rengi çerçeve taşır.
|
|
299
|
+
*/
|
|
300
|
+
export function box({ title, lines, tint = c.cyan }) {
|
|
301
|
+
const width = Math.max(BOX, title.length + 4, ...lines.map((text) => text.length + 4));
|
|
302
|
+
const top = `┌─ ${title} ${"─".repeat(width - title.length - 3)}┐`;
|
|
303
|
+
const bottom = `└${"─".repeat(width)}┘`;
|
|
304
|
+
|
|
305
|
+
write(`\n${tint(top)}\n`);
|
|
306
|
+
for (const text of ["", ...lines, ""]) {
|
|
307
|
+
write(`${tint("│")} ${text}${" ".repeat(width - 4 - text.length)} ${tint("│")}\n`);
|
|
308
|
+
}
|
|
309
|
+
write(`${tint(bottom)}\n\n`);
|
|
310
|
+
}
|
|
311
|
+
|
|
287
312
|
/**
|
|
288
313
|
* Çerçeveli hata kutusu — kendi framework'ünü geliştirirken hatanın
|
|
289
314
|
* akış içinde kaybolmaması için.
|
|
@@ -26,10 +26,18 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import crypto from "node:crypto";
|
|
28
28
|
import fs from "node:fs";
|
|
29
|
+
import os from "node:os";
|
|
29
30
|
import path from "node:path";
|
|
30
31
|
import process from "node:process";
|
|
31
32
|
import express from "express";
|
|
33
|
+
import * as log from "../log.mjs";
|
|
32
34
|
import { FRAMEWORK_ROOT, getConfig } from "../config/index.js";
|
|
35
|
+
import {
|
|
36
|
+
FRAMEWORK_HOMEPAGE,
|
|
37
|
+
FRAMEWORK_LICENSE,
|
|
38
|
+
FRAMEWORK_NODE_RANGE,
|
|
39
|
+
FRAMEWORK_VERSION,
|
|
40
|
+
} from "../version.mjs";
|
|
33
41
|
import { safeEqual } from "../http/cookies.js";
|
|
34
42
|
import {
|
|
35
43
|
clearHtmlCache,
|
|
@@ -44,8 +52,25 @@ import {
|
|
|
44
52
|
getDataCacheEntries,
|
|
45
53
|
getDataCacheSize,
|
|
46
54
|
} from "./data-cache.js";
|
|
47
|
-
import {
|
|
55
|
+
import {
|
|
56
|
+
getRedisDetails,
|
|
57
|
+
getRedisStatus,
|
|
58
|
+
inspectRedis,
|
|
59
|
+
redisDropMatching,
|
|
60
|
+
} from "./redis.js";
|
|
48
61
|
import { getUpstreamLimiterStatus } from "./upstream-limiter.js";
|
|
62
|
+
import {
|
|
63
|
+
clearCloudflareCacheReserve,
|
|
64
|
+
cloudflareConfigured,
|
|
65
|
+
fetchCacheAnalytics,
|
|
66
|
+
fetchCloudflareOverview,
|
|
67
|
+
fetchPathEdges,
|
|
68
|
+
getCloudflareStatus,
|
|
69
|
+
purgeCloudflare,
|
|
70
|
+
setCloudflareFeature,
|
|
71
|
+
setCloudflareSetting,
|
|
72
|
+
toCloudflareUrls,
|
|
73
|
+
} from "./cloudflare.js";
|
|
49
74
|
import { prewarm, prewarmProgress } from "./prewarm.js";
|
|
50
75
|
|
|
51
76
|
/** Panel dosyaları framework paketinden servis edilir, uygulamadan değil. */
|
|
@@ -199,9 +224,9 @@ function notFound(res) {
|
|
|
199
224
|
* göndermenin faydası yok.
|
|
200
225
|
*
|
|
201
226
|
* @param {string} query Anahtar filtresi (boş → filtre yok).
|
|
202
|
-
* @returns {object}
|
|
227
|
+
* @returns {Promise<object>}
|
|
203
228
|
*/
|
|
204
|
-
function snapshot(query) {
|
|
229
|
+
async function snapshot(query) {
|
|
205
230
|
const term = query.trim().toLowerCase();
|
|
206
231
|
/** @param {{ key: string }} entry */
|
|
207
232
|
const matches = (entry) => !term || entry.key.toLowerCase().includes(term);
|
|
@@ -216,6 +241,13 @@ function snapshot(query) {
|
|
|
216
241
|
return {
|
|
217
242
|
boot: BOOT_ID,
|
|
218
243
|
generatedAt: Date.now(),
|
|
244
|
+
release: {
|
|
245
|
+
version: FRAMEWORK_VERSION,
|
|
246
|
+
license: FRAMEWORK_LICENSE,
|
|
247
|
+
node: FRAMEWORK_NODE_RANGE,
|
|
248
|
+
homepage: FRAMEWORK_HOMEPAGE,
|
|
249
|
+
},
|
|
250
|
+
host: await hostStatus(),
|
|
219
251
|
process: {
|
|
220
252
|
pid: process.pid,
|
|
221
253
|
node: process.version,
|
|
@@ -231,7 +263,14 @@ function snapshot(query) {
|
|
|
231
263
|
matched: htmlMatched.length,
|
|
232
264
|
entries: htmlMatched
|
|
233
265
|
.sort((a, b) => b.bytes - a.bytes)
|
|
234
|
-
.slice(0, MAX_LISTED)
|
|
266
|
+
.slice(0, MAX_LISTED)
|
|
267
|
+
// Anahtar `yol?query`; query boşken sonda yalnız bir `?` kalıyor.
|
|
268
|
+
// Silme işlemleri gerçek anahtarı kullanmaya devam eder, panel
|
|
269
|
+
// ziyaret edilebilir yolu gösterir.
|
|
270
|
+
.map((entry) => ({
|
|
271
|
+
...entry,
|
|
272
|
+
url: entry.key.endsWith("?") ? entry.key.slice(0, -1) : entry.key,
|
|
273
|
+
})),
|
|
235
274
|
},
|
|
236
275
|
data: {
|
|
237
276
|
size: getDataCacheSize(),
|
|
@@ -242,12 +281,60 @@ function snapshot(query) {
|
|
|
242
281
|
.sort((a, b) => a.expiresIn - b.expiresIn)
|
|
243
282
|
.slice(0, MAX_LISTED),
|
|
244
283
|
},
|
|
245
|
-
redis: getRedisStatus(),
|
|
284
|
+
redis: { ...getRedisStatus(), ...getRedisDetails() },
|
|
285
|
+
// Cloudflare yalnızca kurulum özeti: zone bilgisi ve analitik ağa çıkıyor
|
|
286
|
+
// ve döküm birkaç saniyede bir yenileniyor. Ayrıntı ayrı uçta.
|
|
287
|
+
cloudflare: getCloudflareStatus(),
|
|
246
288
|
upstream: getUpstreamLimiterStatus(),
|
|
247
289
|
prewarm: { ...prewarmProgress },
|
|
248
290
|
};
|
|
249
291
|
}
|
|
250
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Makinenin RAM ve disk durumu.
|
|
295
|
+
*
|
|
296
|
+
* Paylaşımlı kademe kapalıyken önbelleğin tamamı bu sürecin belleğinde
|
|
297
|
+
* yaşıyor ve `maxEntries` ile RAM arasındaki ilişki panelde görünmediği sürece
|
|
298
|
+
* ayarlanamıyor: "500 girdi çok mu" sorusunun cevabı makinede ne kadar boş
|
|
299
|
+
* bellek kaldığına bağlı. Disk, HTML önbelleğinin kendisi için değil, ısıtma
|
|
300
|
+
* ve build çıktısının yaşadığı yer için: dolu bir diskte `jskelet build`
|
|
301
|
+
* sessizce hatalı manifest bırakıyor.
|
|
302
|
+
*
|
|
303
|
+
* `statfs` desteklenmeyen bir platform ya da erişilemeyen bir yol sadece
|
|
304
|
+
* `null` disk demek; panel yine açılır.
|
|
305
|
+
*
|
|
306
|
+
* @returns {Promise<object>}
|
|
307
|
+
*/
|
|
308
|
+
async function hostStatus() {
|
|
309
|
+
const total = os.totalmem();
|
|
310
|
+
const free = os.freemem();
|
|
311
|
+
|
|
312
|
+
/** @type {{ path: string, total: number, free: number } | null} */
|
|
313
|
+
let disk = null;
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
const root = getConfig().root;
|
|
317
|
+
const stats = await fs.promises.statfs(root);
|
|
318
|
+
disk = {
|
|
319
|
+
path: root,
|
|
320
|
+
total: stats.blocks * stats.bsize,
|
|
321
|
+
// `bavail`, `bfree` değil: ayrılmış blokları boş göstermek yanıltıcı.
|
|
322
|
+
free: stats.bavail * stats.bsize,
|
|
323
|
+
};
|
|
324
|
+
} catch {
|
|
325
|
+
disk = null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
platform: `${os.platform()} ${os.arch()}`,
|
|
330
|
+
cpus: os.cpus().length,
|
|
331
|
+
// Windows'ta `loadavg()` her zaman sıfır döner; panel bunu gizler.
|
|
332
|
+
load: os.loadavg()[0],
|
|
333
|
+
memory: { total, free, used: total - free },
|
|
334
|
+
disk,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
251
338
|
/* --------------------------------------------------------------- aksiyonlar */
|
|
252
339
|
|
|
253
340
|
/**
|
|
@@ -316,6 +403,20 @@ async function runAction(body, req) {
|
|
|
316
403
|
};
|
|
317
404
|
}
|
|
318
405
|
|
|
406
|
+
case "redis:inspect": {
|
|
407
|
+
const result = await inspectRedis();
|
|
408
|
+
if (!result.ok) return { ok: false, message: "Redis is not reachable" };
|
|
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);
|
|
416
|
+
|
|
417
|
+
return { ok: true, message: parts.join(" · ") };
|
|
418
|
+
}
|
|
419
|
+
|
|
319
420
|
case "redis:drop": {
|
|
320
421
|
const kind = body.kind === "data" ? "data" : "html";
|
|
321
422
|
const status = getRedisStatus();
|
|
@@ -327,6 +428,93 @@ async function runAction(body, req) {
|
|
|
327
428
|
return { ok: true, message: `${dropped} shared ${kind} keys dropped` };
|
|
328
429
|
}
|
|
329
430
|
|
|
431
|
+
/* ------------------------------------------------------- cloudflare */
|
|
432
|
+
|
|
433
|
+
case "cf:purge-everything": {
|
|
434
|
+
const result = await purgeCloudflare({ everything: true });
|
|
435
|
+
return {
|
|
436
|
+
ok: result.ok,
|
|
437
|
+
message: result.ok
|
|
438
|
+
? "Cloudflare cache purged (everything)"
|
|
439
|
+
: `Cloudflare: ${result.error}`,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
case "cf:purge-urls": {
|
|
444
|
+
// Panelin elindeki HTML anahtarları yol; Cloudflare tam URL istiyor.
|
|
445
|
+
// Origin, isteğin geldiği host'tan türetilir ki tek zone'lu kurulumda
|
|
446
|
+
// ayrıca ayar gerekmesin.
|
|
447
|
+
const paths = Array.isArray(body.paths) ? body.paths.map(String) : [];
|
|
448
|
+
if (!paths.length) return { ok: false, message: "No paths given" };
|
|
449
|
+
|
|
450
|
+
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
|
+
}
|
|
457
|
+
|
|
458
|
+
const result = await purgeCloudflare({ files: urls });
|
|
459
|
+
return {
|
|
460
|
+
ok: result.ok,
|
|
461
|
+
message: result.ok
|
|
462
|
+
? `Purged ${result.purged} URLs at Cloudflare in ${result.batches} request(s)`
|
|
463
|
+
: `Cloudflare: ${result.error}`,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
case "cf:purge-keys": {
|
|
468
|
+
/** @type {"prefixes" | "hosts" | "tags"} */
|
|
469
|
+
const kind =
|
|
470
|
+
body.kind === "hosts" ? "hosts" : body.kind === "tags" ? "tags" : "prefixes";
|
|
471
|
+
|
|
472
|
+
const values = String(body.values ?? "")
|
|
473
|
+
.split(/[\s,]+/)
|
|
474
|
+
.filter(Boolean);
|
|
475
|
+
|
|
476
|
+
if (!values.length) return { ok: false, message: "Nothing to purge" };
|
|
477
|
+
|
|
478
|
+
const result = await purgeCloudflare({ [kind]: values });
|
|
479
|
+
return {
|
|
480
|
+
ok: result.ok,
|
|
481
|
+
message: result.ok
|
|
482
|
+
? `Purged ${result.purged} ${kind} at Cloudflare`
|
|
483
|
+
: `Cloudflare: ${result.error}`,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
case "cf:setting": {
|
|
488
|
+
const result = await setCloudflareSetting(body.id, body.value);
|
|
489
|
+
return {
|
|
490
|
+
ok: result.ok,
|
|
491
|
+
message: result.ok
|
|
492
|
+
? `Cloudflare ${body.id} is now ${result.value}`
|
|
493
|
+
: `Cloudflare: ${result.error}`,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
case "cf:feature": {
|
|
498
|
+
const value = body.value === "on" ? "on" : "off";
|
|
499
|
+
const result = await setCloudflareFeature(body.feature, value);
|
|
500
|
+
return {
|
|
501
|
+
ok: result.ok,
|
|
502
|
+
message: result.ok
|
|
503
|
+
? `Cloudflare ${body.feature} turned ${value}`
|
|
504
|
+
: `Cloudflare: ${result.error}`,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
case "cf:clear-reserve": {
|
|
509
|
+
const result = await clearCloudflareCacheReserve();
|
|
510
|
+
return {
|
|
511
|
+
ok: result.ok,
|
|
512
|
+
message: result.ok
|
|
513
|
+
? "Cache Reserve clear started — it runs asynchronously at Cloudflare"
|
|
514
|
+
: `Cloudflare: ${result.error}`,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
330
518
|
case "prewarm": {
|
|
331
519
|
if (prewarmProgress.active) {
|
|
332
520
|
return { ok: false, message: "A prewarm round is already running" };
|
|
@@ -396,13 +584,18 @@ function router() {
|
|
|
396
584
|
sendFile(res, authenticated(req) ? "panel.html" : "login.html");
|
|
397
585
|
});
|
|
398
586
|
|
|
399
|
-
// Stil giriş sayfasında da gerekiyor, yani oturumdan önce servis
|
|
400
|
-
//
|
|
587
|
+
// Stil ve logo giriş sayfasında da gerekiyor, yani oturumdan önce servis
|
|
588
|
+
// edilir. İkisinde de durum bilgisi yok.
|
|
401
589
|
api.get("/panel.css", (req, res) => {
|
|
402
590
|
res.type("text/css");
|
|
403
591
|
sendFile(res, "panel.css");
|
|
404
592
|
});
|
|
405
593
|
|
|
594
|
+
api.get("/logo.png", (req, res) => {
|
|
595
|
+
res.type("image/png");
|
|
596
|
+
fs.createReadStream(path.join(FRAMEWORK_ROOT, "src", "logo.png")).pipe(res);
|
|
597
|
+
});
|
|
598
|
+
|
|
406
599
|
api.post("/login", express.json({ limit: "4kb" }), (req, res) => {
|
|
407
600
|
const password = typeof req.body?.password === "string" ? req.body.password : "";
|
|
408
601
|
|
|
@@ -457,9 +650,29 @@ function router() {
|
|
|
457
650
|
sendFile(res, "panel.js");
|
|
458
651
|
});
|
|
459
652
|
|
|
460
|
-
api.get("/data", (req, res) => {
|
|
653
|
+
api.get("/data", async (req, res) => {
|
|
461
654
|
const query = typeof req.query.q === "string" ? req.query.q : "";
|
|
462
|
-
res.json(snapshot(query));
|
|
655
|
+
res.json(await snapshot(query));
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
// Cloudflare ayrı uçlarda: her ikisi de ağa çıkıyor ve döküm turuna
|
|
659
|
+
// bağlanırlarsa panel Cloudflare'in gecikmesi kadar yavaşlar.
|
|
660
|
+
api.get("/cloudflare", async (req, res) => {
|
|
661
|
+
res.json(await fetchCloudflareOverview({ force: req.query.force === "1" }));
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
api.post("/cloudflare/analytics", express.json({ limit: "8kb" }), async (req, res) => {
|
|
665
|
+
if (!cloudflareConfigured()) {
|
|
666
|
+
res.json({ ok: false, error: "not configured" });
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const hours = Number(req.body?.hours) || undefined;
|
|
671
|
+
const path = typeof req.body?.path === "string" ? req.body.path.trim() : "";
|
|
672
|
+
|
|
673
|
+
// Yol verildiyse soru "bu sayfa hangi edge'lerden servis edildi", yoksa
|
|
674
|
+
// "zone genelinde cache oranı ne".
|
|
675
|
+
res.json(path ? await fetchPathEdges({ path, hours }) : await fetchCacheAnalytics({ hours }));
|
|
463
676
|
});
|
|
464
677
|
|
|
465
678
|
api.post("/action", express.json({ limit: "64kb" }), async (req, res) => {
|
|
@@ -479,6 +692,18 @@ function router() {
|
|
|
479
692
|
return api;
|
|
480
693
|
}
|
|
481
694
|
|
|
695
|
+
/**
|
|
696
|
+
* Purge URL'lerinin kökü. `cloudflare.hostname` verilmediğinde panelin
|
|
697
|
+
* açıldığı origin kullanılır: tek zone'lu bir kurulumda ikisi zaten aynı.
|
|
698
|
+
*
|
|
699
|
+
* @param {import('express').Request} req
|
|
700
|
+
* @returns {string | undefined}
|
|
701
|
+
*/
|
|
702
|
+
function originOf(req) {
|
|
703
|
+
const host = req.get("host");
|
|
704
|
+
return host ? `${req.protocol}://${host}` : undefined;
|
|
705
|
+
}
|
|
706
|
+
|
|
482
707
|
/**
|
|
483
708
|
* @param {import('express').Response} res
|
|
484
709
|
* @param {string} name
|
|
@@ -497,9 +722,17 @@ export function mountCachePanel(app) {
|
|
|
497
722
|
|
|
498
723
|
app.use(settings.basePath, router());
|
|
499
724
|
|
|
725
|
+
// Şifre akışın içinde tek bir satır olarak kaybolmamalı: kullanıcı onu bir
|
|
726
|
+
// kez görüyor ve her restart'ta değişiyor.
|
|
500
727
|
const port = process.env.PORT ?? 3000;
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
728
|
+
log.box({
|
|
729
|
+
title: "CACHE PANEL",
|
|
730
|
+
lines: [
|
|
731
|
+
`http://localhost:${port}${settings.basePath}`,
|
|
732
|
+
"",
|
|
733
|
+
`password ${PASSWORD}`,
|
|
734
|
+
"",
|
|
735
|
+
"Valid until this process restarts.",
|
|
736
|
+
],
|
|
737
|
+
});
|
|
505
738
|
}
|