jskelet 0.2.4 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +132 -132
- package/CHANGELOG.md +8 -0
- package/LICENSE +21 -21
- package/bin/jskelet.mjs +103 -103
- package/docs/01-baslangic.md +285 -285
- package/docs/02-mimari.md +287 -287
- package/docs/03-routing.md +480 -480
- package/docs/04-render-ve-sablonlar.md +490 -490
- package/docs/05-islands.md +482 -482
- package/docs/06-cache.md +1209 -1209
- package/docs/08-build.md +366 -366
- package/docs/09-dev-araclari.md +335 -335
- package/docs/10-dagitim.md +329 -329
- package/docs/12-panel-ve-oturum.md +384 -384
- package/docs/README.md +105 -105
- package/docs/en/01-getting-started.md +292 -292
- package/docs/en/02-architecture.md +305 -305
- package/docs/en/03-routing.md +497 -497
- package/docs/en/04-rendering.md +504 -504
- package/docs/en/05-islands.md +492 -492
- package/docs/en/06-caching.md +1239 -1239
- package/docs/en/07-configuration.md +986 -986
- package/docs/en/08-build.md +383 -383
- package/docs/en/09-dev-tools.md +342 -342
- package/docs/en/10-deployment.md +332 -332
- package/docs/en/11-migration.md +359 -359
- package/docs/en/12-dashboards-and-sessions.md +392 -392
- package/docs/en/README.md +112 -112
- package/package.json +102 -102
- package/src/build/ensure-build.mjs +15 -15
- package/src/build/paths.mjs +143 -143
- package/src/build/resolve-peer.mjs +36 -36
- package/src/build/tasks/client.mjs +268 -268
- package/src/build/tasks/css.mjs +124 -124
- package/src/build/tasks/fonts.mjs +146 -146
- package/src/build/tasks/icons.mjs +224 -224
- package/src/build/tasks/images.mjs +244 -244
- package/src/build/tasks/precompress.mjs +78 -78
- package/src/client/cache-panel/i18n.js +670 -670
- package/src/client/cache-panel/login.html +74 -74
- package/src/client/cache-panel/panel.css +756 -756
- package/src/client/cache-panel/panel.html +308 -308
- package/src/client/cache-panel/panel.js +915 -915
- package/src/client/devtools/report.html +185 -185
- package/src/client/devtools/report.js +725 -725
- package/src/client/dom.js +95 -95
- package/src/client/form.js +192 -192
- package/src/client/index.js +35 -35
- package/src/client/registry.js +297 -297
- package/src/client/safe-image.js +91 -91
- package/src/client/store.js +36 -36
- package/src/client/swap.js +188 -188
- package/src/config/pattern.js +107 -107
- package/src/http/control-flow.js +71 -71
- package/src/http/cookies.js +257 -257
- package/src/http/request-cache.js +46 -46
- package/src/http/request-context.js +162 -162
- package/src/index.js +83 -83
- package/src/init.mjs +221 -221
- package/src/runtime/alias-hooks.mjs +119 -119
- package/src/runtime/register.mjs +4 -4
- package/src/server/assets.js +147 -147
- package/src/server/cache-deps.js +42 -42
- package/src/server/cache-panel.js +759 -759
- package/src/server/cloudflare.js +607 -595
- package/src/server/create-app.js +291 -291
- package/src/server/data-cache.js +462 -462
- package/src/server/dev/report.js +369 -369
- package/src/server/dev/socket.js +170 -170
- package/src/server/dev/version-check.mjs +139 -139
- package/src/server/html-cache.js +817 -817
- package/src/server/metadata.js +102 -102
- package/src/server/middleware/compression.js +205 -205
- package/src/server/middleware/csrf.js +134 -134
- package/src/server/middleware/dev-gate.js +62 -62
- package/src/server/middleware/headers.js +37 -37
- package/src/server/middleware/redirects.js +32 -32
- package/src/server/middleware/static-precompressed.js +100 -100
- package/src/server/middleware/upstream-proxy.js +141 -141
- package/src/server/prewarm.js +601 -601
- package/src/server/redis.js +569 -569
- package/src/server/router.js +128 -128
- package/src/server/status-page.js +164 -164
- package/src/server/upstream-limiter.js +376 -376
- package/src/server/upstream-tracking.js +166 -166
- package/src/start.mjs +7 -7
- package/src/templates/layout.ejs +44 -44
- package/src/version.mjs +31 -31
- package/src/views/components/loader.js +85 -85
- package/src/views/helpers/html.js +102 -102
- package/src/views/helpers/tags.js +245 -245
|
@@ -1,915 +1,915 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Önbellek panelinin istemci tarafı.
|
|
3
|
-
*
|
|
4
|
-
* Build hattından geçmez, doğrudan diskten servis edilir: panelin açık olduğu
|
|
5
|
-
* bir kurulumda `jskelet build` hiç koşmamış olabilir ve panel yine çalışmalı.
|
|
6
|
-
* Bu yüzden burada import edilen bir framework modülü yok.
|
|
7
|
-
*
|
|
8
|
-
* Yenileme WebSocket değil, kısa aralıklı `fetch` ile: panel nadiren ve kısa
|
|
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.
|
|
13
|
-
*/
|
|
14
|
-
import { applyTranslations, languageSelect, t } from "./i18n.js";
|
|
15
|
-
|
|
16
|
-
const REFRESH_MS = 3000;
|
|
17
|
-
|
|
18
|
-
const $ = (/** @type {string} */ id) => document.getElementById(id);
|
|
19
|
-
|
|
20
|
-
/** @type {"html" | "data"} */
|
|
21
|
-
let tab = "html";
|
|
22
|
-
let query = "";
|
|
23
|
-
/** @type {any} */
|
|
24
|
-
let latest = null;
|
|
25
|
-
/** @type {number | null} */
|
|
26
|
-
let timer = null;
|
|
27
|
-
let busy = false;
|
|
28
|
-
|
|
29
|
-
/* ------------------------------------------------------------ biçimlendirme */
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* @param {number} bytes
|
|
33
|
-
* @returns {string}
|
|
34
|
-
*/
|
|
35
|
-
function formatBytes(bytes) {
|
|
36
|
-
if (!bytes) return "0 B";
|
|
37
|
-
const units = ["B", "KB", "MB", "GB"];
|
|
38
|
-
const power = Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(1024)));
|
|
39
|
-
const value = bytes / 1024 ** power;
|
|
40
|
-
return `${value >= 100 || power === 0 ? Math.round(value) : value.toFixed(1)} ${units[power]}`;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* @param {number} seconds
|
|
45
|
-
* @returns {string}
|
|
46
|
-
*/
|
|
47
|
-
function formatDuration(seconds) {
|
|
48
|
-
const total = Math.max(0, Math.round(seconds));
|
|
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}`;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* @param {string} message
|
|
61
|
-
* @param {boolean} [ok]
|
|
62
|
-
*/
|
|
63
|
-
function toast(message, ok = true) {
|
|
64
|
-
const node = document.createElement("div");
|
|
65
|
-
node.className = ok ? "toast" : "toast bad";
|
|
66
|
-
node.textContent = message;
|
|
67
|
-
$("toast").append(node);
|
|
68
|
-
|
|
69
|
-
setTimeout(() => node.remove(), 5000);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/* -------------------------------------------------------------------- veri */
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Sunucu yetkisiz isteklere 404 dönüyor (panelin varlığını doğrulamamak
|
|
76
|
-
* için). Oturum düştüyse sayfa yeniden yüklenir ve giriş formuna dönülür;
|
|
77
|
-
* arka planda dönmeye devam eden bir yoklama, IP'yi kendi kendine
|
|
78
|
-
* yasaklatırdı.
|
|
79
|
-
*
|
|
80
|
-
* @param {Response} response
|
|
81
|
-
* @returns {boolean} İstek işlenebilir mi.
|
|
82
|
-
*/
|
|
83
|
-
function alive(response) {
|
|
84
|
-
if (response.status !== 404) return true;
|
|
85
|
-
|
|
86
|
-
stopPolling();
|
|
87
|
-
location.reload();
|
|
88
|
-
return false;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async function load() {
|
|
92
|
-
if (busy) return;
|
|
93
|
-
busy = true;
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
const response = await fetch(`data?q=${encodeURIComponent(query)}`, {
|
|
97
|
-
headers: { Accept: "application/json" },
|
|
98
|
-
});
|
|
99
|
-
if (!alive(response)) return;
|
|
100
|
-
if (!response.ok) return;
|
|
101
|
-
|
|
102
|
-
latest = await response.json();
|
|
103
|
-
render();
|
|
104
|
-
} catch {
|
|
105
|
-
// Sunucu yeniden başlıyor olabilir; bir sonraki tur dener.
|
|
106
|
-
} finally {
|
|
107
|
-
busy = false;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* @param {Record<string, unknown>} body
|
|
113
|
-
* @returns {Promise<void>}
|
|
114
|
-
*/
|
|
115
|
-
async function act(body) {
|
|
116
|
-
const response = await fetch("action", {
|
|
117
|
-
method: "POST",
|
|
118
|
-
headers: {
|
|
119
|
-
"Content-Type": "application/json",
|
|
120
|
-
// Panelin CSRF freni: çapraz siteden gönderilemeyen bir başlık.
|
|
121
|
-
"X-JSkelet-Cache-Panel": "1",
|
|
122
|
-
},
|
|
123
|
-
body: JSON.stringify(body),
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
if (!alive(response)) return;
|
|
127
|
-
|
|
128
|
-
const result = await response.json().catch(() => null);
|
|
129
|
-
toast(describe(result), result?.ok !== false);
|
|
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");
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/* ------------------------------------------------------------------- çizim */
|
|
162
|
-
|
|
163
|
-
function render() {
|
|
164
|
-
if (!latest) return;
|
|
165
|
-
|
|
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;
|
|
177
|
-
|
|
178
|
-
$("env").textContent = proc.env;
|
|
179
|
-
$("pid").textContent = `pid ${proc.pid}`;
|
|
180
|
-
$("uptime").textContent = t("chip.up", { value: formatDuration(proc.uptime) });
|
|
181
|
-
$("memory").textContent = `rss ${formatBytes(proc.memory.rss)}`;
|
|
182
|
-
|
|
183
|
-
$("html-size").textContent = html.size.toLocaleString();
|
|
184
|
-
$("html-sub").textContent = t("card.limit", { count: html.maxEntries.toLocaleString() });
|
|
185
|
-
$("html-bytes").textContent = formatBytes(html.bytes);
|
|
186
|
-
$("html-stale").textContent = t("card.stale", { count: html.stale });
|
|
187
|
-
|
|
188
|
-
$("data-size").textContent = data.size.toLocaleString();
|
|
189
|
-
$("data-sub").textContent = t("card.staleAndLimit", {
|
|
190
|
-
stale: data.stale,
|
|
191
|
-
limit: data.maxEntries.toLocaleString(),
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
const redisState = !redis.enabled
|
|
195
|
-
? "off"
|
|
196
|
-
: redis.bypassed
|
|
197
|
-
? "bypassed"
|
|
198
|
-
: redis.connected
|
|
199
|
-
? "connected"
|
|
200
|
-
: "disconnected";
|
|
201
|
-
|
|
202
|
-
$("redis-state").textContent = t(`state.${redisState}`);
|
|
203
|
-
$("redis-sub").textContent = redis.enabled
|
|
204
|
-
? t("redis.sub", { address: redis.address, errors: redis.errors })
|
|
205
|
-
: t("redis.disabled");
|
|
206
|
-
|
|
207
|
-
renderRedis(redis, redisState);
|
|
208
|
-
renderHost(host, proc, redis);
|
|
209
|
-
|
|
210
|
-
const done = prewarm.done ?? 0;
|
|
211
|
-
const total = prewarm.total ?? 0;
|
|
212
|
-
$("prewarm-state").textContent = prewarm.active ? `${done}/${total}` : t("state.idle");
|
|
213
|
-
$("prewarm-sub").textContent = prewarm.active
|
|
214
|
-
? t("prewarm.inProgress")
|
|
215
|
-
: total
|
|
216
|
-
? t("prewarm.lastRound", { done, total })
|
|
217
|
-
: t("prewarm.never");
|
|
218
|
-
|
|
219
|
-
renderTable();
|
|
220
|
-
}
|
|
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
|
-
|
|
594
|
-
function renderTable() {
|
|
595
|
-
const source = tab === "html" ? latest.html : latest.data;
|
|
596
|
-
const thead = $("thead");
|
|
597
|
-
const tbody = $("tbody");
|
|
598
|
-
|
|
599
|
-
$("listed").textContent =
|
|
600
|
-
source.matched > source.entries.length
|
|
601
|
-
? t("entries.shownPartial", { shown: source.entries.length, total: source.matched })
|
|
602
|
-
: t("entries.shown", { total: source.matched });
|
|
603
|
-
|
|
604
|
-
const columns =
|
|
605
|
-
tab === "html"
|
|
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);
|
|
626
|
-
|
|
627
|
-
tbody.replaceChildren(
|
|
628
|
-
...source.entries.map((/** @type {any} */ entry) => row(entry)),
|
|
629
|
-
);
|
|
630
|
-
|
|
631
|
-
$("empty").hidden = source.entries.length > 0;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
/**
|
|
635
|
-
* @param {any} entry
|
|
636
|
-
* @returns {HTMLTableRowElement}
|
|
637
|
-
*/
|
|
638
|
-
function row(entry) {
|
|
639
|
-
const tr = document.createElement("tr");
|
|
640
|
-
|
|
641
|
-
const key = document.createElement("td");
|
|
642
|
-
key.className = "key mono";
|
|
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
|
-
|
|
666
|
-
tr.append(key);
|
|
667
|
-
|
|
668
|
-
const state = document.createElement("td");
|
|
669
|
-
const tag = document.createElement("span");
|
|
670
|
-
tag.className = entry.stale ? "tag stale" : "tag fresh";
|
|
671
|
-
tag.textContent = entry.stale ? t("entries.stale") : t("entries.fresh");
|
|
672
|
-
state.append(tag);
|
|
673
|
-
tr.append(state);
|
|
674
|
-
|
|
675
|
-
if (tab === "html") {
|
|
676
|
-
tr.append(
|
|
677
|
-
cell(formatBytes(entry.bytes), "num"),
|
|
678
|
-
cell(String(entry.status), "num"),
|
|
679
|
-
cell(entry.stale ? "—" : formatDuration(entry.expiresIn), "num"),
|
|
680
|
-
cell(String(entry.deps), "num"),
|
|
681
|
-
cell(entry.encodings.length ? entry.encodings.join(", ") : "—"),
|
|
682
|
-
);
|
|
683
|
-
} else {
|
|
684
|
-
tr.append(cell(entry.stale ? "—" : formatDuration(entry.expiresIn), "num"));
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
const actions = document.createElement("td");
|
|
688
|
-
actions.className = "actions";
|
|
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
|
-
|
|
703
|
-
const drop = document.createElement("button");
|
|
704
|
-
drop.className = "tiny danger";
|
|
705
|
-
drop.textContent = t("entries.drop");
|
|
706
|
-
drop.addEventListener("click", () => {
|
|
707
|
-
void act({ type: tab === "html" ? "html:drop" : "data:drop", key: entry.key });
|
|
708
|
-
});
|
|
709
|
-
|
|
710
|
-
actions.append(drop);
|
|
711
|
-
tr.append(actions);
|
|
712
|
-
|
|
713
|
-
return tr;
|
|
714
|
-
}
|
|
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
|
-
|
|
733
|
-
/**
|
|
734
|
-
* @param {string} text
|
|
735
|
-
* @param {string} [className]
|
|
736
|
-
* @returns {HTMLTableCellElement}
|
|
737
|
-
*/
|
|
738
|
-
function cell(text, className) {
|
|
739
|
-
const td = document.createElement("td");
|
|
740
|
-
if (className) td.className = className;
|
|
741
|
-
td.textContent = text;
|
|
742
|
-
return td;
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
/* ------------------------------------------------------------------ olaylar */
|
|
746
|
-
|
|
747
|
-
function startPolling() {
|
|
748
|
-
stopPolling();
|
|
749
|
-
timer = setInterval(load, REFRESH_MS);
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
function stopPolling() {
|
|
753
|
-
if (timer !== null) clearInterval(timer);
|
|
754
|
-
timer = null;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
$("auto").addEventListener("change", (event) => {
|
|
758
|
-
if (/** @type {HTMLInputElement} */ (event.target).checked) startPolling();
|
|
759
|
-
else stopPolling();
|
|
760
|
-
});
|
|
761
|
-
|
|
762
|
-
$("refresh").addEventListener("click", () => void load());
|
|
763
|
-
|
|
764
|
-
$("logout").addEventListener("click", async () => {
|
|
765
|
-
stopPolling();
|
|
766
|
-
await fetch("logout", { method: "POST" });
|
|
767
|
-
location.reload();
|
|
768
|
-
});
|
|
769
|
-
|
|
770
|
-
// Arama her tuşta sunucuya gitmez: on binlerce anahtarlı bir önbellekte
|
|
771
|
-
// filtreleme sunucuda yapılıyor.
|
|
772
|
-
let searchTimer = 0;
|
|
773
|
-
$("search").addEventListener("input", (event) => {
|
|
774
|
-
query = /** @type {HTMLInputElement} */ (event.target).value;
|
|
775
|
-
clearTimeout(searchTimer);
|
|
776
|
-
searchTimer = setTimeout(load, 200);
|
|
777
|
-
});
|
|
778
|
-
|
|
779
|
-
for (const name of /** @type {const} */ (["html", "data"])) {
|
|
780
|
-
$(`tab-${name}`).addEventListener("click", () => {
|
|
781
|
-
tab = name;
|
|
782
|
-
$("tab-html").setAttribute("aria-selected", String(name === "html"));
|
|
783
|
-
$("tab-data").setAttribute("aria-selected", String(name === "data"));
|
|
784
|
-
renderTable();
|
|
785
|
-
});
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
$("invalidate").addEventListener("click", () => {
|
|
789
|
-
const target = $("target").value.trim();
|
|
790
|
-
if (!target) {
|
|
791
|
-
toast(t("toast.needTarget"), false);
|
|
792
|
-
return;
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
void act({ type: "html:invalidate", target, hard: $("hard").checked });
|
|
796
|
-
});
|
|
797
|
-
|
|
798
|
-
$("clear-html").addEventListener("click", () => {
|
|
799
|
-
if (!confirm(t("confirm.clearHtml"))) return;
|
|
800
|
-
void act({ type: "html:clear" });
|
|
801
|
-
});
|
|
802
|
-
|
|
803
|
-
$("clear-data").addEventListener("click", () => {
|
|
804
|
-
const prefix = $("prefix").value.trim();
|
|
805
|
-
if (!prefix && !confirm(t("confirm.clearData"))) {
|
|
806
|
-
return;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
void act({ type: "data:clear", prefix });
|
|
810
|
-
});
|
|
811
|
-
|
|
812
|
-
$("prewarm").addEventListener("click", () => void act({ type: "prewarm" }));
|
|
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
|
-
|
|
818
|
-
$("redis-html").addEventListener("click", () =>
|
|
819
|
-
void act({ type: "redis:drop", kind: "html" }),
|
|
820
|
-
);
|
|
821
|
-
|
|
822
|
-
$("redis-data").addEventListener("click", () =>
|
|
823
|
-
void act({ type: "redis:drop", kind: "data" }),
|
|
824
|
-
);
|
|
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
|
-
|
|
888
|
-
// Sekme arkaya alındığında yoklama durur: panel açık kalmış bir sekmede
|
|
889
|
-
// dakikalarca boşa istek atmasın.
|
|
890
|
-
document.addEventListener("visibilitychange", () => {
|
|
891
|
-
if (document.hidden) stopPolling();
|
|
892
|
-
else if ($("auto").checked) {
|
|
893
|
-
void load();
|
|
894
|
-
startPolling();
|
|
895
|
-
}
|
|
896
|
-
});
|
|
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
|
-
|
|
913
|
-
void load();
|
|
914
|
-
void loadCloudflare();
|
|
915
|
-
startPolling();
|
|
1
|
+
/**
|
|
2
|
+
* Önbellek panelinin istemci tarafı.
|
|
3
|
+
*
|
|
4
|
+
* Build hattından geçmez, doğrudan diskten servis edilir: panelin açık olduğu
|
|
5
|
+
* bir kurulumda `jskelet build` hiç koşmamış olabilir ve panel yine çalışmalı.
|
|
6
|
+
* Bu yüzden burada import edilen bir framework modülü yok.
|
|
7
|
+
*
|
|
8
|
+
* Yenileme WebSocket değil, kısa aralıklı `fetch` ile: panel nadiren ve kısa
|
|
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.
|
|
13
|
+
*/
|
|
14
|
+
import { applyTranslations, languageSelect, t } from "./i18n.js";
|
|
15
|
+
|
|
16
|
+
const REFRESH_MS = 3000;
|
|
17
|
+
|
|
18
|
+
const $ = (/** @type {string} */ id) => document.getElementById(id);
|
|
19
|
+
|
|
20
|
+
/** @type {"html" | "data"} */
|
|
21
|
+
let tab = "html";
|
|
22
|
+
let query = "";
|
|
23
|
+
/** @type {any} */
|
|
24
|
+
let latest = null;
|
|
25
|
+
/** @type {number | null} */
|
|
26
|
+
let timer = null;
|
|
27
|
+
let busy = false;
|
|
28
|
+
|
|
29
|
+
/* ------------------------------------------------------------ biçimlendirme */
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {number} bytes
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
function formatBytes(bytes) {
|
|
36
|
+
if (!bytes) return "0 B";
|
|
37
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
38
|
+
const power = Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(1024)));
|
|
39
|
+
const value = bytes / 1024 ** power;
|
|
40
|
+
return `${value >= 100 || power === 0 ? Math.round(value) : value.toFixed(1)} ${units[power]}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {number} seconds
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
function formatDuration(seconds) {
|
|
48
|
+
const total = Math.max(0, Math.round(seconds));
|
|
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}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {string} message
|
|
61
|
+
* @param {boolean} [ok]
|
|
62
|
+
*/
|
|
63
|
+
function toast(message, ok = true) {
|
|
64
|
+
const node = document.createElement("div");
|
|
65
|
+
node.className = ok ? "toast" : "toast bad";
|
|
66
|
+
node.textContent = message;
|
|
67
|
+
$("toast").append(node);
|
|
68
|
+
|
|
69
|
+
setTimeout(() => node.remove(), 5000);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* -------------------------------------------------------------------- veri */
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Sunucu yetkisiz isteklere 404 dönüyor (panelin varlığını doğrulamamak
|
|
76
|
+
* için). Oturum düştüyse sayfa yeniden yüklenir ve giriş formuna dönülür;
|
|
77
|
+
* arka planda dönmeye devam eden bir yoklama, IP'yi kendi kendine
|
|
78
|
+
* yasaklatırdı.
|
|
79
|
+
*
|
|
80
|
+
* @param {Response} response
|
|
81
|
+
* @returns {boolean} İstek işlenebilir mi.
|
|
82
|
+
*/
|
|
83
|
+
function alive(response) {
|
|
84
|
+
if (response.status !== 404) return true;
|
|
85
|
+
|
|
86
|
+
stopPolling();
|
|
87
|
+
location.reload();
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function load() {
|
|
92
|
+
if (busy) return;
|
|
93
|
+
busy = true;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const response = await fetch(`data?q=${encodeURIComponent(query)}`, {
|
|
97
|
+
headers: { Accept: "application/json" },
|
|
98
|
+
});
|
|
99
|
+
if (!alive(response)) return;
|
|
100
|
+
if (!response.ok) return;
|
|
101
|
+
|
|
102
|
+
latest = await response.json();
|
|
103
|
+
render();
|
|
104
|
+
} catch {
|
|
105
|
+
// Sunucu yeniden başlıyor olabilir; bir sonraki tur dener.
|
|
106
|
+
} finally {
|
|
107
|
+
busy = false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @param {Record<string, unknown>} body
|
|
113
|
+
* @returns {Promise<void>}
|
|
114
|
+
*/
|
|
115
|
+
async function act(body) {
|
|
116
|
+
const response = await fetch("action", {
|
|
117
|
+
method: "POST",
|
|
118
|
+
headers: {
|
|
119
|
+
"Content-Type": "application/json",
|
|
120
|
+
// Panelin CSRF freni: çapraz siteden gönderilemeyen bir başlık.
|
|
121
|
+
"X-JSkelet-Cache-Panel": "1",
|
|
122
|
+
},
|
|
123
|
+
body: JSON.stringify(body),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (!alive(response)) return;
|
|
127
|
+
|
|
128
|
+
const result = await response.json().catch(() => null);
|
|
129
|
+
toast(describe(result), result?.ok !== false);
|
|
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");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* ------------------------------------------------------------------- çizim */
|
|
162
|
+
|
|
163
|
+
function render() {
|
|
164
|
+
if (!latest) return;
|
|
165
|
+
|
|
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;
|
|
177
|
+
|
|
178
|
+
$("env").textContent = proc.env;
|
|
179
|
+
$("pid").textContent = `pid ${proc.pid}`;
|
|
180
|
+
$("uptime").textContent = t("chip.up", { value: formatDuration(proc.uptime) });
|
|
181
|
+
$("memory").textContent = `rss ${formatBytes(proc.memory.rss)}`;
|
|
182
|
+
|
|
183
|
+
$("html-size").textContent = html.size.toLocaleString();
|
|
184
|
+
$("html-sub").textContent = t("card.limit", { count: html.maxEntries.toLocaleString() });
|
|
185
|
+
$("html-bytes").textContent = formatBytes(html.bytes);
|
|
186
|
+
$("html-stale").textContent = t("card.stale", { count: html.stale });
|
|
187
|
+
|
|
188
|
+
$("data-size").textContent = data.size.toLocaleString();
|
|
189
|
+
$("data-sub").textContent = t("card.staleAndLimit", {
|
|
190
|
+
stale: data.stale,
|
|
191
|
+
limit: data.maxEntries.toLocaleString(),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const redisState = !redis.enabled
|
|
195
|
+
? "off"
|
|
196
|
+
: redis.bypassed
|
|
197
|
+
? "bypassed"
|
|
198
|
+
: redis.connected
|
|
199
|
+
? "connected"
|
|
200
|
+
: "disconnected";
|
|
201
|
+
|
|
202
|
+
$("redis-state").textContent = t(`state.${redisState}`);
|
|
203
|
+
$("redis-sub").textContent = redis.enabled
|
|
204
|
+
? t("redis.sub", { address: redis.address, errors: redis.errors })
|
|
205
|
+
: t("redis.disabled");
|
|
206
|
+
|
|
207
|
+
renderRedis(redis, redisState);
|
|
208
|
+
renderHost(host, proc, redis);
|
|
209
|
+
|
|
210
|
+
const done = prewarm.done ?? 0;
|
|
211
|
+
const total = prewarm.total ?? 0;
|
|
212
|
+
$("prewarm-state").textContent = prewarm.active ? `${done}/${total}` : t("state.idle");
|
|
213
|
+
$("prewarm-sub").textContent = prewarm.active
|
|
214
|
+
? t("prewarm.inProgress")
|
|
215
|
+
: total
|
|
216
|
+
? t("prewarm.lastRound", { done, total })
|
|
217
|
+
: t("prewarm.never");
|
|
218
|
+
|
|
219
|
+
renderTable();
|
|
220
|
+
}
|
|
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
|
+
|
|
594
|
+
function renderTable() {
|
|
595
|
+
const source = tab === "html" ? latest.html : latest.data;
|
|
596
|
+
const thead = $("thead");
|
|
597
|
+
const tbody = $("tbody");
|
|
598
|
+
|
|
599
|
+
$("listed").textContent =
|
|
600
|
+
source.matched > source.entries.length
|
|
601
|
+
? t("entries.shownPartial", { shown: source.entries.length, total: source.matched })
|
|
602
|
+
: t("entries.shown", { total: source.matched });
|
|
603
|
+
|
|
604
|
+
const columns =
|
|
605
|
+
tab === "html"
|
|
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);
|
|
626
|
+
|
|
627
|
+
tbody.replaceChildren(
|
|
628
|
+
...source.entries.map((/** @type {any} */ entry) => row(entry)),
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
$("empty").hidden = source.entries.length > 0;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* @param {any} entry
|
|
636
|
+
* @returns {HTMLTableRowElement}
|
|
637
|
+
*/
|
|
638
|
+
function row(entry) {
|
|
639
|
+
const tr = document.createElement("tr");
|
|
640
|
+
|
|
641
|
+
const key = document.createElement("td");
|
|
642
|
+
key.className = "key mono";
|
|
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
|
+
|
|
666
|
+
tr.append(key);
|
|
667
|
+
|
|
668
|
+
const state = document.createElement("td");
|
|
669
|
+
const tag = document.createElement("span");
|
|
670
|
+
tag.className = entry.stale ? "tag stale" : "tag fresh";
|
|
671
|
+
tag.textContent = entry.stale ? t("entries.stale") : t("entries.fresh");
|
|
672
|
+
state.append(tag);
|
|
673
|
+
tr.append(state);
|
|
674
|
+
|
|
675
|
+
if (tab === "html") {
|
|
676
|
+
tr.append(
|
|
677
|
+
cell(formatBytes(entry.bytes), "num"),
|
|
678
|
+
cell(String(entry.status), "num"),
|
|
679
|
+
cell(entry.stale ? "—" : formatDuration(entry.expiresIn), "num"),
|
|
680
|
+
cell(String(entry.deps), "num"),
|
|
681
|
+
cell(entry.encodings.length ? entry.encodings.join(", ") : "—"),
|
|
682
|
+
);
|
|
683
|
+
} else {
|
|
684
|
+
tr.append(cell(entry.stale ? "—" : formatDuration(entry.expiresIn), "num"));
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const actions = document.createElement("td");
|
|
688
|
+
actions.className = "actions";
|
|
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
|
+
|
|
703
|
+
const drop = document.createElement("button");
|
|
704
|
+
drop.className = "tiny danger";
|
|
705
|
+
drop.textContent = t("entries.drop");
|
|
706
|
+
drop.addEventListener("click", () => {
|
|
707
|
+
void act({ type: tab === "html" ? "html:drop" : "data:drop", key: entry.key });
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
actions.append(drop);
|
|
711
|
+
tr.append(actions);
|
|
712
|
+
|
|
713
|
+
return tr;
|
|
714
|
+
}
|
|
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
|
+
|
|
733
|
+
/**
|
|
734
|
+
* @param {string} text
|
|
735
|
+
* @param {string} [className]
|
|
736
|
+
* @returns {HTMLTableCellElement}
|
|
737
|
+
*/
|
|
738
|
+
function cell(text, className) {
|
|
739
|
+
const td = document.createElement("td");
|
|
740
|
+
if (className) td.className = className;
|
|
741
|
+
td.textContent = text;
|
|
742
|
+
return td;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/* ------------------------------------------------------------------ olaylar */
|
|
746
|
+
|
|
747
|
+
function startPolling() {
|
|
748
|
+
stopPolling();
|
|
749
|
+
timer = setInterval(load, REFRESH_MS);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function stopPolling() {
|
|
753
|
+
if (timer !== null) clearInterval(timer);
|
|
754
|
+
timer = null;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
$("auto").addEventListener("change", (event) => {
|
|
758
|
+
if (/** @type {HTMLInputElement} */ (event.target).checked) startPolling();
|
|
759
|
+
else stopPolling();
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
$("refresh").addEventListener("click", () => void load());
|
|
763
|
+
|
|
764
|
+
$("logout").addEventListener("click", async () => {
|
|
765
|
+
stopPolling();
|
|
766
|
+
await fetch("logout", { method: "POST" });
|
|
767
|
+
location.reload();
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
// Arama her tuşta sunucuya gitmez: on binlerce anahtarlı bir önbellekte
|
|
771
|
+
// filtreleme sunucuda yapılıyor.
|
|
772
|
+
let searchTimer = 0;
|
|
773
|
+
$("search").addEventListener("input", (event) => {
|
|
774
|
+
query = /** @type {HTMLInputElement} */ (event.target).value;
|
|
775
|
+
clearTimeout(searchTimer);
|
|
776
|
+
searchTimer = setTimeout(load, 200);
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
for (const name of /** @type {const} */ (["html", "data"])) {
|
|
780
|
+
$(`tab-${name}`).addEventListener("click", () => {
|
|
781
|
+
tab = name;
|
|
782
|
+
$("tab-html").setAttribute("aria-selected", String(name === "html"));
|
|
783
|
+
$("tab-data").setAttribute("aria-selected", String(name === "data"));
|
|
784
|
+
renderTable();
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
$("invalidate").addEventListener("click", () => {
|
|
789
|
+
const target = $("target").value.trim();
|
|
790
|
+
if (!target) {
|
|
791
|
+
toast(t("toast.needTarget"), false);
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
void act({ type: "html:invalidate", target, hard: $("hard").checked });
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
$("clear-html").addEventListener("click", () => {
|
|
799
|
+
if (!confirm(t("confirm.clearHtml"))) return;
|
|
800
|
+
void act({ type: "html:clear" });
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
$("clear-data").addEventListener("click", () => {
|
|
804
|
+
const prefix = $("prefix").value.trim();
|
|
805
|
+
if (!prefix && !confirm(t("confirm.clearData"))) {
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
void act({ type: "data:clear", prefix });
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
$("prewarm").addEventListener("click", () => void act({ type: "prewarm" }));
|
|
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
|
+
|
|
818
|
+
$("redis-html").addEventListener("click", () =>
|
|
819
|
+
void act({ type: "redis:drop", kind: "html" }),
|
|
820
|
+
);
|
|
821
|
+
|
|
822
|
+
$("redis-data").addEventListener("click", () =>
|
|
823
|
+
void act({ type: "redis:drop", kind: "data" }),
|
|
824
|
+
);
|
|
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
|
+
|
|
888
|
+
// Sekme arkaya alındığında yoklama durur: panel açık kalmış bir sekmede
|
|
889
|
+
// dakikalarca boşa istek atmasın.
|
|
890
|
+
document.addEventListener("visibilitychange", () => {
|
|
891
|
+
if (document.hidden) stopPolling();
|
|
892
|
+
else if ($("auto").checked) {
|
|
893
|
+
void load();
|
|
894
|
+
startPolling();
|
|
895
|
+
}
|
|
896
|
+
});
|
|
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
|
+
|
|
913
|
+
void load();
|
|
914
|
+
void loadCloudflare();
|
|
915
|
+
startPolling();
|