clay-server 2.5.0

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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +281 -0
  3. package/bin/cli.js +2385 -0
  4. package/lib/cli-sessions.js +270 -0
  5. package/lib/config.js +237 -0
  6. package/lib/daemon.js +489 -0
  7. package/lib/ipc.js +112 -0
  8. package/lib/notes.js +120 -0
  9. package/lib/pages.js +664 -0
  10. package/lib/project.js +1433 -0
  11. package/lib/public/app.js +2795 -0
  12. package/lib/public/apple-touch-icon-dark.png +0 -0
  13. package/lib/public/apple-touch-icon.png +0 -0
  14. package/lib/public/css/base.css +264 -0
  15. package/lib/public/css/diff.css +128 -0
  16. package/lib/public/css/filebrowser.css +1114 -0
  17. package/lib/public/css/highlight.css +144 -0
  18. package/lib/public/css/icon-strip.css +296 -0
  19. package/lib/public/css/input.css +573 -0
  20. package/lib/public/css/menus.css +856 -0
  21. package/lib/public/css/messages.css +1445 -0
  22. package/lib/public/css/mobile-nav.css +354 -0
  23. package/lib/public/css/overlays.css +697 -0
  24. package/lib/public/css/rewind.css +505 -0
  25. package/lib/public/css/server-settings.css +761 -0
  26. package/lib/public/css/sidebar.css +936 -0
  27. package/lib/public/css/sticky-notes.css +358 -0
  28. package/lib/public/css/title-bar.css +314 -0
  29. package/lib/public/favicon-dark.svg +1 -0
  30. package/lib/public/favicon.svg +1 -0
  31. package/lib/public/icon-192-dark.png +0 -0
  32. package/lib/public/icon-192.png +0 -0
  33. package/lib/public/icon-512-dark.png +0 -0
  34. package/lib/public/icon-512.png +0 -0
  35. package/lib/public/icon-mono.svg +1 -0
  36. package/lib/public/index.html +762 -0
  37. package/lib/public/manifest.json +27 -0
  38. package/lib/public/modules/diff.js +398 -0
  39. package/lib/public/modules/events.js +21 -0
  40. package/lib/public/modules/filebrowser.js +1411 -0
  41. package/lib/public/modules/fileicons.js +172 -0
  42. package/lib/public/modules/icons.js +54 -0
  43. package/lib/public/modules/input.js +584 -0
  44. package/lib/public/modules/markdown.js +356 -0
  45. package/lib/public/modules/notifications.js +649 -0
  46. package/lib/public/modules/qrcode.js +70 -0
  47. package/lib/public/modules/rewind.js +345 -0
  48. package/lib/public/modules/server-settings.js +510 -0
  49. package/lib/public/modules/sidebar.js +1083 -0
  50. package/lib/public/modules/state.js +3 -0
  51. package/lib/public/modules/sticky-notes.js +688 -0
  52. package/lib/public/modules/terminal.js +697 -0
  53. package/lib/public/modules/theme.js +738 -0
  54. package/lib/public/modules/tools.js +1608 -0
  55. package/lib/public/modules/utils.js +56 -0
  56. package/lib/public/style.css +15 -0
  57. package/lib/public/sw.js +75 -0
  58. package/lib/push.js +124 -0
  59. package/lib/sdk-bridge.js +989 -0
  60. package/lib/server.js +582 -0
  61. package/lib/sessions.js +424 -0
  62. package/lib/terminal-manager.js +187 -0
  63. package/lib/terminal.js +24 -0
  64. package/lib/themes/ayu-light.json +9 -0
  65. package/lib/themes/catppuccin-latte.json +9 -0
  66. package/lib/themes/catppuccin-mocha.json +9 -0
  67. package/lib/themes/clay-light.json +10 -0
  68. package/lib/themes/clay.json +10 -0
  69. package/lib/themes/dracula.json +9 -0
  70. package/lib/themes/everforest-light.json +9 -0
  71. package/lib/themes/everforest.json +9 -0
  72. package/lib/themes/github-light.json +9 -0
  73. package/lib/themes/gruvbox-dark.json +9 -0
  74. package/lib/themes/gruvbox-light.json +9 -0
  75. package/lib/themes/monokai.json +9 -0
  76. package/lib/themes/nord-light.json +9 -0
  77. package/lib/themes/nord.json +9 -0
  78. package/lib/themes/one-dark.json +9 -0
  79. package/lib/themes/one-light.json +9 -0
  80. package/lib/themes/rose-pine-dawn.json +9 -0
  81. package/lib/themes/rose-pine.json +9 -0
  82. package/lib/themes/solarized-dark.json +9 -0
  83. package/lib/themes/solarized-light.json +9 -0
  84. package/lib/themes/tokyo-night-light.json +9 -0
  85. package/lib/themes/tokyo-night.json +9 -0
  86. package/lib/updater.js +97 -0
  87. package/package.json +47 -0
@@ -0,0 +1,510 @@
1
+ // server-settings.js — Full-screen server settings overlay
2
+ import { refreshIcons } from './icons.js';
3
+ import { getCurrentTheme, openSettingsThemePicker } from './theme.js';
4
+ import { showToast } from './utils.js';
5
+
6
+ var ctx = null;
7
+ var settingsEl = null;
8
+ var settingsBtn = null;
9
+ var closeBtn = null;
10
+ var navItems = null;
11
+ var sections = null;
12
+ var statsTimer = null;
13
+
14
+ export function initServerSettings(appCtx) {
15
+ ctx = appCtx;
16
+ settingsEl = document.getElementById("server-settings");
17
+ settingsBtn = document.getElementById("server-settings-btn");
18
+ closeBtn = document.getElementById("server-settings-close");
19
+
20
+ if (!settingsEl || !settingsBtn) return;
21
+
22
+ navItems = settingsEl.querySelectorAll(".settings-nav-item");
23
+ sections = settingsEl.querySelectorAll(".server-settings-section");
24
+
25
+ // Open settings
26
+ settingsBtn.addEventListener("click", function () {
27
+ openSettings();
28
+ });
29
+
30
+ // Close settings
31
+ closeBtn.addEventListener("click", function () {
32
+ closeSettings();
33
+ });
34
+
35
+ // ESC to close
36
+ document.addEventListener("keydown", function (e) {
37
+ if (e.key === "Escape" && !settingsEl.classList.contains("hidden")) {
38
+ closeSettings();
39
+ }
40
+ });
41
+
42
+ // Nav item clicks
43
+ for (var i = 0; i < navItems.length; i++) {
44
+ navItems[i].addEventListener("click", function () {
45
+ var section = this.dataset.section;
46
+ switchSection(section);
47
+ });
48
+ }
49
+
50
+ // Context view buttons
51
+ var contextViewEl = document.getElementById("settings-context-view");
52
+ if (contextViewEl) {
53
+ var btns = contextViewEl.querySelectorAll(".settings-btn-option");
54
+ for (var b = 0; b < btns.length; b++) {
55
+ btns[b].addEventListener("click", function () {
56
+ var view = this.dataset.view;
57
+ if (ctx.setContextView) ctx.setContextView(view);
58
+ if (ctx.applyContextView) ctx.applyContextView(view);
59
+ updateContextViewButtons();
60
+ });
61
+ }
62
+ }
63
+
64
+ // Notification toggles
65
+ var notifAlert = document.getElementById("settings-notif-alert");
66
+ var notifSound = document.getElementById("settings-notif-sound");
67
+ var notifPush = document.getElementById("settings-notif-push");
68
+
69
+ if (notifAlert) {
70
+ notifAlert.addEventListener("change", function () {
71
+ var src = document.getElementById("notif-toggle-alert");
72
+ if (src) {
73
+ src.checked = this.checked;
74
+ src.dispatchEvent(new Event("change", { bubbles: true }));
75
+ }
76
+ });
77
+ }
78
+
79
+ if (notifSound) {
80
+ notifSound.addEventListener("change", function () {
81
+ var src = document.getElementById("notif-toggle-sound");
82
+ if (src) {
83
+ src.checked = this.checked;
84
+ src.dispatchEvent(new Event("change", { bubbles: true }));
85
+ }
86
+ });
87
+ }
88
+
89
+ if (notifPush) {
90
+ notifPush.addEventListener("change", function () {
91
+ var src = document.getElementById("notif-toggle-push");
92
+ if (src) {
93
+ src.checked = this.checked;
94
+ src.dispatchEvent(new Event("change", { bubbles: true }));
95
+ }
96
+ });
97
+ }
98
+
99
+ // Model item click
100
+ settingsEl.addEventListener("click", function (e) {
101
+ var modelItem = e.target.closest(".settings-model-item");
102
+ if (!modelItem) return;
103
+ var model = modelItem.dataset.model;
104
+ if (!model) return;
105
+ var ws = ctx.ws;
106
+ if (ws && ws.readyState === 1) {
107
+ ws.send(JSON.stringify({ type: "set_model", model: model }));
108
+ }
109
+ });
110
+
111
+ // PIN buttons
112
+ var pinSetBtn = document.getElementById("settings-pin-set-btn");
113
+ var pinRemoveBtn = document.getElementById("settings-pin-remove-btn");
114
+ var pinSaveBtn = document.getElementById("settings-pin-save-btn");
115
+ var pinCancelBtn = document.getElementById("settings-pin-cancel-btn");
116
+ var pinInput = document.getElementById("settings-pin-input");
117
+
118
+ if (pinSetBtn) pinSetBtn.addEventListener("click", function () { showPinForm(); });
119
+ if (pinRemoveBtn) pinRemoveBtn.addEventListener("click", function () {
120
+ var ws = ctx.ws;
121
+ if (ws && ws.readyState === 1) {
122
+ ws.send(JSON.stringify({ type: "set_pin", pin: null }));
123
+ }
124
+ });
125
+ if (pinSaveBtn) pinSaveBtn.addEventListener("click", function () { submitPin(); });
126
+ if (pinCancelBtn) pinCancelBtn.addEventListener("click", function () { hidePinForm(); });
127
+ if (pinInput) pinInput.addEventListener("keydown", function (e) {
128
+ if (e.key === "Enter") { e.preventDefault(); submitPin(); }
129
+ if (e.key === "Escape") { e.preventDefault(); hidePinForm(); }
130
+ });
131
+
132
+ // Keep awake toggle
133
+ var keepAwakeToggle = document.getElementById("settings-keep-awake");
134
+ if (keepAwakeToggle) {
135
+ keepAwakeToggle.addEventListener("change", function () {
136
+ var ws = ctx.ws;
137
+ if (ws && ws.readyState === 1) {
138
+ ws.send(JSON.stringify({ type: "set_keep_awake", value: this.checked }));
139
+ }
140
+ });
141
+ }
142
+
143
+ // Shutdown server
144
+ var shutdownInput = document.getElementById("settings-shutdown-input");
145
+ var shutdownBtn = document.getElementById("settings-shutdown-btn");
146
+
147
+ if (shutdownInput && shutdownBtn) {
148
+ shutdownInput.addEventListener("input", function () {
149
+ var val = this.value.trim().toLowerCase();
150
+ shutdownBtn.disabled = val !== "shutdown";
151
+ });
152
+
153
+ shutdownInput.addEventListener("keydown", function (e) {
154
+ if (e.key === "Enter") {
155
+ e.preventDefault();
156
+ if (!shutdownBtn.disabled) shutdownBtn.click();
157
+ }
158
+ });
159
+
160
+ shutdownBtn.addEventListener("click", function () {
161
+ var val = shutdownInput.value.trim().toLowerCase();
162
+ if (val !== "shutdown") return;
163
+ var ws = ctx.ws;
164
+ if (ws && ws.readyState === 1) {
165
+ shutdownBtn.disabled = true;
166
+ shutdownBtn.textContent = "Shutting down...";
167
+ shutdownInput.disabled = true;
168
+ ws.send(JSON.stringify({ type: "shutdown_server" }));
169
+ }
170
+ });
171
+ }
172
+ }
173
+
174
+ function switchSection(sectionName) {
175
+ for (var i = 0; i < navItems.length; i++) {
176
+ var isActive = navItems[i].dataset.section === sectionName;
177
+ navItems[i].classList.toggle("active", isActive);
178
+ // On mobile, scroll the active tab into view
179
+ if (isActive) {
180
+ navItems[i].scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" });
181
+ }
182
+ }
183
+ for (var j = 0; j < sections.length; j++) {
184
+ var isActive2 = sections[j].dataset.section === sectionName;
185
+ sections[j].classList.toggle("active", isActive2);
186
+ }
187
+ }
188
+
189
+ function openSettings() {
190
+ settingsEl.classList.remove("hidden");
191
+ settingsBtn.classList.add("active");
192
+ refreshIcons(settingsEl);
193
+ populateSettings();
194
+ requestDaemonConfig();
195
+ resetShutdownForm();
196
+
197
+ // Start periodic stats refresh
198
+ requestStats();
199
+ statsTimer = setInterval(requestStats, 5000);
200
+ }
201
+
202
+ function resetShutdownForm() {
203
+ var input = document.getElementById("settings-shutdown-input");
204
+ var btn = document.getElementById("settings-shutdown-btn");
205
+ var errorEl = document.getElementById("settings-shutdown-error");
206
+ if (input) { input.value = ""; input.disabled = false; }
207
+ if (btn) { btn.disabled = true; btn.textContent = "Shutdown"; }
208
+ if (errorEl) errorEl.classList.add("hidden");
209
+ }
210
+
211
+ function closeSettings() {
212
+ settingsEl.classList.add("hidden");
213
+ settingsBtn.classList.remove("active");
214
+ if (statsTimer) {
215
+ clearInterval(statsTimer);
216
+ statsTimer = null;
217
+ }
218
+ }
219
+
220
+ export function isSettingsOpen() {
221
+ return settingsEl && !settingsEl.classList.contains("hidden");
222
+ }
223
+
224
+ function requestStats() {
225
+ var ws = ctx.ws;
226
+ if (ws && ws.readyState === 1) {
227
+ ws.send(JSON.stringify({ type: "process_stats" }));
228
+ }
229
+ }
230
+
231
+ function populateSettings() {
232
+ // Server name
233
+ var nameEl = document.getElementById("settings-server-name");
234
+ var projNameEl = document.getElementById("settings-project-name");
235
+ var cwdEl = document.getElementById("settings-project-cwd");
236
+ var versionEl = document.getElementById("settings-server-version");
237
+ var slugEl = document.getElementById("settings-project-slug");
238
+ var wsPathEl = document.getElementById("settings-ws-path");
239
+ var skipPermsEl = document.getElementById("settings-skip-perms");
240
+
241
+ var projectName = ctx.projectName || "-";
242
+ if (nameEl) nameEl.textContent = projectName;
243
+ if (projNameEl) projNameEl.textContent = projectName;
244
+ if (cwdEl) cwdEl.textContent = ctx.projectName || "-";
245
+
246
+ var footerVersion = document.getElementById("footer-version");
247
+ if (versionEl && footerVersion) {
248
+ versionEl.textContent = footerVersion.textContent || "-";
249
+ }
250
+
251
+ if (slugEl) slugEl.textContent = ctx.currentSlug || "(default)";
252
+ if (wsPathEl) wsPathEl.textContent = ctx.wsPath || "/ws";
253
+
254
+ // Skip permissions
255
+ var spBanner = document.getElementById("skip-perms-banner");
256
+ if (skipPermsEl) {
257
+ var isSkip = spBanner && !spBanner.classList.contains("hidden");
258
+ skipPermsEl.textContent = isSkip ? "Enabled" : "Disabled";
259
+ skipPermsEl.classList.toggle("settings-badge-on", isSkip);
260
+ }
261
+
262
+ // Sync notification toggles
263
+ syncNotifToggles();
264
+
265
+ // Theme
266
+ updateThemeDisplay();
267
+
268
+ // Context view
269
+ updateContextViewButtons();
270
+
271
+ // Models
272
+ updateModelList();
273
+ }
274
+
275
+ function syncNotifToggles() {
276
+ var pairs = [
277
+ ["notif-toggle-alert", "settings-notif-alert"],
278
+ ["notif-toggle-sound", "settings-notif-sound"],
279
+ ["notif-toggle-push", "settings-notif-push"],
280
+ ];
281
+ for (var i = 0; i < pairs.length; i++) {
282
+ var src = document.getElementById(pairs[i][0]);
283
+ var dst = document.getElementById(pairs[i][1]);
284
+ if (src && dst) dst.checked = src.checked;
285
+ }
286
+ }
287
+
288
+ function updateThemeDisplay() {
289
+ var container = document.getElementById("settings-theme-picker-container");
290
+ if (container) {
291
+ openSettingsThemePicker(container);
292
+ }
293
+ }
294
+
295
+ function updateContextViewButtons() {
296
+ var view = "off";
297
+ try { view = localStorage.getItem("clay-context-view") || "off"; } catch (e) {}
298
+ var btns = document.querySelectorAll("#settings-context-view .settings-btn-option");
299
+ for (var i = 0; i < btns.length; i++) {
300
+ btns[i].classList.toggle("active", btns[i].dataset.view === view);
301
+ }
302
+ }
303
+
304
+ function updateModelList() {
305
+ var listEl = document.getElementById("settings-model-list");
306
+ var currentEl = document.getElementById("settings-current-model");
307
+ if (!listEl) return;
308
+
309
+ var models = ctx.currentModels || [];
310
+ var currentModel = ctx._currentModelValue || "";
311
+
312
+ // Look up display name for settings panel
313
+ var displayName = currentModel;
314
+ for (var j = 0; j < models.length; j++) {
315
+ if (models[j].value === currentModel && models[j].displayName) {
316
+ displayName = models[j].displayName;
317
+ break;
318
+ }
319
+ }
320
+ if (currentEl) currentEl.textContent = displayName || "-";
321
+
322
+ listEl.innerHTML = "";
323
+ if (models.length === 0) {
324
+ listEl.innerHTML = '<div style="font-size:13px;color:var(--text-dimmer);">No models available</div>';
325
+ return;
326
+ }
327
+
328
+ for (var i = 0; i < models.length; i++) {
329
+ var m = models[i];
330
+ var value = m.value || "";
331
+ var label = m.displayName || value;
332
+ var item = document.createElement("div");
333
+ item.className = "settings-model-item";
334
+ if (label === currentModel || value === currentModel) item.classList.add("active");
335
+ item.dataset.model = value;
336
+ item.textContent = label;
337
+ listEl.appendChild(item);
338
+ }
339
+ }
340
+
341
+ export function updateSettingsStats(data) {
342
+ if (!isSettingsOpen()) return;
343
+ var pid = document.getElementById("settings-status-pid");
344
+ var uptime = document.getElementById("settings-status-uptime");
345
+ var rss = document.getElementById("settings-status-rss");
346
+ var sessions = document.getElementById("settings-status-sessions");
347
+ var clients = document.getElementById("settings-status-clients");
348
+
349
+ if (pid) pid.textContent = String(data.pid);
350
+ if (uptime) uptime.textContent = formatUptime(data.uptime);
351
+ if (rss) rss.textContent = formatBytes(data.memory.rss);
352
+ if (sessions) sessions.textContent = String(data.sessions);
353
+ if (clients) clients.textContent = String(data.clients);
354
+ }
355
+
356
+ export function updateSettingsModels(current, models) {
357
+ if (!ctx) return;
358
+ ctx.currentModels = models;
359
+ ctx._currentModelValue = current;
360
+ if (isSettingsOpen()) {
361
+ updateModelList();
362
+ }
363
+ }
364
+
365
+ // --- Daemon config ---
366
+ function requestDaemonConfig() {
367
+ var ws = ctx.ws;
368
+ if (ws && ws.readyState === 1) {
369
+ ws.send(JSON.stringify({ type: "get_daemon_config" }));
370
+ }
371
+ }
372
+
373
+ export function updateDaemonConfig(config) {
374
+ // Port
375
+ var portEl = document.getElementById("settings-port");
376
+ if (portEl) portEl.textContent = String(config.port || "-");
377
+
378
+ // TLS
379
+ var tlsEl = document.getElementById("settings-tls");
380
+ if (tlsEl) {
381
+ tlsEl.textContent = config.tls ? "Enabled" : "Disabled";
382
+ tlsEl.classList.toggle("settings-badge-green", !!config.tls);
383
+ }
384
+
385
+ // Debug
386
+ var debugEl = document.getElementById("settings-debug");
387
+ if (debugEl) {
388
+ debugEl.textContent = config.debug ? "Enabled" : "Disabled";
389
+ debugEl.classList.toggle("settings-badge-on", !!config.debug);
390
+ }
391
+
392
+ // PIN status
393
+ updatePinStatus(!!config.pinEnabled);
394
+
395
+ // Keep awake
396
+ var keepAwakeToggle = document.getElementById("settings-keep-awake");
397
+ if (keepAwakeToggle) keepAwakeToggle.checked = !!config.keepAwake;
398
+
399
+ // Show keep awake card only on macOS
400
+ var keepAwakeCard = document.getElementById("settings-keep-awake-card");
401
+ if (keepAwakeCard) {
402
+ if (config.platform === "darwin") {
403
+ keepAwakeCard.classList.remove("hidden");
404
+ } else {
405
+ keepAwakeCard.classList.add("hidden");
406
+ }
407
+ }
408
+ }
409
+
410
+ export function handleSetPinResult(msg) {
411
+ if (msg.ok) {
412
+ updatePinStatus(!!msg.pinEnabled);
413
+ hidePinForm();
414
+ showToast(msg.pinEnabled ? "PIN set successfully" : "PIN removed");
415
+ }
416
+ }
417
+
418
+ export function handleKeepAwakeChanged(msg) {
419
+ var keepAwakeToggle = document.getElementById("settings-keep-awake");
420
+ if (keepAwakeToggle) keepAwakeToggle.checked = !!msg.keepAwake;
421
+ }
422
+
423
+ export function handleShutdownResult(msg) {
424
+ var shutdownInput = document.getElementById("settings-shutdown-input");
425
+ var shutdownBtn = document.getElementById("settings-shutdown-btn");
426
+ var errorEl = document.getElementById("settings-shutdown-error");
427
+
428
+ if (msg.ok) {
429
+ if (shutdownBtn) shutdownBtn.textContent = "Server stopped";
430
+ showToast("Server is shutting down...");
431
+ } else {
432
+ if (shutdownBtn) {
433
+ shutdownBtn.textContent = "Shutdown";
434
+ shutdownBtn.disabled = false;
435
+ }
436
+ if (shutdownInput) shutdownInput.disabled = false;
437
+ if (errorEl) {
438
+ errorEl.textContent = msg.error || "Shutdown failed";
439
+ errorEl.classList.remove("hidden");
440
+ }
441
+ }
442
+ }
443
+
444
+ // --- PIN form management ---
445
+ function showPinForm() {
446
+ var form = document.getElementById("settings-pin-form");
447
+ var input = document.getElementById("settings-pin-input");
448
+ var errorEl = document.getElementById("settings-pin-error");
449
+ if (form) form.classList.remove("hidden");
450
+ if (errorEl) errorEl.classList.add("hidden");
451
+ if (input) { input.value = ""; input.focus(); }
452
+ }
453
+
454
+ function hidePinForm() {
455
+ var form = document.getElementById("settings-pin-form");
456
+ var input = document.getElementById("settings-pin-input");
457
+ var errorEl = document.getElementById("settings-pin-error");
458
+ if (form) form.classList.add("hidden");
459
+ if (input) input.value = "";
460
+ if (errorEl) errorEl.classList.add("hidden");
461
+ }
462
+
463
+ function submitPin() {
464
+ var input = document.getElementById("settings-pin-input");
465
+ var errorEl = document.getElementById("settings-pin-error");
466
+ if (!input) return;
467
+ var pin = input.value.trim();
468
+ if (!/^\d{6}$/.test(pin)) {
469
+ if (errorEl) errorEl.classList.remove("hidden");
470
+ input.focus();
471
+ return;
472
+ }
473
+ if (errorEl) errorEl.classList.add("hidden");
474
+ var ws = ctx.ws;
475
+ if (ws && ws.readyState === 1) {
476
+ ws.send(JSON.stringify({ type: "set_pin", pin: pin }));
477
+ }
478
+ }
479
+
480
+ function updatePinStatus(enabled) {
481
+ var statusEl = document.getElementById("settings-pin-status");
482
+ var setBtn = document.getElementById("settings-pin-set-btn");
483
+ var removeBtn = document.getElementById("settings-pin-remove-btn");
484
+ var actionLabel = document.getElementById("settings-pin-action-label");
485
+
486
+ if (statusEl) {
487
+ statusEl.textContent = enabled ? "Enabled" : "Disabled";
488
+ statusEl.classList.toggle("settings-badge-green", enabled);
489
+ }
490
+ if (setBtn) setBtn.textContent = enabled ? "Change PIN" : "Set PIN";
491
+ if (removeBtn) removeBtn.classList.toggle("hidden", !enabled);
492
+ if (actionLabel) actionLabel.textContent = enabled ? "Change PIN" : "Set PIN";
493
+ }
494
+
495
+ function formatBytes(n) {
496
+ if (n >= 1073741824) return (n / 1073741824).toFixed(1) + " GB";
497
+ if (n >= 1048576) return (n / 1048576).toFixed(1) + " MB";
498
+ if (n >= 1024) return (n / 1024).toFixed(1) + " KB";
499
+ return n + " B";
500
+ }
501
+
502
+ function formatUptime(seconds) {
503
+ var d = Math.floor(seconds / 86400);
504
+ var h = Math.floor((seconds % 86400) / 3600);
505
+ var m = Math.floor((seconds % 3600) / 60);
506
+ var s = Math.floor(seconds % 60);
507
+ if (d > 0) return d + "d " + h + "h " + m + "m";
508
+ if (h > 0) return h + "h " + m + "m " + s + "s";
509
+ return m + "m " + s + "s";
510
+ }