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 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.0",
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",
@@ -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.ramShared": "This process: {rss} RSS.",
189
- "host.ramOnly": "This process holds the whole cache: {rss} RSS, {html} of it HTML.",
190
- "host.diskNote": "{free} free on {path} — build output and logs live here.",
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.ramShared": "Bu süreç: {rss} RSS.",
486
- "host.ramOnly": "Önbelleğin tamamı bu süreçte: {rss} RSS, {html} kadarı HTML.",
487
- "host.diskNote": "{path} üzerinde {free} boş — build çıktısı ve loglar burada.",
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",
@@ -330,9 +330,14 @@ footer {
330
330
  }
331
331
 
332
332
  main {
333
- max-width: 1180px;
333
+ width: 100%;
334
+ max-width: 1600px;
334
335
  margin: 0 auto;
335
- padding: 20px;
336
+ padding: 20px 24px 28px;
337
+ }
338
+
339
+ .page {
340
+ width: 100%;
336
341
  }
337
342
 
338
343
  .cards {
@@ -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
- * Makinenin RAM ve disk durumu. Paylaşımlı kademe kapalıyken önbelleğin
595
- * tamamı bu sürecin belleğinde yaşıyor; `maxEntries` ile RAM arasındaki
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
- const ramUsed = host.memory.used / host.memory.total;
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(host.memory.used)} / ${formatBytes(host.memory.total)}`;
614
- meter("ram-bar", ramUsed);
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", { rss: formatBytes(proc.memory.rss) })
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 diskUsed = (host.disk.total - host.disk.free) / host.disk.total;
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(host.disk.total - host.disk.free)} / ${formatBytes(host.disk.total)}`;
632
- meter("disk-bar", diskUsed);
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: await hostStatus(),
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
  */