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.
- package/.github/agents/Mobile Developer.agent.md +89 -0
- package/AGENTS.md +4 -0
- package/README.md +130 -0
- package/admin-ui/client.js +73 -43
- package/admin-ui/v1/admin_auth_service.js +197 -0
- package/admin-ui/v1/admin_user_store.js +71 -0
- package/admin-ui/v1/client.js +17 -0
- package/admin-ui/v1/controls/admin_shell.js +1399 -0
- package/admin-ui/v1/controls/group_box.js +84 -0
- package/admin-ui/v1/controls/stat_card.js +125 -0
- package/admin-ui/v1/server.js +658 -0
- package/admin-ui/v1/utils/formatters.js +68 -0
- package/docs/admin-extension-guide.md +345 -0
- package/docs/books/adaptive-control-improvements/01-control-candidate-matrix.md +122 -0
- package/docs/books/adaptive-control-improvements/02-tier-1-layout-playbooks.md +207 -0
- package/docs/books/adaptive-control-improvements/03-tier-2-navigation-form-overlay.md +140 -0
- package/docs/books/adaptive-control-improvements/04-cross-cutting-platform-functionality.md +141 -0
- package/docs/books/adaptive-control-improvements/05-styling-theming-density-upgrades.md +114 -0
- package/docs/books/adaptive-control-improvements/06-testing-quality-gates.md +97 -0
- package/docs/books/adaptive-control-improvements/07-delivery-roadmap-and-ownership.md +137 -0
- package/docs/books/adaptive-control-improvements/08-appendix-tier1-acceptance-and-pr-templates.md +261 -0
- package/docs/books/adaptive-control-improvements/README.md +66 -0
- package/docs/books/admin-ui-authentication/01-threat-model-and-goals.md +124 -0
- package/docs/books/admin-ui-authentication/02-session-model-and-token-model.md +75 -0
- package/docs/books/admin-ui-authentication/03-auth-middleware-patterns.md +77 -0
- package/docs/books/admin-ui-authentication/README.md +25 -0
- package/docs/books/creating-a-new-admin-ui/01-introduction-and-vision.md +130 -0
- package/docs/books/creating-a-new-admin-ui/02-architecture-and-data-flow.md +298 -0
- package/docs/books/creating-a-new-admin-ui/03-server-introspection.md +381 -0
- package/docs/books/creating-a-new-admin-ui/04-admin-module-adapter-layer.md +592 -0
- package/docs/books/creating-a-new-admin-ui/05-domain-controls-stat-cards-and-gauges.md +513 -0
- package/docs/books/creating-a-new-admin-ui/06-domain-controls-process-manager.md +544 -0
- package/docs/books/creating-a-new-admin-ui/07-domain-controls-resource-pool-inspector.md +493 -0
- package/docs/books/creating-a-new-admin-ui/08-domain-controls-route-table-and-api-explorer.md +586 -0
- package/docs/books/creating-a-new-admin-ui/09-domain-controls-log-viewer-and-activity-feed.md +490 -0
- package/docs/books/creating-a-new-admin-ui/10-domain-controls-build-status-and-bundle-inspector.md +526 -0
- package/docs/books/creating-a-new-admin-ui/11-domain-controls-configuration-panel.md +808 -0
- package/docs/books/creating-a-new-admin-ui/12-admin-shell-layout-sidebar-navigation.md +210 -0
- package/docs/books/creating-a-new-admin-ui/13-telemetry-integration.md +556 -0
- package/docs/books/creating-a-new-admin-ui/14-realtime-sse-observable-integration.md +485 -0
- package/docs/books/creating-a-new-admin-ui/15-styling-theming-aero-design-system.md +521 -0
- package/docs/books/creating-a-new-admin-ui/16-testing-and-quality-assurance.md +147 -0
- package/docs/books/creating-a-new-admin-ui/17-next-steps-process-resource-roadmap.md +356 -0
- package/docs/books/creating-a-new-admin-ui/README.md +68 -0
- package/docs/books/device-adaptive-composition/01-platform-feature-audit.md +177 -0
- package/docs/books/device-adaptive-composition/02-responsive-composition-model.md +187 -0
- package/docs/books/device-adaptive-composition/03-data-model-vs-view-model.md +231 -0
- package/docs/books/device-adaptive-composition/04-styling-theme-breakpoints.md +234 -0
- package/docs/books/device-adaptive-composition/05-showcase-app-multi-device-assessment.md +193 -0
- package/docs/books/device-adaptive-composition/06-implementation-patterns-and-apis.md +346 -0
- package/docs/books/device-adaptive-composition/07-testing-harness-and-quality-gates.md +265 -0
- package/docs/books/device-adaptive-composition/08-roadmap-and-adoption-plan.md +250 -0
- package/docs/books/device-adaptive-composition/README.md +47 -0
- package/docs/comparison-report-express-plex-cpanel.md +549 -0
- package/docs/designs/server-admin-interface-aero.svg +611 -0
- package/docs/troubleshooting.md +84 -53
- package/module.js +16 -11
- package/package.json +1 -1
- package/serve-factory.js +1 -0
- package/server.js +199 -0
- package/tests/README.md +5 -0
- package/tests/admin-ui-jsgui-controls.test.js +581 -0
- package/tests/test-runner.js +1 -0
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-071799b982906680f5fd699d.js +0 -40
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-07352945ad5c92654fcb8b65.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-138a601fadb6191ea314c6fd.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-171f6c381c2cadf2e9fa7087.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-1d973388156b84a04373fac9.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-20e117bc8a10d2cd16234bbe.js +0 -40
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-2b028a82b0e5efddba42425f.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-4518556cd5c7e059e82b22b8.js +0 -40
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-5bac1aa0f213902f718ed74f.js +0 -40
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-5f9996ac7822caf777d92f56.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-60a92c702e65fd9cf748e3ec.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-6164c1f8f738995c541895d2.js +0 -44
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-6718a85eb9e5aa782dd47a05.js +0 -45
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-69e280f14e37aee76a1d4675.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-7570d1b030d44b111ed59c4c.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-7798c9bbd55e510d5039f936.js +0 -42
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-78cd511ea1ef18ecb03d1be5.js +0 -40
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-7d482e0b95bcb5e3c543118b.js +0 -43
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-80e9476d1127c55b40fdb36f.js +0 -40
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-810ced55d5320a3088a05b13.js +0 -40
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-8423565f1a40e329afc8c6cf.js +0 -40
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-900bef783b8cee36506ec282.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-a1a37aff6416fdad74040ddf.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-ad48d5e8eda40f175b4df090.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-aec5a2d963015528c9099462.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-af9d34e0f1722fab9e28c269.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-b818e4015e2f1fe86280b5ab.js +0 -41
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-bcb2541adc70b7aba61768c5.js +0 -44
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-bfe89d2c78ed44f95ed7dd73.js +0 -40
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-c06f04806a1e688e1187110c.js +0 -40
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-c3f3adf904f585afc544b96a.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-d45acb873e1d8e32d5e60f2e.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-db06f132533706f4a0163b8c.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-f660f40d78b135fc8560a862.js +0 -39
- 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.
|