jsgui3-server 0.0.149 → 0.0.150

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 (98) hide show
  1. package/.github/agents/Mobile Developer.agent.md +89 -0
  2. package/AGENTS.md +4 -0
  3. package/README.md +130 -0
  4. package/admin-ui/client.js +73 -43
  5. package/admin-ui/v1/admin_auth_service.js +197 -0
  6. package/admin-ui/v1/admin_user_store.js +71 -0
  7. package/admin-ui/v1/client.js +17 -0
  8. package/admin-ui/v1/controls/admin_shell.js +1399 -0
  9. package/admin-ui/v1/controls/group_box.js +84 -0
  10. package/admin-ui/v1/controls/stat_card.js +125 -0
  11. package/admin-ui/v1/server.js +658 -0
  12. package/admin-ui/v1/utils/formatters.js +68 -0
  13. package/docs/admin-extension-guide.md +345 -0
  14. package/docs/books/adaptive-control-improvements/01-control-candidate-matrix.md +122 -0
  15. package/docs/books/adaptive-control-improvements/02-tier-1-layout-playbooks.md +207 -0
  16. package/docs/books/adaptive-control-improvements/03-tier-2-navigation-form-overlay.md +140 -0
  17. package/docs/books/adaptive-control-improvements/04-cross-cutting-platform-functionality.md +141 -0
  18. package/docs/books/adaptive-control-improvements/05-styling-theming-density-upgrades.md +114 -0
  19. package/docs/books/adaptive-control-improvements/06-testing-quality-gates.md +97 -0
  20. package/docs/books/adaptive-control-improvements/07-delivery-roadmap-and-ownership.md +137 -0
  21. package/docs/books/adaptive-control-improvements/08-appendix-tier1-acceptance-and-pr-templates.md +261 -0
  22. package/docs/books/adaptive-control-improvements/README.md +66 -0
  23. package/docs/books/admin-ui-authentication/01-threat-model-and-goals.md +124 -0
  24. package/docs/books/admin-ui-authentication/02-session-model-and-token-model.md +75 -0
  25. package/docs/books/admin-ui-authentication/03-auth-middleware-patterns.md +77 -0
  26. package/docs/books/admin-ui-authentication/README.md +25 -0
  27. package/docs/books/creating-a-new-admin-ui/01-introduction-and-vision.md +130 -0
  28. package/docs/books/creating-a-new-admin-ui/02-architecture-and-data-flow.md +298 -0
  29. package/docs/books/creating-a-new-admin-ui/03-server-introspection.md +381 -0
  30. package/docs/books/creating-a-new-admin-ui/04-admin-module-adapter-layer.md +592 -0
  31. package/docs/books/creating-a-new-admin-ui/05-domain-controls-stat-cards-and-gauges.md +513 -0
  32. package/docs/books/creating-a-new-admin-ui/06-domain-controls-process-manager.md +544 -0
  33. package/docs/books/creating-a-new-admin-ui/07-domain-controls-resource-pool-inspector.md +493 -0
  34. package/docs/books/creating-a-new-admin-ui/08-domain-controls-route-table-and-api-explorer.md +586 -0
  35. package/docs/books/creating-a-new-admin-ui/09-domain-controls-log-viewer-and-activity-feed.md +490 -0
  36. package/docs/books/creating-a-new-admin-ui/10-domain-controls-build-status-and-bundle-inspector.md +526 -0
  37. package/docs/books/creating-a-new-admin-ui/11-domain-controls-configuration-panel.md +808 -0
  38. package/docs/books/creating-a-new-admin-ui/12-admin-shell-layout-sidebar-navigation.md +210 -0
  39. package/docs/books/creating-a-new-admin-ui/13-telemetry-integration.md +556 -0
  40. package/docs/books/creating-a-new-admin-ui/14-realtime-sse-observable-integration.md +485 -0
  41. package/docs/books/creating-a-new-admin-ui/15-styling-theming-aero-design-system.md +521 -0
  42. package/docs/books/creating-a-new-admin-ui/16-testing-and-quality-assurance.md +147 -0
  43. package/docs/books/creating-a-new-admin-ui/17-next-steps-process-resource-roadmap.md +356 -0
  44. package/docs/books/creating-a-new-admin-ui/README.md +68 -0
  45. package/docs/books/device-adaptive-composition/01-platform-feature-audit.md +177 -0
  46. package/docs/books/device-adaptive-composition/02-responsive-composition-model.md +187 -0
  47. package/docs/books/device-adaptive-composition/03-data-model-vs-view-model.md +231 -0
  48. package/docs/books/device-adaptive-composition/04-styling-theme-breakpoints.md +234 -0
  49. package/docs/books/device-adaptive-composition/05-showcase-app-multi-device-assessment.md +193 -0
  50. package/docs/books/device-adaptive-composition/06-implementation-patterns-and-apis.md +346 -0
  51. package/docs/books/device-adaptive-composition/07-testing-harness-and-quality-gates.md +265 -0
  52. package/docs/books/device-adaptive-composition/08-roadmap-and-adoption-plan.md +250 -0
  53. package/docs/books/device-adaptive-composition/README.md +47 -0
  54. package/docs/comparison-report-express-plex-cpanel.md +549 -0
  55. package/docs/designs/server-admin-interface-aero.svg +611 -0
  56. package/docs/troubleshooting.md +84 -53
  57. package/module.js +16 -11
  58. package/package.json +1 -1
  59. package/serve-factory.js +1 -0
  60. package/server.js +199 -0
  61. package/tests/README.md +5 -0
  62. package/tests/admin-ui-jsgui-controls.test.js +581 -0
  63. package/tests/test-runner.js +1 -0
  64. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-071799b982906680f5fd699d.js +0 -40
  65. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-07352945ad5c92654fcb8b65.js +0 -39
  66. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-138a601fadb6191ea314c6fd.js +0 -39
  67. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-171f6c381c2cadf2e9fa7087.js +0 -39
  68. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-1d973388156b84a04373fac9.js +0 -39
  69. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-20e117bc8a10d2cd16234bbe.js +0 -40
  70. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-2b028a82b0e5efddba42425f.js +0 -39
  71. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-4518556cd5c7e059e82b22b8.js +0 -40
  72. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-5bac1aa0f213902f718ed74f.js +0 -40
  73. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-5f9996ac7822caf777d92f56.js +0 -39
  74. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-60a92c702e65fd9cf748e3ec.js +0 -39
  75. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-6164c1f8f738995c541895d2.js +0 -44
  76. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-6718a85eb9e5aa782dd47a05.js +0 -45
  77. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-69e280f14e37aee76a1d4675.js +0 -39
  78. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-7570d1b030d44b111ed59c4c.js +0 -39
  79. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-7798c9bbd55e510d5039f936.js +0 -42
  80. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-78cd511ea1ef18ecb03d1be5.js +0 -40
  81. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-7d482e0b95bcb5e3c543118b.js +0 -43
  82. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-80e9476d1127c55b40fdb36f.js +0 -40
  83. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-810ced55d5320a3088a05b13.js +0 -40
  84. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-8423565f1a40e329afc8c6cf.js +0 -40
  85. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-900bef783b8cee36506ec282.js +0 -39
  86. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-a1a37aff6416fdad74040ddf.js +0 -39
  87. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-ad48d5e8eda40f175b4df090.js +0 -39
  88. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-aec5a2d963015528c9099462.js +0 -39
  89. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-af9d34e0f1722fab9e28c269.js +0 -39
  90. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-b818e4015e2f1fe86280b5ab.js +0 -41
  91. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-bcb2541adc70b7aba61768c5.js +0 -44
  92. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-bfe89d2c78ed44f95ed7dd73.js +0 -40
  93. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-c06f04806a1e688e1187110c.js +0 -40
  94. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-c3f3adf904f585afc544b96a.js +0 -39
  95. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-d45acb873e1d8e32d5e60f2e.js +0 -39
  96. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-db06f132533706f4a0163b8c.js +0 -39
  97. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-f660f40d78b135fc8560a862.js +0 -39
  98. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-f9dee4ec18a96e09bee06bae.js +0 -39
@@ -0,0 +1,513 @@
1
+ # Chapter 5: Domain Controls — Stat Cards & Gauges
2
+
3
+ ## Overview
4
+
5
+ Stat cards are the most prominent visual elements in the admin UI. They appear as a horizontal row across the top of the dashboard, each displaying a single key metric with a label, a large value, and a supplementary detail line. The design reference shows five stat cards:
6
+
7
+ 1. **Main Process** — PID with running status and memory
8
+ 2. **Child Processes** — Count with child names
9
+ 3. **Resource Pool** — Loaded count with health status
10
+ 4. **Routes** — Total count with category breakdown
11
+ 5. **Requests/Min** — Throughput with trend indicator
12
+
13
+ This chapter specifies the `Stat_Card` control and supporting gauge/indicator primitives.
14
+
15
+ ---
16
+
17
+ ## Stat_Card Control
18
+
19
+ ### Spec
20
+
21
+ ```javascript
22
+ {
23
+ __type_name: 'stat_card',
24
+ label: 'MAIN PROCESS', // Small caps label at top
25
+ value: 'PID 7824', // Large primary value
26
+ detail: '▲ Running — 128 MB RSS', // Small detail line at bottom
27
+ detail_color: 'green', // 'green', 'blue', 'amber', 'red', 'gray'
28
+ indicator: 'running', // Optional status indicator: 'running', 'stopped', 'warning', 'error'
29
+ width: 220 // Optional fixed width (default: flex)
30
+ }
31
+ ```
32
+
33
+ ### Visual Anatomy
34
+
35
+ ```
36
+ ┌──────────────────────────┐
37
+ │ MAIN PROCESS │ ← label (9px, gray, caps, letter-spacing)
38
+ │ │
39
+ │ PID 7824 ● │ ← value (22px, bold, dark) + optional indicator
40
+ │ │
41
+ │ ▲ Running — 128 MB RSS │ ← detail (9px, colored)
42
+ └──────────────────────────┘
43
+ ```
44
+
45
+ ### Constructor Pattern
46
+
47
+ ```javascript
48
+ class Stat_Card extends jsgui.Control {
49
+ constructor(spec = {}) {
50
+ spec.__type_name = spec.__type_name || 'stat_card';
51
+ super(spec);
52
+ const { context } = this;
53
+
54
+ this._label_text = spec.label || '';
55
+ this._value_text = spec.value || '';
56
+ this._detail_text = spec.detail || '';
57
+ this._detail_color = spec.detail_color || 'gray';
58
+ this._indicator = spec.indicator || null;
59
+
60
+ const compose = () => {
61
+ // Label
62
+ const label = new controls.div({ context, class: 'stat-card-label' });
63
+ label.add(this._label_text);
64
+ this.add(label);
65
+
66
+ // Value row
67
+ const value_row = new controls.div({ context, class: 'stat-card-value-row' });
68
+ this.add(value_row);
69
+
70
+ const value = new controls.span({ context, class: 'stat-card-value' });
71
+ value.add(this._value_text);
72
+ value_row.add(value);
73
+ this._value_el = value;
74
+
75
+ if (this._indicator) {
76
+ const indicator = new controls.span({
77
+ context,
78
+ class: `stat-card-indicator indicator-${this._indicator}`
79
+ });
80
+ value_row.add(indicator);
81
+ this._indicator_el = indicator;
82
+ }
83
+
84
+ // Detail
85
+ const detail = new controls.div({
86
+ context,
87
+ class: `stat-card-detail detail-${this._detail_color}`
88
+ });
89
+ detail.add(this._detail_text);
90
+ this.add(detail);
91
+ this._detail_el = detail;
92
+ };
93
+
94
+ if (!spec.el) { compose(); }
95
+ }
96
+
97
+ // Client-side update methods
98
+ set_value(text) {
99
+ if (this._value_el && this._value_el.el) {
100
+ this._value_el.el.innerText = text;
101
+ }
102
+ }
103
+
104
+ set_detail(text, color) {
105
+ if (this._detail_el && this._detail_el.el) {
106
+ this._detail_el.el.innerText = text;
107
+ if (color) {
108
+ this._detail_el.el.className = `stat-card-detail detail-${color}`;
109
+ }
110
+ }
111
+ }
112
+
113
+ set_indicator(state) {
114
+ if (this._indicator_el && this._indicator_el.el) {
115
+ this._indicator_el.el.className = `stat-card-indicator indicator-${state}`;
116
+ }
117
+ }
118
+ }
119
+ ```
120
+
121
+ ### CSS
122
+
123
+ ```css
124
+ .stat_card {
125
+ background: linear-gradient(to bottom, #F6F4F0, #EAE8E2);
126
+ border: 1px solid #C0B8A8;
127
+ border-radius: 4px;
128
+ padding: 12px 16px;
129
+ min-width: 130px;
130
+ box-shadow: 0 1px 3px rgba(0,0,0,0.12);
131
+ }
132
+
133
+ .stat-card-label {
134
+ font-size: 9px;
135
+ color: #808080;
136
+ font-weight: 600;
137
+ letter-spacing: 0.5px;
138
+ text-transform: uppercase;
139
+ margin-bottom: 8px;
140
+ }
141
+
142
+ .stat-card-value-row {
143
+ display: flex;
144
+ align-items: center;
145
+ justify-content: space-between;
146
+ margin-bottom: 8px;
147
+ }
148
+
149
+ .stat-card-value {
150
+ font-size: 22px;
151
+ font-weight: 700;
152
+ color: #2A4060;
153
+ }
154
+
155
+ .stat-card-indicator {
156
+ width: 12px;
157
+ height: 12px;
158
+ border-radius: 50%;
159
+ display: inline-block;
160
+ }
161
+
162
+ .indicator-running {
163
+ background: #48B848;
164
+ box-shadow: 0 0 4px rgba(72, 184, 72, 0.5);
165
+ }
166
+
167
+ .indicator-stopped {
168
+ background: #808080;
169
+ }
170
+
171
+ .indicator-warning {
172
+ background: #D8A020;
173
+ box-shadow: 0 0 4px rgba(216, 160, 32, 0.5);
174
+ }
175
+
176
+ .indicator-error {
177
+ background: #CC4444;
178
+ box-shadow: 0 0 4px rgba(204, 68, 68, 0.5);
179
+ }
180
+
181
+ .stat-card-detail {
182
+ font-size: 9px;
183
+ }
184
+
185
+ .detail-green { color: #48A848; }
186
+ .detail-blue { color: #4488CC; }
187
+ .detail-amber { color: #D8A020; }
188
+ .detail-red { color: #CC4444; }
189
+ .detail-gray { color: #808080; }
190
+ ```
191
+
192
+ ---
193
+
194
+ ## Stat Card Instances — Dashboard Row
195
+
196
+ ### 1. Main Process Card
197
+
198
+ ```javascript
199
+ const main_process_card = new Stat_Card({
200
+ context,
201
+ label: 'MAIN PROCESS',
202
+ value: `PID ${snapshot.server.pid}`,
203
+ detail: `▲ Running — ${format_bytes(snapshot.memory.rss)} RSS`,
204
+ detail_color: 'green',
205
+ indicator: 'running'
206
+ });
207
+ ```
208
+
209
+ **Data source**: `GET /api/admin/snapshot` → `server.pid`, `memory.rss`
210
+
211
+ **Update strategy**: Poll every 5s, update value and detail text.
212
+
213
+ ### 2. Child Processes Card
214
+
215
+ ```javascript
216
+ const children_card = new Stat_Card({
217
+ context,
218
+ label: 'CHILD PROCESSES',
219
+ value: `${snapshot.processes.children.length}`,
220
+ detail: snapshot.processes.children.map(c => c.name).join(' · ') || 'No child processes',
221
+ detail_color: 'blue'
222
+ });
223
+ ```
224
+
225
+ **Data source**: `GET /api/admin/snapshot` → `processes.children`
226
+
227
+ ### 3. Resource Pool Card
228
+
229
+ ```javascript
230
+ const resource_card = new Stat_Card({
231
+ context,
232
+ label: 'RESOURCE POOL',
233
+ value: `${snapshot.resources.total}`,
234
+ detail: snapshot.resources.crashed > 0
235
+ ? `${snapshot.resources.crashed} crashed`
236
+ : 'All requirements met',
237
+ detail_color: snapshot.resources.crashed > 0 ? 'red' : 'green'
238
+ });
239
+ ```
240
+
241
+ **Data source**: `GET /api/admin/snapshot` → `resources.total`, `resources.crashed`
242
+
243
+ ### 4. Routes Card
244
+
245
+ ```javascript
246
+ const routes_card = new Stat_Card({
247
+ context,
248
+ label: 'ROUTES',
249
+ value: `${snapshot.routes.length}`,
250
+ detail: summarize_route_categories(snapshot.routes),
251
+ detail_color: 'blue'
252
+ });
253
+ ```
254
+
255
+ **Helper function:**
256
+ ```javascript
257
+ function summarize_route_categories(routes) {
258
+ const counts = {};
259
+ routes.forEach(r => {
260
+ counts[r.category] = (counts[r.category] || 0) + 1;
261
+ });
262
+ return Object.entries(counts)
263
+ .map(([cat, n]) => `${n} ${cat}`)
264
+ .join(' · ');
265
+ }
266
+ ```
267
+
268
+ ### 5. Requests/Min Card
269
+
270
+ ```javascript
271
+ const requests_card = new Stat_Card({
272
+ context,
273
+ label: 'REQUESTS / MIN',
274
+ value: `${snapshot.requests.per_minute}`,
275
+ detail: '— no trend data yet',
276
+ detail_color: 'gray'
277
+ });
278
+ ```
279
+
280
+ **Data source**: `GET /api/admin/snapshot` → `requests.per_minute`
281
+
282
+ **Update strategy**: Recalculate client-side from SSE `request` events received in the last 60 seconds.
283
+
284
+ ---
285
+
286
+ ## Stat Card Row Layout
287
+
288
+ The five cards sit in a flex row at the top of the dashboard content area:
289
+
290
+ ```css
291
+ .stat-card-row {
292
+ display: flex;
293
+ gap: 12px;
294
+ padding: 14px 16px;
295
+ flex-wrap: wrap;
296
+ }
297
+
298
+ .stat-card-row .stat_card {
299
+ flex: 1 1 180px;
300
+ max-width: 260px;
301
+ }
302
+ ```
303
+
304
+ ---
305
+
306
+ ## Supporting Primitives
307
+
308
+ ### Health_Badge
309
+
310
+ A small inline badge showing resource health.
311
+
312
+ ```javascript
313
+ class Health_Badge extends jsgui.Control {
314
+ constructor(spec = {}) {
315
+ spec.__type_name = spec.__type_name || 'health_badge';
316
+ super(spec);
317
+ const { context } = this;
318
+
319
+ const compose = () => {
320
+ const dot = new controls.span({ context, class: `health-dot health-${spec.state || 'unknown'}` });
321
+ this.add(dot);
322
+
323
+ if (spec.text) {
324
+ const label = new controls.span({ context, class: 'health-label' });
325
+ label.add(spec.text);
326
+ this.add(label);
327
+ }
328
+ };
329
+
330
+ if (!spec.el) { compose(); }
331
+ }
332
+
333
+ set_state(state, text) {
334
+ // Client-side update
335
+ if (this.el) {
336
+ const dot = this.el.querySelector('.health-dot');
337
+ if (dot) dot.className = `health-dot health-${state}`;
338
+ if (text) {
339
+ const label = this.el.querySelector('.health-label');
340
+ if (label) label.innerText = text;
341
+ }
342
+ }
343
+ }
344
+ }
345
+
346
+ Health_Badge.css = `
347
+ .health_badge {
348
+ display: inline-flex;
349
+ align-items: center;
350
+ gap: 6px;
351
+ padding: 2px 8px;
352
+ border-radius: 3px;
353
+ font-size: 8px;
354
+ font-weight: 500;
355
+ }
356
+
357
+ .health-dot {
358
+ width: 6px;
359
+ height: 6px;
360
+ border-radius: 50%;
361
+ display: inline-block;
362
+ }
363
+
364
+ .health-running, .health-ready, .health-on {
365
+ background: #48B848;
366
+ }
367
+
368
+ .health-stopped, .health-off {
369
+ background: #808080;
370
+ }
371
+
372
+ .health-warning, .health-unhealthy {
373
+ background: #D8A020;
374
+ }
375
+
376
+ .health-crashed, .health-error, .health-unreachable {
377
+ background: #CC4444;
378
+ }
379
+
380
+ .health-unknown {
381
+ background: #B0A898;
382
+ }
383
+
384
+ .health-label {
385
+ font-size: 9px;
386
+ }
387
+ `;
388
+ ```
389
+
390
+ ### Status_Indicator
391
+
392
+ A simple colored circle used inline. Used inside the toolbar to show "Server Online."
393
+
394
+ ```javascript
395
+ class Status_Indicator extends jsgui.Control {
396
+ constructor(spec = {}) {
397
+ spec.__type_name = spec.__type_name || 'status_indicator';
398
+ super(spec);
399
+ const { context } = this;
400
+
401
+ const compose = () => {
402
+ const dot = new controls.span({
403
+ context,
404
+ class: `status-dot status-${spec.state || 'unknown'}`
405
+ });
406
+ this.add(dot);
407
+
408
+ const label = new controls.span({ context, class: 'status-label' });
409
+ label.add(spec.text || '');
410
+ this.add(label);
411
+ };
412
+
413
+ if (!spec.el) { compose(); }
414
+ }
415
+ }
416
+
417
+ Status_Indicator.css = `
418
+ .status_indicator {
419
+ display: inline-flex;
420
+ align-items: center;
421
+ gap: 6px;
422
+ }
423
+
424
+ .status-dot {
425
+ width: 8px;
426
+ height: 8px;
427
+ border-radius: 50%;
428
+ }
429
+
430
+ .status-running { background: #48B848; }
431
+ .status-stopped { background: #808080; }
432
+ .status-warning { background: #D8A020; }
433
+ .status-error { background: #CC4444; }
434
+
435
+ .status-label {
436
+ font-size: 10px;
437
+ font-weight: 500;
438
+ }
439
+ `;
440
+ ```
441
+
442
+ ---
443
+
444
+ ## Utility Functions
445
+
446
+ ```javascript
447
+ function format_bytes(bytes) {
448
+ if (bytes === 0) return '0 B';
449
+ if (bytes === null || bytes === undefined) return '—';
450
+ const units = ['B', 'KB', 'MB', 'GB'];
451
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
452
+ return Math.round(bytes / Math.pow(1024, i)) + ' ' + units[i];
453
+ }
454
+
455
+ function format_uptime(seconds) {
456
+ const h = Math.floor(seconds / 3600);
457
+ const m = Math.floor((seconds % 3600) / 60);
458
+ const s = Math.floor(seconds % 60);
459
+ if (h > 0) return `${h}h ${m}m ${s}s`;
460
+ if (m > 0) return `${m}m ${s}s`;
461
+ return `${s}s`;
462
+ }
463
+
464
+ function format_time(timestamp) {
465
+ const d = new Date(timestamp);
466
+ return d.toTimeString().split(' ')[0]; // "14:23:07"
467
+ }
468
+ ```
469
+
470
+ ---
471
+
472
+ ## Data Binding Pattern
473
+
474
+ Each stat card can be bound to a data model for reactive updates:
475
+
476
+ ```javascript
477
+ // In Admin_Shell.activate():
478
+ const { Data_Object, field } = require('obext');
479
+
480
+ const stats_model = new Data_Object({
481
+ pid: field(0),
482
+ memory_rss: field(0),
483
+ child_count: field(0),
484
+ resource_count: field(0),
485
+ route_count: field(0),
486
+ requests_per_minute: field(0)
487
+ });
488
+
489
+ // Bind card updates to model changes
490
+ stats_model.on('change.memory_rss', (e) => {
491
+ main_process_card.set_detail(
492
+ `▲ Running — ${format_bytes(e.value)} RSS`,
493
+ 'green'
494
+ );
495
+ });
496
+
497
+ stats_model.on('change.requests_per_minute', (e) => {
498
+ requests_card.set_value(String(e.value));
499
+ });
500
+
501
+ // Poll for updates
502
+ setInterval(async () => {
503
+ const snapshot = await fetch('/api/admin/snapshot').then(r => r.json());
504
+ stats_model.pid = snapshot.server.pid;
505
+ stats_model.memory_rss = snapshot.memory.rss;
506
+ stats_model.child_count = snapshot.processes.children.length;
507
+ stats_model.resource_count = snapshot.resources.total;
508
+ stats_model.route_count = snapshot.routes.length;
509
+ stats_model.requests_per_minute = snapshot.requests.per_minute;
510
+ }, 5000);
511
+ ```
512
+
513
+ This separates data acquisition from UI updates, making the controls testable in isolation.