jskelet 0.3.0 → 0.3.1
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 +5 -0
- package/package.json +1 -1
- package/src/client/admin/i18n.js +14 -6
- package/src/client/admin/panel.css +7 -2
- package/src/client/admin/panel.html +9 -0
- package/src/client/admin/panel.js +20 -11
- package/src/server/admin/snapshot.js +116 -1
package/CHANGELOG.md
CHANGED
|
@@ -207,6 +207,11 @@ false`.
|
|
|
207
207
|
|
|
208
208
|
### Changed
|
|
209
209
|
|
|
210
|
+
- Admin panel System meters (CPU, memory, disk) show this process's share of
|
|
211
|
+
the host — RSS and project disk footprint against machine totals, plus
|
|
212
|
+
process CPU across all cores — instead of whole-machine fullness. The panel
|
|
213
|
+
content width is wider (`1600px`) so Overview, Routes, Views and System use
|
|
214
|
+
the screen better.
|
|
210
215
|
- The release history page in `examples/marketing` now shows one release at a
|
|
211
216
|
time: the newest one is expanded and older releases collapse to a single
|
|
212
217
|
header row with their date, status and change count. Every release used to be
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jskelet",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "A framework that feels like no framework: Express 5 + EJS server rendering, vanilla JS islands, Tailwind v4 and an in-process HTML TTL cache.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/client/admin/i18n.js
CHANGED
|
@@ -182,12 +182,16 @@ export const MESSAGES = {
|
|
|
182
182
|
|
|
183
183
|
/* --- host --- */
|
|
184
184
|
"host.section": "Host",
|
|
185
|
+
"host.cpu": "CPU",
|
|
185
186
|
"host.memory": "Memory",
|
|
186
187
|
"host.disk": "Disk",
|
|
187
188
|
"host.noStats": "This platform does not report filesystem stats.",
|
|
188
|
-
"host.
|
|
189
|
-
"host.
|
|
190
|
-
"host.
|
|
189
|
+
"host.cpuNote": "This process across {cores} cores (since last refresh).",
|
|
190
|
+
"host.ramShared": "This process: {rss} RSS · {heap} heap.",
|
|
191
|
+
"host.ramOnly":
|
|
192
|
+
"This process holds the whole cache: {rss} RSS · {heap} heap, {html} of it HTML.",
|
|
193
|
+
"host.diskNote":
|
|
194
|
+
"Project files on {path} (excluding node_modules) · {free} free on the volume.",
|
|
191
195
|
|
|
192
196
|
/* --- işlemler --- */
|
|
193
197
|
"actions.section": "Actions",
|
|
@@ -479,12 +483,16 @@ export const MESSAGES = {
|
|
|
479
483
|
"cf.rowPurgeTitle": "Bu URL'i Cloudflare'de düşür",
|
|
480
484
|
|
|
481
485
|
"host.section": "Makine",
|
|
486
|
+
"host.cpu": "CPU",
|
|
482
487
|
"host.memory": "Bellek",
|
|
483
488
|
"host.disk": "Disk",
|
|
484
489
|
"host.noStats": "Bu platform dosya sistemi bilgisi vermiyor.",
|
|
485
|
-
"host.
|
|
486
|
-
"host.
|
|
487
|
-
"host.
|
|
490
|
+
"host.cpuNote": "Bu sürecin {cores} çekirdek üzerindeki payı (son yenilemeden beri).",
|
|
491
|
+
"host.ramShared": "Bu süreç: {rss} RSS · {heap} heap.",
|
|
492
|
+
"host.ramOnly":
|
|
493
|
+
"Önbelleğin tamamı bu süreçte: {rss} RSS · {heap} heap, {html} kadarı HTML.",
|
|
494
|
+
"host.diskNote":
|
|
495
|
+
"{path} üzerindeki proje dosyaları (node_modules hariç) · birimde {free} boş.",
|
|
488
496
|
|
|
489
497
|
"actions.section": "İşlemler",
|
|
490
498
|
"actions.target.placeholder": "/haber/:slug · /haber/bir-yazi · invalidation hedefi",
|
|
@@ -437,6 +437,15 @@ JSKELET_CLOUDFLARE_ZONE_ID=your_zone_id</code></pre>
|
|
|
437
437
|
<span class="count" id="host-heading">—</span>
|
|
438
438
|
</h2>
|
|
439
439
|
<div class="body">
|
|
440
|
+
<div class="meter">
|
|
441
|
+
<div class="meter-head">
|
|
442
|
+
<span data-i18n="host.cpu">CPU</span>
|
|
443
|
+
<span class="mono" id="cpu-text">—</span>
|
|
444
|
+
</div>
|
|
445
|
+
<div class="bar"><span id="cpu-bar"></span></div>
|
|
446
|
+
<p class="hint m0" id="cpu-note">—</p>
|
|
447
|
+
</div>
|
|
448
|
+
|
|
440
449
|
<div class="meter">
|
|
441
450
|
<div class="meter-head">
|
|
442
451
|
<span data-i18n="host.memory">Memory</span>
|
|
@@ -591,9 +591,8 @@ function renderRedis(redis, state) {
|
|
|
591
591
|
}
|
|
592
592
|
|
|
593
593
|
/**
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
* ilişkiyi görmeden ayar yapmak körlemesine oluyor.
|
|
594
|
+
* System sayfası: çubuklar bu sürecin makine kapasitesindeki payı
|
|
595
|
+
* (CPU / RSS / proje diski), host'un genel doluluğu değil.
|
|
597
596
|
*
|
|
598
597
|
* @param {any} host
|
|
599
598
|
* @param {any} proc
|
|
@@ -608,14 +607,24 @@ function renderHost(host, proc, redis) {
|
|
|
608
607
|
.filter(Boolean)
|
|
609
608
|
.join(" · ");
|
|
610
609
|
|
|
611
|
-
|
|
610
|
+
// Çubuklar makine doluluğunu değil, **bu sürecin** payını gösterir.
|
|
611
|
+
const cpuPercent = Number(proc.cpuPercent) || 0;
|
|
612
|
+
$("cpu-text").textContent = `${cpuPercent.toFixed(1)}%`;
|
|
613
|
+
meter("cpu-bar", cpuPercent / 100);
|
|
614
|
+
$("cpu-note").textContent = t("host.cpuNote", { cores: host.cpus });
|
|
615
|
+
|
|
616
|
+
const ramRatio = host.memory.total > 0 ? proc.memory.rss / host.memory.total : 0;
|
|
612
617
|
$("ram-text").textContent =
|
|
613
|
-
`${formatBytes(
|
|
614
|
-
meter("ram-bar",
|
|
618
|
+
`${formatBytes(proc.memory.rss)} / ${formatBytes(host.memory.total)}`;
|
|
619
|
+
meter("ram-bar", ramRatio);
|
|
615
620
|
$("ram-note").textContent = redis.connected
|
|
616
|
-
? t("host.ramShared", {
|
|
621
|
+
? t("host.ramShared", {
|
|
622
|
+
rss: formatBytes(proc.memory.rss),
|
|
623
|
+
heap: formatBytes(proc.memory.heapUsed),
|
|
624
|
+
})
|
|
617
625
|
: t("host.ramOnly", {
|
|
618
626
|
rss: formatBytes(proc.memory.rss),
|
|
627
|
+
heap: formatBytes(proc.memory.heapUsed),
|
|
619
628
|
html: formatBytes(latest.html.bytes),
|
|
620
629
|
});
|
|
621
630
|
|
|
@@ -626,11 +635,11 @@ function renderHost(host, proc, redis) {
|
|
|
626
635
|
return;
|
|
627
636
|
}
|
|
628
637
|
|
|
629
|
-
const
|
|
638
|
+
const appDisk = Number(proc.diskBytes) || 0;
|
|
639
|
+
const diskRatio = host.disk.total > 0 ? appDisk / host.disk.total : 0;
|
|
630
640
|
$("disk-text").textContent =
|
|
631
|
-
`${formatBytes(
|
|
632
|
-
meter("disk-bar",
|
|
633
|
-
// Önbellek diske yazılmıyor; disk build çıktısı ve log için önemli.
|
|
641
|
+
`${formatBytes(appDisk)} / ${formatBytes(host.disk.total)}`;
|
|
642
|
+
meter("disk-bar", diskRatio);
|
|
634
643
|
$("disk-note").textContent = t("host.diskNote", {
|
|
635
644
|
free: formatBytes(host.disk.free),
|
|
636
645
|
path: host.disk.path,
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
* Admin paneli cache / host / process dökümü.
|
|
3
3
|
*
|
|
4
4
|
* HTML gövdesi ve veri değerleri dönmez — yalnızca meta.
|
|
5
|
+
* System sayfasındaki çubuklar **bu sürecin** payını gösterir (makine
|
|
6
|
+
* toplamına oranla), host'un genel doluluğunu değil.
|
|
5
7
|
*/
|
|
6
8
|
import fs from "node:fs";
|
|
7
9
|
import os from "node:os";
|
|
10
|
+
import path from "node:path";
|
|
8
11
|
import process from "node:process";
|
|
9
12
|
import { getConfig } from "../../config/index.js";
|
|
10
13
|
import {
|
|
@@ -29,8 +32,25 @@ import { prewarmProgress } from "../prewarm.js";
|
|
|
29
32
|
/** Listelerin üst sınırı: veri önbelleğinde on binlerce anahtar olabiliyor. */
|
|
30
33
|
const MAX_LISTED = 500;
|
|
31
34
|
|
|
35
|
+
/** Disk ölçümünde atlanan dizinler — node_modules her poll'da taranmasın. */
|
|
36
|
+
const SKIP_DIRS = new Set([
|
|
37
|
+
"node_modules",
|
|
38
|
+
".git",
|
|
39
|
+
".hg",
|
|
40
|
+
".svn",
|
|
41
|
+
"coverage",
|
|
42
|
+
".turbo",
|
|
43
|
+
".next",
|
|
44
|
+
]);
|
|
45
|
+
|
|
32
46
|
export const BOOT_ID = `${Date.now()}-${process.pid}`;
|
|
33
47
|
|
|
48
|
+
/** @type {{ usage: NodeJS.CpuUsage; at: number } | null} */
|
|
49
|
+
let cpuSample = null;
|
|
50
|
+
|
|
51
|
+
/** @type {{ at: number, bytes: number, root: string } | null} */
|
|
52
|
+
let diskSample = null;
|
|
53
|
+
|
|
34
54
|
/**
|
|
35
55
|
* @param {string} query Anahtar filtresi (boş → filtre yok).
|
|
36
56
|
* @returns {Promise<object>}
|
|
@@ -46,6 +66,9 @@ export async function snapshot(query) {
|
|
|
46
66
|
const htmlMatched = html.filter(matches);
|
|
47
67
|
const dataMatched = data.filter(matches);
|
|
48
68
|
const usage = process.memoryUsage();
|
|
69
|
+
const host = await hostStatus();
|
|
70
|
+
const cpuPercent = sampleProcessCpu(host.cpus);
|
|
71
|
+
const appDiskBytes = await sampleAppDisk(getConfig().root);
|
|
49
72
|
|
|
50
73
|
return {
|
|
51
74
|
boot: BOOT_ID,
|
|
@@ -56,13 +79,17 @@ export async function snapshot(query) {
|
|
|
56
79
|
node: FRAMEWORK_NODE_RANGE,
|
|
57
80
|
homepage: FRAMEWORK_HOMEPAGE,
|
|
58
81
|
},
|
|
59
|
-
host
|
|
82
|
+
host,
|
|
60
83
|
process: {
|
|
61
84
|
pid: process.pid,
|
|
62
85
|
node: process.version,
|
|
63
86
|
uptime: process.uptime(),
|
|
64
87
|
env: process.env.NODE_ENV ?? "production",
|
|
65
88
|
memory: { rss: usage.rss, heapUsed: usage.heapUsed },
|
|
89
|
+
/** 0–100: bu sürecin tüm çekirdek kapasitesine oranı. */
|
|
90
|
+
cpuPercent,
|
|
91
|
+
/** Proje kökünün (node_modules hariç) disk boyutu. */
|
|
92
|
+
diskBytes: appDiskBytes,
|
|
66
93
|
},
|
|
67
94
|
html: {
|
|
68
95
|
size: getHtmlCacheSize(),
|
|
@@ -94,6 +121,94 @@ export async function snapshot(query) {
|
|
|
94
121
|
};
|
|
95
122
|
}
|
|
96
123
|
|
|
124
|
+
/**
|
|
125
|
+
* İki örnek arasındaki `process.cpuUsage` farkından süreç CPU yüzdesi.
|
|
126
|
+
* İlk çağrıda örnek alınır ve 0 döner — panelin bir sonraki turunda anlamlı.
|
|
127
|
+
*
|
|
128
|
+
* @param {number} cores
|
|
129
|
+
* @returns {number}
|
|
130
|
+
*/
|
|
131
|
+
function sampleProcessCpu(cores) {
|
|
132
|
+
const now = Date.now();
|
|
133
|
+
const current = process.cpuUsage();
|
|
134
|
+
|
|
135
|
+
if (!cpuSample) {
|
|
136
|
+
cpuSample = { usage: current, at: now };
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const elapsedMs = now - cpuSample.at;
|
|
141
|
+
const delta = process.cpuUsage(cpuSample.usage);
|
|
142
|
+
cpuSample = { usage: current, at: now };
|
|
143
|
+
|
|
144
|
+
if (elapsedMs <= 0 || cores <= 0) return 0;
|
|
145
|
+
|
|
146
|
+
// user+system mikro-saniye → ms; duvar süresi × çekirdek = kapasite.
|
|
147
|
+
const cpuMs = (delta.user + delta.system) / 1000;
|
|
148
|
+
const capacityMs = elapsedMs * cores;
|
|
149
|
+
return Math.max(0, Math.min(100, (cpuMs / capacityMs) * 100));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Proje kökünün boyutu (ağır dizinler atlanır). 60 sn önbellek.
|
|
154
|
+
*
|
|
155
|
+
* @param {string} root
|
|
156
|
+
* @returns {Promise<number>}
|
|
157
|
+
*/
|
|
158
|
+
async function sampleAppDisk(root) {
|
|
159
|
+
if (diskSample && diskSample.root === root && Date.now() - diskSample.at < 60_000) {
|
|
160
|
+
return diskSample.bytes;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const bytes = await measureDir(root);
|
|
164
|
+
diskSample = { at: Date.now(), bytes, root };
|
|
165
|
+
return bytes;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @param {string} dir
|
|
170
|
+
* @returns {Promise<number>}
|
|
171
|
+
*/
|
|
172
|
+
async function measureDir(dir) {
|
|
173
|
+
let total = 0;
|
|
174
|
+
|
|
175
|
+
/** @type {string[]} */
|
|
176
|
+
const stack = [dir];
|
|
177
|
+
|
|
178
|
+
while (stack.length) {
|
|
179
|
+
const current = stack.pop();
|
|
180
|
+
if (!current) continue;
|
|
181
|
+
|
|
182
|
+
let entries;
|
|
183
|
+
try {
|
|
184
|
+
entries = await fs.promises.readdir(current, { withFileTypes: true });
|
|
185
|
+
} catch {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
for (const entry of entries) {
|
|
190
|
+
if (entry.name === "." || entry.name === "..") continue;
|
|
191
|
+
if (entry.isDirectory() && SKIP_DIRS.has(entry.name)) continue;
|
|
192
|
+
|
|
193
|
+
const full = path.join(current, entry.name);
|
|
194
|
+
if (entry.isDirectory()) {
|
|
195
|
+
stack.push(full);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (!entry.isFile()) continue;
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const stats = await fs.promises.stat(full);
|
|
202
|
+
total += stats.size;
|
|
203
|
+
} catch {
|
|
204
|
+
// silinmiş / erişilemeyen dosya
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return total;
|
|
210
|
+
}
|
|
211
|
+
|
|
97
212
|
/**
|
|
98
213
|
* @returns {Promise<object>}
|
|
99
214
|
*/
|