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,485 @@
|
|
|
1
|
+
# Chapter 14: Real-Time Updates — SSE & Observable Integration
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The Admin UI uses **Server-Sent Events (SSE)** as its primary real-time channel. jsgui3-server provides two publisher types that support SSE:
|
|
6
|
+
|
|
7
|
+
1. **`HTTP_SSE_Publisher`** — Multi-client broadcast channel with event history
|
|
8
|
+
2. **`HTTP_Observable_Publisher`** — Streams an observable's emissions to SSE clients
|
|
9
|
+
|
|
10
|
+
The Admin UI uses `HTTP_SSE_Publisher` for the admin event channel (`/api/admin/v1/events`), with the adapter broadcasting events from various server subsystems into this single channel.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## SSE Architecture
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
18
|
+
│ Server │
|
|
19
|
+
│ │
|
|
20
|
+
│ ┌─────────────────┐ ┌─────────────────┐ ┌────────────────┐ │
|
|
21
|
+
│ │ Request Handler │ │ Resource Pool │ │ Build System │ │
|
|
22
|
+
│ │ (instrumented) │ │ (events) │ │ (ready event) │ │
|
|
23
|
+
│ └────────┬────────┘ └────────┬────────┘ └───────┬────────┘ │
|
|
24
|
+
│ │ │ │ │
|
|
25
|
+
│ ▼ ▼ ▼ │
|
|
26
|
+
│ ┌────────────────────────────────────────────────────────────┐ │
|
|
27
|
+
│ │ Admin_Module_V1._broadcast() │ │
|
|
28
|
+
│ │ │ │
|
|
29
|
+
│ │ Normalizes event data → JSON → SSE_Publisher.broadcast() │ │
|
|
30
|
+
│ └──────────────────────────┬─────────────────────────────────┘ │
|
|
31
|
+
│ │ │
|
|
32
|
+
│ ┌──────────────────────────▼─────────────────────────────────┐ │
|
|
33
|
+
│ │ HTTP_SSE_Publisher │ │
|
|
34
|
+
│ │ /api/admin/v1/events │ │
|
|
35
|
+
│ │ │ │
|
|
36
|
+
│ │ clients: Map<id, {res, connected_at}> │ │
|
|
37
|
+
│ │ event_history: Array (last 100) │ │
|
|
38
|
+
│ └──────┬────────────┬────────────┬───────────────────────────┘ │
|
|
39
|
+
│ │ │ │ │
|
|
40
|
+
└─────────┼────────────┼────────────┼──────────────────────────────┘
|
|
41
|
+
│ │ │
|
|
42
|
+
▼ ▼ ▼
|
|
43
|
+
Browser 1 Browser 2 Browser 3
|
|
44
|
+
EventSource EventSource EventSource
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Event Types
|
|
50
|
+
|
|
51
|
+
The SSE channel emits the following event types:
|
|
52
|
+
|
|
53
|
+
| Event Name | Source | Frequency | Data Shape |
|
|
54
|
+
|------------|--------|-----------|------------|
|
|
55
|
+
| `heartbeat` | Timer (5s) | Periodic | `{ uptime, pid, memory, request_count, requests_per_minute, pool_summary }` |
|
|
56
|
+
| `request` | Request handler | Per-request | `{ method, url, status, duration_ms, timestamp }` |
|
|
57
|
+
| `resource_state_change` | Resource pool | On change | `{ resourceName, from, to, timestamp }` |
|
|
58
|
+
| `crashed` | Resource pool | On crash | `{ resourceName, code, signal, timestamp }` |
|
|
59
|
+
| `unhealthy` | Resource pool | On unhealthy | `{ resourceName, timestamp, details }` |
|
|
60
|
+
| `recovered` | Resource pool | On recovery | `{ resourceName, timestamp }` |
|
|
61
|
+
| `process_state_change` | Process resources | On change | `{ name, pid, from, to, timestamp }` |
|
|
62
|
+
| `process_health` | Process resources | On check | `{ name, pid, healthy, memory_usage, timestamp }` |
|
|
63
|
+
| `build_complete` | Publisher | On build | `{ items: [{type, size_identity, size_gzip}], built_at }` |
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Client-Side SSE Connection
|
|
68
|
+
|
|
69
|
+
### EventSource Setup
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
// In admin-ui/v1/utils/sse_client.js
|
|
73
|
+
|
|
74
|
+
class SSE_Client {
|
|
75
|
+
constructor(url, options = {}) {
|
|
76
|
+
this._url = url;
|
|
77
|
+
this._handlers = {};
|
|
78
|
+
this._reconnect_delay = options.reconnect_delay || 3000;
|
|
79
|
+
this._max_reconnect_delay = options.max_reconnect_delay || 30000;
|
|
80
|
+
this._current_delay = this._reconnect_delay;
|
|
81
|
+
this._connected = false;
|
|
82
|
+
this._event_source = null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
connect() {
|
|
86
|
+
if (this._event_source) {
|
|
87
|
+
this._event_source.close();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this._event_source = new EventSource(this._url);
|
|
91
|
+
|
|
92
|
+
this._event_source.addEventListener('open', () => {
|
|
93
|
+
this._connected = true;
|
|
94
|
+
this._current_delay = this._reconnect_delay; // Reset backoff
|
|
95
|
+
this._emit_internal('connected');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
this._event_source.addEventListener('error', () => {
|
|
99
|
+
this._connected = false;
|
|
100
|
+
this._emit_internal('disconnected');
|
|
101
|
+
// EventSource handles reconnection automatically,
|
|
102
|
+
// but we track state for the UI
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Register all event handlers
|
|
106
|
+
for (const event_name of Object.keys(this._handlers)) {
|
|
107
|
+
if (event_name.startsWith('_')) continue; // Skip internal events
|
|
108
|
+
this._event_source.addEventListener(event_name, (e) => {
|
|
109
|
+
try {
|
|
110
|
+
const data = JSON.parse(e.data);
|
|
111
|
+
this._handlers[event_name].forEach(fn => fn(data));
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.warn(`SSE parse error for ${event_name}:`, err);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
on(event_name, handler) {
|
|
120
|
+
if (!this._handlers[event_name]) {
|
|
121
|
+
this._handlers[event_name] = [];
|
|
122
|
+
|
|
123
|
+
// If already connected, register the listener dynamically
|
|
124
|
+
if (this._event_source) {
|
|
125
|
+
this._event_source.addEventListener(event_name, (e) => {
|
|
126
|
+
try {
|
|
127
|
+
const data = JSON.parse(e.data);
|
|
128
|
+
this._handlers[event_name].forEach(fn => fn(data));
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.warn(`SSE parse error for ${event_name}:`, err);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
this._handlers[event_name].push(handler);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
off(event_name, handler) {
|
|
139
|
+
if (this._handlers[event_name]) {
|
|
140
|
+
this._handlers[event_name] = this._handlers[event_name].filter(fn => fn !== handler);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
_emit_internal(event_name) {
|
|
145
|
+
const key = `_${event_name}`;
|
|
146
|
+
if (this._handlers[key]) {
|
|
147
|
+
this._handlers[key].forEach(fn => fn());
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
on_connected(handler) {
|
|
152
|
+
this.on('_connected', handler);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
on_disconnected(handler) {
|
|
156
|
+
this.on('_disconnected', handler);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
get connected() {
|
|
160
|
+
return this._connected;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
close() {
|
|
164
|
+
if (this._event_source) {
|
|
165
|
+
this._event_source.close();
|
|
166
|
+
this._event_source = null;
|
|
167
|
+
}
|
|
168
|
+
this._connected = false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = SSE_Client;
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Integration with Admin_Shell
|
|
176
|
+
|
|
177
|
+
```javascript
|
|
178
|
+
// In Admin_Shell.activate()
|
|
179
|
+
_connect_sse() {
|
|
180
|
+
const sse = new SSE_Client('/api/admin/v1/events');
|
|
181
|
+
|
|
182
|
+
// Connection state
|
|
183
|
+
sse.on_connected(() => this._set_connection_status('connected'));
|
|
184
|
+
sse.on_disconnected(() => this._set_connection_status('disconnected'));
|
|
185
|
+
|
|
186
|
+
// Event routing
|
|
187
|
+
sse.on('heartbeat', (data) => this._on_heartbeat(data));
|
|
188
|
+
sse.on('request', (data) => this._on_request_event(data));
|
|
189
|
+
sse.on('resource_state_change', (data) => this._on_resource_event(data));
|
|
190
|
+
sse.on('crashed', (data) => this._on_resource_event(data));
|
|
191
|
+
sse.on('unhealthy', (data) => this._on_resource_event(data));
|
|
192
|
+
sse.on('recovered', (data) => this._on_resource_event(data));
|
|
193
|
+
sse.on('process_state_change', (data) => this._on_process_event(data));
|
|
194
|
+
sse.on('build_complete', (data) => this._on_build_event(data));
|
|
195
|
+
|
|
196
|
+
sse.connect();
|
|
197
|
+
this._sse = sse;
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Event Routing to Views
|
|
204
|
+
|
|
205
|
+
The Admin_Shell routes SSE events to whichever view is currently active:
|
|
206
|
+
|
|
207
|
+
```javascript
|
|
208
|
+
_on_heartbeat(data) {
|
|
209
|
+
// Status bar — always updated
|
|
210
|
+
this._update_status_bar(data);
|
|
211
|
+
|
|
212
|
+
// Dashboard stat cards — updated if dashboard is visible
|
|
213
|
+
if (this._current_view === 'dashboard' && this._dashboard_controls) {
|
|
214
|
+
const dc = this._dashboard_controls;
|
|
215
|
+
|
|
216
|
+
dc.uptime_card.update({
|
|
217
|
+
value: format_uptime(data.uptime),
|
|
218
|
+
detail: format_bytes(data.memory?.rss || 0)
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
dc.rps_card.update({
|
|
222
|
+
value: data.requests_per_minute || 0,
|
|
223
|
+
detail: `${data.request_count || 0} total`
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
if (data.pool_summary) {
|
|
227
|
+
dc.pool_card.update({
|
|
228
|
+
value: `${data.pool_summary.running || 0}/${data.pool_summary.total || 0}`,
|
|
229
|
+
detail: 'running'
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
dc.routes_card.update({
|
|
234
|
+
value: data.route_count || 0,
|
|
235
|
+
detail: 'registered'
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
_on_request_event(data) {
|
|
241
|
+
// Log viewer — always receives events (for any active log viewer)
|
|
242
|
+
if (this._current_view === 'dashboard' && this._dashboard_controls?.activity_log) {
|
|
243
|
+
this._dashboard_controls.activity_log.append_request(data);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (this._current_view === 'logs' && this._logs_viewer) {
|
|
247
|
+
this._logs_viewer.append_request(data);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
_on_resource_event(data) {
|
|
252
|
+
// Resource table — update if visible
|
|
253
|
+
if (this._current_view === 'dashboard' && this._dashboard_controls?.resource_table) {
|
|
254
|
+
this._dashboard_controls.resource_table.refresh();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (this._current_view === 'resources' && this._resources_table) {
|
|
258
|
+
this._resources_table.refresh();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Log viewer — resource events are also logged
|
|
262
|
+
if (this._current_view === 'dashboard' && this._dashboard_controls?.activity_log) {
|
|
263
|
+
this._dashboard_controls.activity_log.append_resource_event(data);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (this._current_view === 'logs' && this._logs_viewer) {
|
|
267
|
+
this._logs_viewer.append_resource_event(data);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Status view — update health indicator
|
|
271
|
+
if (this._current_view === 'status') {
|
|
272
|
+
this._update_health_status(data);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
_on_process_event(data) {
|
|
277
|
+
if (this._current_view === 'dashboard' && this._dashboard_controls?.process_panel) {
|
|
278
|
+
this._dashboard_controls.process_panel.refresh();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (this._current_view === 'processes' && this._process_panel) {
|
|
282
|
+
this._process_panel.refresh();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
_on_build_event(data) {
|
|
287
|
+
if (this._current_view === 'dashboard' && this._dashboard_controls?.build_status) {
|
|
288
|
+
this._dashboard_controls.build_status.update(data);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (this._current_view === 'build' && this._build_status) {
|
|
292
|
+
this._build_status.update(data);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Log the build event
|
|
296
|
+
if (this._current_view === 'dashboard' && this._dashboard_controls?.activity_log) {
|
|
297
|
+
this._dashboard_controls.activity_log.append_build_event(data);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## Server-Side: HTTP_SSE_Publisher Usage
|
|
305
|
+
|
|
306
|
+
### Broadcast Method
|
|
307
|
+
|
|
308
|
+
The `HTTP_SSE_Publisher` from jsgui3-server provides a `broadcast(event_name, data)` method that sends to all connected clients:
|
|
309
|
+
|
|
310
|
+
```javascript
|
|
311
|
+
// From publishers/http-sse-publisher.js
|
|
312
|
+
// The publisher handles:
|
|
313
|
+
// - Client connection registration
|
|
314
|
+
// - Client disconnection cleanup
|
|
315
|
+
// - Event history for replay on reconnect
|
|
316
|
+
// - JSON serialization of data
|
|
317
|
+
// - SSE format: "event: name\ndata: json\n\n"
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Client Tracking
|
|
321
|
+
|
|
322
|
+
The publisher maintains a `clients` Map:
|
|
323
|
+
```javascript
|
|
324
|
+
{
|
|
325
|
+
client_id: {
|
|
326
|
+
res: http.ServerResponse,
|
|
327
|
+
connected_at: timestamp
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
The admin adapter can query this to show active SSE connections:
|
|
333
|
+
```javascript
|
|
334
|
+
get_sse_client_count() {
|
|
335
|
+
if (this._sse_publisher && this._sse_publisher.clients) {
|
|
336
|
+
return this._sse_publisher.clients.size;
|
|
337
|
+
}
|
|
338
|
+
return 0;
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## Event History & Replay
|
|
345
|
+
|
|
346
|
+
When a client reconnects (browser refresh, network hiccup), it receives the last N events from the history buffer. This prevents the dashboard from showing empty data after a reconnect.
|
|
347
|
+
|
|
348
|
+
The `HTTP_SSE_Publisher` stores up to `history_size` events (configured as 100 in the admin channel). On reconnect, the client's `Last-Event-ID` header is used to determine which events to replay:
|
|
349
|
+
|
|
350
|
+
```javascript
|
|
351
|
+
// Server behavior on client connect:
|
|
352
|
+
// 1. Send any events after Last-Event-ID
|
|
353
|
+
// 2. Each broadcast includes an auto-incrementing ID
|
|
354
|
+
// 3. Client EventSource automatically sends Last-Event-ID on reconnect
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
This is handled by the publisher internally — the admin module doesn't need to implement replay logic.
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## Observable Publisher Integration
|
|
362
|
+
|
|
363
|
+
For existing `HTTP_Observable_Publisher` instances registered with the server, the admin UI can display their status:
|
|
364
|
+
|
|
365
|
+
```javascript
|
|
366
|
+
// Adapter: enumerate observable publishers
|
|
367
|
+
get_observables_status(server) {
|
|
368
|
+
const result = [];
|
|
369
|
+
const publishers = server._publishers || [];
|
|
370
|
+
|
|
371
|
+
publishers.forEach(pub => {
|
|
372
|
+
if (pub.constructor.name === 'HTTP_Observable_Publisher' ||
|
|
373
|
+
pub.active_sse_connections !== undefined) {
|
|
374
|
+
result.push({
|
|
375
|
+
name: pub.name || pub.path || 'unnamed',
|
|
376
|
+
path: pub.path || null,
|
|
377
|
+
active_connections: pub.active_sse_connections || 0,
|
|
378
|
+
paused: pub._paused || false,
|
|
379
|
+
total_emitted: pub._emission_count || 0
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
return result;
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
This information is displayed in the Resources view under a separate "Observable Streams" section.
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
## Throttling & Backpressure
|
|
393
|
+
|
|
394
|
+
### Server-Side Throttling
|
|
395
|
+
|
|
396
|
+
High-frequency events (requests under load) could overwhelm SSE clients. The adapter applies event-level throttling:
|
|
397
|
+
|
|
398
|
+
```javascript
|
|
399
|
+
// In Admin_Module_V1
|
|
400
|
+
_broadcast_request(data) {
|
|
401
|
+
// Throttle request events to max 10 per second
|
|
402
|
+
const now = Date.now();
|
|
403
|
+
if (!this._last_request_broadcast) {
|
|
404
|
+
this._last_request_broadcast = now;
|
|
405
|
+
this._request_broadcast_count = 0;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (now - this._last_request_broadcast > 1000) {
|
|
409
|
+
// Reset window
|
|
410
|
+
this._last_request_broadcast = now;
|
|
411
|
+
this._request_broadcast_count = 0;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
this._request_broadcast_count++;
|
|
415
|
+
if (this._request_broadcast_count <= 10) {
|
|
416
|
+
this._broadcast('request', data);
|
|
417
|
+
}
|
|
418
|
+
// Requests beyond the limit are still counted in telemetry
|
|
419
|
+
// but not broadcast individually
|
|
420
|
+
}
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### Client-Side Batching
|
|
424
|
+
|
|
425
|
+
As described in Chapter 9, the Log_Viewer uses `requestAnimationFrame` to batch DOM updates when events arrive rapidly:
|
|
426
|
+
|
|
427
|
+
```javascript
|
|
428
|
+
// In Log_Viewer — batched rendering
|
|
429
|
+
_throttled_append(entry) {
|
|
430
|
+
this._pending_entries.push(entry);
|
|
431
|
+
if (!this._flush_scheduled) {
|
|
432
|
+
this._flush_scheduled = true;
|
|
433
|
+
requestAnimationFrame(() => {
|
|
434
|
+
this._pending_entries.forEach(e => this._render_entry(e));
|
|
435
|
+
this._pending_entries = [];
|
|
436
|
+
this._flush_scheduled = false;
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
## Connection Resilience
|
|
445
|
+
|
|
446
|
+
### Automatic Reconnection
|
|
447
|
+
|
|
448
|
+
The browser's `EventSource` API automatically reconnects on connection loss. The SSE_Client wrapper tracks state and updates the UI accordingly:
|
|
449
|
+
|
|
450
|
+
```
|
|
451
|
+
Connected ──── Connection lost ──── Reconnecting ──── Connected
|
|
452
|
+
● ○ ◐ ●
|
|
453
|
+
green red amber green
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### Stale Data Handling
|
|
457
|
+
|
|
458
|
+
After reconnection, the dashboard fetches a fresh snapshot via `GET /api/admin/v1/status` to ensure stat cards show current values, not stale data from before the disconnection:
|
|
459
|
+
|
|
460
|
+
```javascript
|
|
461
|
+
// In SSE_Client
|
|
462
|
+
sse.on_connected(() => {
|
|
463
|
+
this._set_connection_status('connected');
|
|
464
|
+
// Refresh all snapshot data
|
|
465
|
+
this._fetch_initial_data();
|
|
466
|
+
});
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
---
|
|
470
|
+
|
|
471
|
+
## SSE vs WebSocket — Why SSE
|
|
472
|
+
|
|
473
|
+
jsgui3-server already has SSE infrastructure (`HTTP_SSE_Publisher`, `HTTP_Observable_Publisher`). WebSocket support would require:
|
|
474
|
+
- New dependency or custom implementation
|
|
475
|
+
- Different publisher type
|
|
476
|
+
- Client-side WebSocket handling
|
|
477
|
+
|
|
478
|
+
SSE is the right choice because:
|
|
479
|
+
1. **Already built** — No new infrastructure needed
|
|
480
|
+
2. **Unidirectional** — Admin telemetry flows server → client only
|
|
481
|
+
3. **Auto-reconnect** — Browser EventSource handles reconnection
|
|
482
|
+
4. **HTTP compatible** — Works through proxies and load balancers
|
|
483
|
+
5. **Event history** — The publisher already supports replay
|
|
484
|
+
|
|
485
|
+
The only case where WebSocket would be preferred is bidirectional communication (e.g., interactive terminal). For the Admin UI's Phase 1 read-only dashboard, SSE is sufficient and optimal.
|