jsgui3-server 0.0.148 → 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/.github/workflows/control-scan-manifest-check.yml +31 -0
- package/AGENTS.md +4 -0
- package/README.md +215 -3
- package/admin-ui/client.js +81 -51
- 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/dev-status.svg +139 -0
- package/docs/admin-extension-guide.md +345 -0
- package/docs/api-reference.md +301 -43
- 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/books/jsgui3-bundling-research-book/00-table-of-contents.md +35 -0
- package/docs/books/jsgui3-bundling-research-book/01-pipeline-and-runtime-semantics.md +34 -0
- package/docs/books/jsgui3-bundling-research-book/02-javascript-bundling-core.md +36 -0
- package/docs/books/jsgui3-bundling-research-book/03-style-extraction-and-css-compilation.md +35 -0
- package/docs/books/jsgui3-bundling-research-book/04-static-publishing-and-delivery.md +39 -0
- package/docs/books/jsgui3-bundling-research-book/05-current-limits-and-size-bloat-vectors.md +25 -0
- package/docs/books/jsgui3-bundling-research-book/06-unused-module-elimination-strategy.md +77 -0
- package/docs/books/jsgui3-bundling-research-book/07-jsgui3-html-control-and-mixin-pruning.md +63 -0
- package/docs/books/jsgui3-bundling-research-book/08-test-and-verification-methodology.md +43 -0
- package/docs/books/jsgui3-bundling-research-book/09-roadmap-and-rollout.md +42 -0
- package/docs/books/jsgui3-bundling-research-book/10-further-research-strategies-and-upgrades.md +211 -0
- package/docs/books/jsgui3-bundling-research-book/README.md +35 -0
- package/docs/bundling-system-deep-dive.md +9 -4
- package/docs/comparison-report-express-plex-cpanel.md +549 -0
- package/docs/comprehensive-documentation.md +49 -18
- package/docs/configuration-reference.md +152 -27
- package/docs/core/README.md +19 -0
- package/docs/core/jsgui3-server-core-book/00-table-of-contents.md +21 -0
- package/docs/core/jsgui3-server-core-book/01-startup-readiness-state-machine.md +41 -0
- package/docs/core/jsgui3-server-core-book/02-resource-abstraction-and-lifecycle.md +92 -0
- package/docs/core/jsgui3-server-core-book/03-resource-pool-and-event-topology.md +47 -0
- package/docs/core/jsgui3-server-core-book/04-sse-publisher-semantics.md +41 -0
- package/docs/core/jsgui3-server-core-book/05-serve-factory-resource-wiring.md +46 -0
- package/docs/core/jsgui3-server-core-book/06-e2e-testing-methodology.md +48 -0
- package/docs/core/jsgui3-server-core-book/07-defect-detection-and-hardening-loop.md +47 -0
- package/docs/designs/server-admin-interface-aero.svg +611 -0
- package/docs/publishers-guide.md +59 -4
- package/docs/resources-guide.md +184 -35
- package/docs/simple-server-api-design.md +72 -17
- package/docs/system-architecture.md +18 -14
- package/docs/troubleshooting.md +84 -53
- package/examples/controls/15) window, observable SSE/server.js +6 -1
- package/examples/controls/19) window, auto observable ui/server.js +9 -0
- package/examples/controls/20) window, task manager app/README.md +133 -0
- package/examples/controls/20) window, task manager app/client.js +797 -0
- package/examples/controls/20) window, task manager app/server.js +178 -0
- package/examples/controls/6) window, color_palette/client.js +165 -68
- package/examples/controls/9) window, date picker/client.js +362 -76
- package/examples/controls/9b) window, shared data.model mirrored date pickers/client.js +104 -83
- package/examples/jsgui3-html/06) theming/client.js +22 -1
- package/examples/jsgui3-html/10) binding-debugger/client.js +137 -1
- package/http/responders/static/Static_Route_HTTP_Responder.js +52 -34
- package/lab/experiments/capture-color-controls.js +196 -0
- package/lab/results/screenshots/color-controls/full_page.png +0 -0
- package/lab/results/screenshots/color-controls/section_1_color_grid_12x12.png +0 -0
- package/lab/results/screenshots/color-controls/section_2_color_grid_4x2.png +0 -0
- package/lab/results/screenshots/color-controls/section_3_color_palette.png +0 -0
- package/lab/results/screenshots/color-controls/section_4_palette_comparison.png +0 -0
- package/lab/results/screenshots/color-controls/section_5_raw_swatches.png +0 -0
- package/lab/results/screenshots/color-controls/section_6_optimized_crayola.png +0 -0
- package/lab/results/screenshots/color-controls/section_7_pastel_palette.png +0 -0
- package/lab/results/screenshots/color-controls/section_8_extended_144.png +0 -0
- package/lab/screenshot-utils.js +248 -0
- package/module.js +12 -0
- package/package.json +12 -2
- package/publishers/Publishers.js +4 -3
- package/publishers/helpers/assigners/static-compressed-response-buffers/Single_Control_Webpage_Server_Static_Compressed_Response_Buffers_Assigner.js +5 -5
- package/publishers/http-sse-publisher.js +341 -0
- package/resources/process-resource.js +950 -0
- package/resources/processors/bundlers/js/esbuild/Advanced_JS_Bundler_Using_ESBuild.js +129 -33
- package/resources/processors/bundlers/js/esbuild/Core_JS_Non_Minifying_Bundler_Using_ESBuild.js +18 -7
- package/resources/processors/bundlers/js/esbuild/JSGUI3_HTML_Control_Optimizer.js +829 -0
- package/resources/remote-process-resource.js +355 -0
- package/resources/server-resource-pool.js +354 -41
- package/serve-factory.js +442 -259
- package/server.js +288 -13
- package/tests/README.md +71 -4
- package/tests/admin-ui-jsgui-controls.test.js +581 -0
- package/tests/admin-ui-render.test.js +24 -0
- package/tests/assigners.test.js +56 -40
- package/tests/bundling-default-control-elimination.puppeteer.test.js +260 -0
- package/tests/configuration-validation.test.js +21 -18
- package/tests/content-analysis.test.js +7 -6
- package/tests/control-optimizer-cache-behavior.test.js +52 -0
- package/tests/control-scan-manifest-regression.test.js +144 -0
- package/tests/end-to-end.test.js +15 -14
- package/tests/error-handling.test.js +222 -179
- package/tests/fixtures/bundling-default-button-client.js +37 -0
- package/tests/fixtures/bundling-default-window-client.js +34 -0
- package/tests/fixtures/control_scan_manifest_expectations.json +48 -0
- package/tests/fixtures/resource-monitor-client.js +319 -0
- package/tests/helpers/puppeteer-e2e-harness.js +317 -0
- package/tests/http-sse-publisher.test.js +136 -0
- package/tests/performance.test.js +69 -65
- package/tests/process-resource.test.js +138 -0
- package/tests/publishers.test.js +7 -7
- package/tests/remote-process-resource.test.js +160 -0
- package/tests/sass-controls.e2e.test.js +7 -1
- package/tests/serve-resources.test.js +270 -0
- package/tests/serve.test.js +120 -50
- package/tests/server-resource-pool.test.js +106 -0
- package/tests/small-controls-bundle-size.test.js +252 -0
- package/tests/test-runner.js +14 -1
- package/tests/window-examples.puppeteer.test.js +204 -1
- package/tests/window-resource-integration.puppeteer.test.js +585 -0
- package/tests/temp_invalid.js +0 -7
- package/tests/temp_invalid_utf8.js +0 -1
- package/tests/temp_malformed.js +0 -10
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
# Chapter 13: Telemetry Integration — Instrumenting the Server
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This chapter covers the code changes needed to make the server emit the telemetry data that the Admin UI consumes. Unlike the domain controls (Chapters 5–11), which specify client-side controls, this chapter specifies **server-side modifications** — the adapter functions that intercept, measure, and broadcast server activity.
|
|
6
|
+
|
|
7
|
+
The guiding principle: **instrument, don't restructure**. We wrap existing methods and listen to existing events rather than rewriting core systems.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## What Needs Instrumentation
|
|
12
|
+
|
|
13
|
+
From the data availability audit in Chapter 3, the following data sources require adapter-level instrumentation:
|
|
14
|
+
|
|
15
|
+
| Data | Current State | Instrumentation Needed |
|
|
16
|
+
|------|---------------|----------------------|
|
|
17
|
+
| Request count & timing | Not tracked | Wrap `router.process` |
|
|
18
|
+
| Request rate (req/min) | Not tracked | Rolling window counter |
|
|
19
|
+
| Response status codes | Not tracked | Wrap `res.end` |
|
|
20
|
+
| Route list | No listing API | Wrap `set_route` |
|
|
21
|
+
| Build timestamps | Not persisted | Listen to publisher `ready` |
|
|
22
|
+
| Bundle sizes (gzip) | Not computed | Compute on `ready` |
|
|
23
|
+
| SSE connection count | Available per-publisher | Aggregate across publishers |
|
|
24
|
+
| Process resource events | Events exist | Forward to SSE channel |
|
|
25
|
+
| Config snapshot | Scattered properties | Aggregate adapter function |
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Request Telemetry
|
|
30
|
+
|
|
31
|
+
### Interception Point
|
|
32
|
+
|
|
33
|
+
The server's request handling flows through `router.process(req, res)`. We wrap this method to capture timing and status:
|
|
34
|
+
|
|
35
|
+
```javascript
|
|
36
|
+
// In Admin_Module_V1.init(server)
|
|
37
|
+
instrument_request_handler(server) {
|
|
38
|
+
const router = server.router;
|
|
39
|
+
if (!router || !router.process) return;
|
|
40
|
+
|
|
41
|
+
// Preserve original
|
|
42
|
+
const original_process = router.process.bind(router);
|
|
43
|
+
|
|
44
|
+
// Counters
|
|
45
|
+
this._request_count = 0;
|
|
46
|
+
this._request_window = []; // timestamps for rate calculation
|
|
47
|
+
this._status_counts = {}; // { 200: 142, 404: 3, ... }
|
|
48
|
+
|
|
49
|
+
router.process = (req, res) => {
|
|
50
|
+
const start = Date.now();
|
|
51
|
+
this._request_count++;
|
|
52
|
+
|
|
53
|
+
// Track in rolling window (last 60 seconds)
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
this._request_window.push(now);
|
|
56
|
+
this._trim_request_window(now);
|
|
57
|
+
|
|
58
|
+
// Wrap res.end to capture status and timing
|
|
59
|
+
const original_end = res.end.bind(res);
|
|
60
|
+
res.end = (...args) => {
|
|
61
|
+
const duration_ms = Date.now() - start;
|
|
62
|
+
const status = res.statusCode || 200;
|
|
63
|
+
|
|
64
|
+
// Update status counts
|
|
65
|
+
this._status_counts[status] = (this._status_counts[status] || 0) + 1;
|
|
66
|
+
|
|
67
|
+
// Broadcast to SSE
|
|
68
|
+
this._broadcast_request({
|
|
69
|
+
method: req.method,
|
|
70
|
+
url: req.url,
|
|
71
|
+
status: status,
|
|
72
|
+
duration_ms: duration_ms,
|
|
73
|
+
timestamp: start,
|
|
74
|
+
content_length: res.getHeader('content-length') || null
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return original_end(...args);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return original_process(req, res);
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
_trim_request_window(now) {
|
|
85
|
+
const cutoff = now - 60000; // 60 second window
|
|
86
|
+
while (this._request_window.length > 0 && this._request_window[0] < cutoff) {
|
|
87
|
+
this._request_window.shift();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
get_requests_per_minute() {
|
|
92
|
+
this._trim_request_window(Date.now());
|
|
93
|
+
return this._request_window.length;
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Filter: Admin Routes
|
|
98
|
+
|
|
99
|
+
Admin API requests (`/api/admin/*`) should **not** be counted in the request telemetry to avoid feedback loops. The SSE connection itself also generates requests that shouldn't inflate the counter:
|
|
100
|
+
|
|
101
|
+
```javascript
|
|
102
|
+
// In the wrapped router.process
|
|
103
|
+
router.process = (req, res) => {
|
|
104
|
+
// Skip admin routes from telemetry
|
|
105
|
+
if (req.url && req.url.startsWith('/api/admin/')) {
|
|
106
|
+
return original_process(req, res);
|
|
107
|
+
}
|
|
108
|
+
if (req.url && req.url.startsWith('/admin')) {
|
|
109
|
+
return original_process(req, res);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ... instrumentation continues
|
|
113
|
+
};
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Route Registration Tracking
|
|
119
|
+
|
|
120
|
+
### Interception Point
|
|
121
|
+
|
|
122
|
+
The router's `set_route` method is wrapped to maintain a list of all registered routes:
|
|
123
|
+
|
|
124
|
+
```javascript
|
|
125
|
+
track_route_registration(server) {
|
|
126
|
+
const router = server.router;
|
|
127
|
+
if (!router || !router.set_route) return;
|
|
128
|
+
|
|
129
|
+
this._routes = [];
|
|
130
|
+
const original_set_route = router.set_route.bind(router);
|
|
131
|
+
|
|
132
|
+
router.set_route = (path, responder_or_handler, handler) => {
|
|
133
|
+
// Determine the handler type
|
|
134
|
+
const route_info = {
|
|
135
|
+
path: path,
|
|
136
|
+
type: this._categorize_route(path, responder_or_handler),
|
|
137
|
+
handler_name: this._get_handler_name(responder_or_handler, handler),
|
|
138
|
+
registered_at: Date.now()
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
this._routes.push(route_info);
|
|
142
|
+
|
|
143
|
+
return original_set_route(path, responder_or_handler, handler);
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
_categorize_route(path, handler) {
|
|
148
|
+
if (path.startsWith('/api/admin')) return 'admin';
|
|
149
|
+
if (path.startsWith('/api/')) return 'api';
|
|
150
|
+
if (path === '/admin') return 'admin';
|
|
151
|
+
|
|
152
|
+
// Check handler type
|
|
153
|
+
const handler_name = handler?.constructor?.name || '';
|
|
154
|
+
if (handler_name.includes('Webpage')) return 'webpage';
|
|
155
|
+
if (handler_name.includes('Function')) return 'api';
|
|
156
|
+
if (handler_name.includes('Observable')) return 'observable';
|
|
157
|
+
if (handler_name.includes('SSE')) return 'sse';
|
|
158
|
+
if (handler_name.includes('CSS')) return 'static';
|
|
159
|
+
if (handler_name.includes('JS')) return 'static';
|
|
160
|
+
|
|
161
|
+
return 'route';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
_get_handler_name(responder, handler) {
|
|
165
|
+
if (handler && typeof handler === 'function') {
|
|
166
|
+
return handler.name || 'anonymous';
|
|
167
|
+
}
|
|
168
|
+
if (responder && responder.constructor) {
|
|
169
|
+
return responder.constructor.name;
|
|
170
|
+
}
|
|
171
|
+
return 'unknown';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
get_routes_list() {
|
|
175
|
+
return this._routes.map(r => ({
|
|
176
|
+
path: r.path,
|
|
177
|
+
type: r.type,
|
|
178
|
+
handler: r.handler_name,
|
|
179
|
+
method: this._infer_method(r.type)
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
_infer_method(type) {
|
|
184
|
+
switch (type) {
|
|
185
|
+
case 'api': return 'POST';
|
|
186
|
+
case 'observable': return 'GET';
|
|
187
|
+
case 'sse': return 'GET';
|
|
188
|
+
case 'static': return 'GET';
|
|
189
|
+
case 'webpage': return 'GET';
|
|
190
|
+
default: return 'ANY';
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Build & Bundle Telemetry
|
|
198
|
+
|
|
199
|
+
### Interception Point
|
|
200
|
+
|
|
201
|
+
Build data is captured when `HTTP_Webpage_Publisher` instances emit their `ready` event:
|
|
202
|
+
|
|
203
|
+
```javascript
|
|
204
|
+
capture_bundle_info(server) {
|
|
205
|
+
this._build_info = null;
|
|
206
|
+
const publishers = server._publishers || [];
|
|
207
|
+
|
|
208
|
+
publishers.forEach(pub => {
|
|
209
|
+
// Check if it's a webpage publisher with bundling
|
|
210
|
+
if (pub.js_output_path !== undefined || pub.css_output_path !== undefined) {
|
|
211
|
+
const capture = () => {
|
|
212
|
+
const info = {
|
|
213
|
+
publisher_name: pub.name || pub.constructor.name || 'default',
|
|
214
|
+
items: [],
|
|
215
|
+
built_at: Date.now(),
|
|
216
|
+
entry_point: pub.src_path_client_js || null,
|
|
217
|
+
build_path: pub.build_path || null,
|
|
218
|
+
source_maps: !!pub.source_maps
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// JS bundle
|
|
222
|
+
if (pub.js_output_path && fs.existsSync(pub.js_output_path)) {
|
|
223
|
+
const stat = fs.statSync(pub.js_output_path);
|
|
224
|
+
let gzip_size = 0;
|
|
225
|
+
try {
|
|
226
|
+
const buf = fs.readFileSync(pub.js_output_path);
|
|
227
|
+
gzip_size = zlib.gzipSync(buf).length;
|
|
228
|
+
} catch (e) { /* non-critical */ }
|
|
229
|
+
|
|
230
|
+
info.items.push({
|
|
231
|
+
type: 'js',
|
|
232
|
+
path: pub.js_output_path,
|
|
233
|
+
filename: path.basename(pub.js_output_path),
|
|
234
|
+
size_identity: stat.size,
|
|
235
|
+
size_gzip: gzip_size,
|
|
236
|
+
compression_ratio: gzip_size > 0
|
|
237
|
+
? (gzip_size / stat.size * 100).toFixed(1) : '0'
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// CSS bundle
|
|
242
|
+
if (pub.css_output_path && fs.existsSync(pub.css_output_path)) {
|
|
243
|
+
const stat = fs.statSync(pub.css_output_path);
|
|
244
|
+
let gzip_size = 0;
|
|
245
|
+
try {
|
|
246
|
+
const buf = fs.readFileSync(pub.css_output_path);
|
|
247
|
+
gzip_size = zlib.gzipSync(buf).length;
|
|
248
|
+
} catch (e) { /* non-critical */ }
|
|
249
|
+
|
|
250
|
+
info.items.push({
|
|
251
|
+
type: 'css',
|
|
252
|
+
path: pub.css_output_path,
|
|
253
|
+
filename: path.basename(pub.css_output_path),
|
|
254
|
+
size_identity: stat.size,
|
|
255
|
+
size_gzip: gzip_size,
|
|
256
|
+
compression_ratio: gzip_size > 0
|
|
257
|
+
? (gzip_size / stat.size * 100).toFixed(1) : '0'
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
this._build_info = info;
|
|
262
|
+
|
|
263
|
+
// Broadcast build event
|
|
264
|
+
this._broadcast('build_complete', info);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// Listen for ready
|
|
268
|
+
if (typeof pub.on === 'function') {
|
|
269
|
+
pub.on('ready', capture);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// If already ready, capture now
|
|
273
|
+
if (pub._ready) {
|
|
274
|
+
capture();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## Resource Pool Event Forwarding
|
|
284
|
+
|
|
285
|
+
The `Server_Resource_Pool` already emits events (`resource_state_change`, `crashed`, `unhealthy`, `unreachable`, `recovered`). The adapter subscribes and forwards to the SSE channel:
|
|
286
|
+
|
|
287
|
+
```javascript
|
|
288
|
+
subscribe_resource_events(server) {
|
|
289
|
+
const pool = server.resource_pool;
|
|
290
|
+
if (!pool) return;
|
|
291
|
+
|
|
292
|
+
const events_to_forward = [
|
|
293
|
+
'resource_state_change',
|
|
294
|
+
'crashed',
|
|
295
|
+
'unhealthy',
|
|
296
|
+
'unreachable',
|
|
297
|
+
'recovered',
|
|
298
|
+
'removed'
|
|
299
|
+
];
|
|
300
|
+
|
|
301
|
+
events_to_forward.forEach(event_name => {
|
|
302
|
+
pool.on(event_name, (data) => {
|
|
303
|
+
this._broadcast(event_name, {
|
|
304
|
+
event: event_name,
|
|
305
|
+
resourceName: data.name || data.resourceName || 'unknown',
|
|
306
|
+
from: data.from || null,
|
|
307
|
+
to: data.to || data.state || null,
|
|
308
|
+
timestamp: Date.now(),
|
|
309
|
+
details: data
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Process Resource Event Forwarding
|
|
319
|
+
|
|
320
|
+
Individual `Process_Resource` instances emit stdout, stderr, and lifecycle events. These are forwarded for the Process Panel and Log Viewer:
|
|
321
|
+
|
|
322
|
+
```javascript
|
|
323
|
+
subscribe_process_events(server) {
|
|
324
|
+
const pool = server.resource_pool;
|
|
325
|
+
if (!pool || !pool.resources) return;
|
|
326
|
+
|
|
327
|
+
pool.resources.forEach((resource, name) => {
|
|
328
|
+
if (resource.type === 'process' || resource.constructor.name === 'Process_Resource') {
|
|
329
|
+
// State changes
|
|
330
|
+
resource.on('state_change', (data) => {
|
|
331
|
+
this._broadcast('process_state_change', {
|
|
332
|
+
name: name,
|
|
333
|
+
pid: resource.pid,
|
|
334
|
+
from: data.from,
|
|
335
|
+
to: data.to,
|
|
336
|
+
timestamp: Date.now()
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// Health checks
|
|
341
|
+
resource.on('health_check', (data) => {
|
|
342
|
+
this._broadcast('process_health', {
|
|
343
|
+
name: name,
|
|
344
|
+
pid: resource.pid,
|
|
345
|
+
healthy: data.healthy,
|
|
346
|
+
memory_usage: data.memory_usage || null,
|
|
347
|
+
timestamp: Date.now()
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// Also watch for newly added resources
|
|
354
|
+
pool.on('resource_added', (data) => {
|
|
355
|
+
// Re-subscribe to new process resources
|
|
356
|
+
const resource = pool.resources.get(data.name);
|
|
357
|
+
if (resource && (resource.type === 'process' || resource.constructor.name === 'Process_Resource')) {
|
|
358
|
+
resource.on('state_change', (state_data) => {
|
|
359
|
+
this._broadcast('process_state_change', {
|
|
360
|
+
name: data.name,
|
|
361
|
+
pid: resource.pid,
|
|
362
|
+
from: state_data.from,
|
|
363
|
+
to: state_data.to,
|
|
364
|
+
timestamp: Date.now()
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## Heartbeat Emission
|
|
375
|
+
|
|
376
|
+
A periodic heartbeat event provides status bar data without requiring the client to poll:
|
|
377
|
+
|
|
378
|
+
```javascript
|
|
379
|
+
start_heartbeat(server) {
|
|
380
|
+
this._heartbeat_interval = setInterval(() => {
|
|
381
|
+
const pool = server.resource_pool;
|
|
382
|
+
const pool_summary = pool ? pool.summary : { total: 0, running: 0 };
|
|
383
|
+
|
|
384
|
+
this._broadcast('heartbeat', {
|
|
385
|
+
uptime: Math.floor(process.uptime()),
|
|
386
|
+
pid: process.pid,
|
|
387
|
+
memory: process.memoryUsage(),
|
|
388
|
+
request_count: this._request_count || 0,
|
|
389
|
+
requests_per_minute: this.get_requests_per_minute(),
|
|
390
|
+
pool_summary: pool_summary,
|
|
391
|
+
route_count: this._routes ? this._routes.length : 0,
|
|
392
|
+
timestamp: Date.now()
|
|
393
|
+
});
|
|
394
|
+
}, 5000); // Every 5 seconds
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Aggregate Status Endpoint
|
|
401
|
+
|
|
402
|
+
`GET /api/admin/v1/status` returns a comprehensive snapshot that the dashboard uses for initial population:
|
|
403
|
+
|
|
404
|
+
```javascript
|
|
405
|
+
get_status(server) {
|
|
406
|
+
const mem = process.memoryUsage();
|
|
407
|
+
const pool = server.resource_pool;
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
process: {
|
|
411
|
+
pid: process.pid,
|
|
412
|
+
title: process.title,
|
|
413
|
+
uptime: Math.floor(process.uptime()),
|
|
414
|
+
memory: {
|
|
415
|
+
rss: mem.rss,
|
|
416
|
+
heap_used: mem.heapUsed,
|
|
417
|
+
heap_total: mem.heapTotal,
|
|
418
|
+
external: mem.external
|
|
419
|
+
},
|
|
420
|
+
node_version: process.version,
|
|
421
|
+
platform: process.platform,
|
|
422
|
+
arch: process.arch,
|
|
423
|
+
cwd: process.cwd()
|
|
424
|
+
},
|
|
425
|
+
server: {
|
|
426
|
+
port: server.port || null,
|
|
427
|
+
host: server.host || '0.0.0.0'
|
|
428
|
+
},
|
|
429
|
+
telemetry: {
|
|
430
|
+
request_count: this._request_count || 0,
|
|
431
|
+
requests_per_minute: this.get_requests_per_minute(),
|
|
432
|
+
status_counts: this._status_counts || {}
|
|
433
|
+
},
|
|
434
|
+
pool: pool ? pool.summary : { total: 0, running: 0, stopped: 0 },
|
|
435
|
+
routes: {
|
|
436
|
+
total: this._routes ? this._routes.length : 0
|
|
437
|
+
},
|
|
438
|
+
build: this._build_info || null
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
---
|
|
444
|
+
|
|
445
|
+
## Initialization Sequence
|
|
446
|
+
|
|
447
|
+
The instrumentation must be applied in the correct order, after the server's core systems are initialized but before it starts accepting requests:
|
|
448
|
+
|
|
449
|
+
```javascript
|
|
450
|
+
class Admin_Module_V1 {
|
|
451
|
+
init(server) {
|
|
452
|
+
// 1. Track route registration FIRST (before other modules register routes)
|
|
453
|
+
this.track_route_registration(server);
|
|
454
|
+
|
|
455
|
+
// 2. Register admin API endpoints (these are tracked by step 1)
|
|
456
|
+
this._register_endpoints(server);
|
|
457
|
+
|
|
458
|
+
// 3. Instrument request handler (after routes exist)
|
|
459
|
+
this.instrument_request_handler(server);
|
|
460
|
+
|
|
461
|
+
// 4. Subscribe to resource pool events
|
|
462
|
+
this.subscribe_resource_events(server);
|
|
463
|
+
|
|
464
|
+
// 5. Subscribe to process resource events
|
|
465
|
+
this.subscribe_process_events(server);
|
|
466
|
+
|
|
467
|
+
// 6. Capture build info (listens for publisher ready)
|
|
468
|
+
this.capture_bundle_info(server);
|
|
469
|
+
|
|
470
|
+
// 7. Start SSE channel
|
|
471
|
+
this._init_sse_channel(server);
|
|
472
|
+
|
|
473
|
+
// 8. Start heartbeat
|
|
474
|
+
this.start_heartbeat(server);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
_register_endpoints(server) {
|
|
478
|
+
const fn_publisher = server.fn_publisher || new HTTP_Function_Publisher({ context: server.context });
|
|
479
|
+
|
|
480
|
+
// GET endpoints
|
|
481
|
+
fn_publisher.publish('/api/admin/v1/status', () => this.get_status(server));
|
|
482
|
+
fn_publisher.publish('/api/admin/v1/resources', () => this.get_resources_tree(server));
|
|
483
|
+
fn_publisher.publish('/api/admin/v1/routes', () => this.get_routes_list());
|
|
484
|
+
fn_publisher.publish('/api/admin/v1/build', () => this._build_info || {});
|
|
485
|
+
fn_publisher.publish('/api/admin/v1/config', () => this.get_config(server));
|
|
486
|
+
|
|
487
|
+
// POST endpoint for config changes
|
|
488
|
+
fn_publisher.publish('/api/admin/v1/config/set', (body) => this.set_config(body));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
_init_sse_channel(server) {
|
|
492
|
+
this._sse_publisher = new HTTP_SSE_Publisher({
|
|
493
|
+
context: server.context,
|
|
494
|
+
history_size: 100
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
server.router.set_route('/api/admin/v1/events', this._sse_publisher);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
_broadcast(event_name, data) {
|
|
501
|
+
if (this._sse_publisher) {
|
|
502
|
+
this._sse_publisher.broadcast(event_name, data);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
_broadcast_request(data) {
|
|
507
|
+
this._broadcast('request', data);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
destroy() {
|
|
511
|
+
if (this._heartbeat_interval) {
|
|
512
|
+
clearInterval(this._heartbeat_interval);
|
|
513
|
+
}
|
|
514
|
+
if (this._sse_publisher) {
|
|
515
|
+
// Close all SSE connections
|
|
516
|
+
this._sse_publisher.close_all && this._sse_publisher.close_all();
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## Impact Assessment
|
|
525
|
+
|
|
526
|
+
### Performance Impact
|
|
527
|
+
|
|
528
|
+
| Instrumentation | Overhead per Request | Notes |
|
|
529
|
+
|-----------------|---------------------|-------|
|
|
530
|
+
| Request wrapping | ~0.1ms | One Date.now() call + array push |
|
|
531
|
+
| Route tracking | None at runtime | Only runs during registration |
|
|
532
|
+
| Build capture | None at runtime | Only runs on publisher ready |
|
|
533
|
+
| Resource events | None per request | Event-driven, no polling |
|
|
534
|
+
| Heartbeat | ~1ms every 5s | process.memoryUsage() is the main cost |
|
|
535
|
+
| SSE broadcast | ~0.5ms per event | JSON serialization + write to clients |
|
|
536
|
+
|
|
537
|
+
Total overhead: **< 1ms per request** for the common case.
|
|
538
|
+
|
|
539
|
+
### Memory Impact
|
|
540
|
+
|
|
541
|
+
| Data Structure | Max Size | Notes |
|
|
542
|
+
|---------------|----------|-------|
|
|
543
|
+
| Request window | ~60K entries max | 1-minute window, trimmed continuously |
|
|
544
|
+
| Route list | ~100 entries | Static after initialization |
|
|
545
|
+
| Build info | 1 object | Overwritten on each build |
|
|
546
|
+
| SSE history | 100 events | Configured via `history_size` |
|
|
547
|
+
| Status counts | ~10 entries | One per status code |
|
|
548
|
+
|
|
549
|
+
Total: **< 1 MB** additional memory for typical workloads.
|
|
550
|
+
|
|
551
|
+
### Safety
|
|
552
|
+
|
|
553
|
+
- **No core modifications**: All instrumentation wraps existing methods; originals are preserved
|
|
554
|
+
- **Admin routes excluded**: The admin panel's own requests don't inflate telemetry
|
|
555
|
+
- **Graceful degradation**: If any instrumentation fails to attach (missing router, no pool), the admin panel shows "–" instead of crashing
|
|
556
|
+
- **Cleanup**: `destroy()` clears intervals and closes SSE connections
|