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.
Files changed (154) hide show
  1. package/.github/agents/Mobile Developer.agent.md +89 -0
  2. package/.github/workflows/control-scan-manifest-check.yml +31 -0
  3. package/AGENTS.md +4 -0
  4. package/README.md +215 -3
  5. package/admin-ui/client.js +81 -51
  6. package/admin-ui/v1/admin_auth_service.js +197 -0
  7. package/admin-ui/v1/admin_user_store.js +71 -0
  8. package/admin-ui/v1/client.js +17 -0
  9. package/admin-ui/v1/controls/admin_shell.js +1399 -0
  10. package/admin-ui/v1/controls/group_box.js +84 -0
  11. package/admin-ui/v1/controls/stat_card.js +125 -0
  12. package/admin-ui/v1/server.js +658 -0
  13. package/admin-ui/v1/utils/formatters.js +68 -0
  14. package/dev-status.svg +139 -0
  15. package/docs/admin-extension-guide.md +345 -0
  16. package/docs/api-reference.md +301 -43
  17. package/docs/books/adaptive-control-improvements/01-control-candidate-matrix.md +122 -0
  18. package/docs/books/adaptive-control-improvements/02-tier-1-layout-playbooks.md +207 -0
  19. package/docs/books/adaptive-control-improvements/03-tier-2-navigation-form-overlay.md +140 -0
  20. package/docs/books/adaptive-control-improvements/04-cross-cutting-platform-functionality.md +141 -0
  21. package/docs/books/adaptive-control-improvements/05-styling-theming-density-upgrades.md +114 -0
  22. package/docs/books/adaptive-control-improvements/06-testing-quality-gates.md +97 -0
  23. package/docs/books/adaptive-control-improvements/07-delivery-roadmap-and-ownership.md +137 -0
  24. package/docs/books/adaptive-control-improvements/08-appendix-tier1-acceptance-and-pr-templates.md +261 -0
  25. package/docs/books/adaptive-control-improvements/README.md +66 -0
  26. package/docs/books/admin-ui-authentication/01-threat-model-and-goals.md +124 -0
  27. package/docs/books/admin-ui-authentication/02-session-model-and-token-model.md +75 -0
  28. package/docs/books/admin-ui-authentication/03-auth-middleware-patterns.md +77 -0
  29. package/docs/books/admin-ui-authentication/README.md +25 -0
  30. package/docs/books/creating-a-new-admin-ui/01-introduction-and-vision.md +130 -0
  31. package/docs/books/creating-a-new-admin-ui/02-architecture-and-data-flow.md +298 -0
  32. package/docs/books/creating-a-new-admin-ui/03-server-introspection.md +381 -0
  33. package/docs/books/creating-a-new-admin-ui/04-admin-module-adapter-layer.md +592 -0
  34. package/docs/books/creating-a-new-admin-ui/05-domain-controls-stat-cards-and-gauges.md +513 -0
  35. package/docs/books/creating-a-new-admin-ui/06-domain-controls-process-manager.md +544 -0
  36. package/docs/books/creating-a-new-admin-ui/07-domain-controls-resource-pool-inspector.md +493 -0
  37. package/docs/books/creating-a-new-admin-ui/08-domain-controls-route-table-and-api-explorer.md +586 -0
  38. package/docs/books/creating-a-new-admin-ui/09-domain-controls-log-viewer-and-activity-feed.md +490 -0
  39. package/docs/books/creating-a-new-admin-ui/10-domain-controls-build-status-and-bundle-inspector.md +526 -0
  40. package/docs/books/creating-a-new-admin-ui/11-domain-controls-configuration-panel.md +808 -0
  41. package/docs/books/creating-a-new-admin-ui/12-admin-shell-layout-sidebar-navigation.md +210 -0
  42. package/docs/books/creating-a-new-admin-ui/13-telemetry-integration.md +556 -0
  43. package/docs/books/creating-a-new-admin-ui/14-realtime-sse-observable-integration.md +485 -0
  44. package/docs/books/creating-a-new-admin-ui/15-styling-theming-aero-design-system.md +521 -0
  45. package/docs/books/creating-a-new-admin-ui/16-testing-and-quality-assurance.md +147 -0
  46. package/docs/books/creating-a-new-admin-ui/17-next-steps-process-resource-roadmap.md +356 -0
  47. package/docs/books/creating-a-new-admin-ui/README.md +68 -0
  48. package/docs/books/device-adaptive-composition/01-platform-feature-audit.md +177 -0
  49. package/docs/books/device-adaptive-composition/02-responsive-composition-model.md +187 -0
  50. package/docs/books/device-adaptive-composition/03-data-model-vs-view-model.md +231 -0
  51. package/docs/books/device-adaptive-composition/04-styling-theme-breakpoints.md +234 -0
  52. package/docs/books/device-adaptive-composition/05-showcase-app-multi-device-assessment.md +193 -0
  53. package/docs/books/device-adaptive-composition/06-implementation-patterns-and-apis.md +346 -0
  54. package/docs/books/device-adaptive-composition/07-testing-harness-and-quality-gates.md +265 -0
  55. package/docs/books/device-adaptive-composition/08-roadmap-and-adoption-plan.md +250 -0
  56. package/docs/books/device-adaptive-composition/README.md +47 -0
  57. package/docs/books/jsgui3-bundling-research-book/00-table-of-contents.md +35 -0
  58. package/docs/books/jsgui3-bundling-research-book/01-pipeline-and-runtime-semantics.md +34 -0
  59. package/docs/books/jsgui3-bundling-research-book/02-javascript-bundling-core.md +36 -0
  60. package/docs/books/jsgui3-bundling-research-book/03-style-extraction-and-css-compilation.md +35 -0
  61. package/docs/books/jsgui3-bundling-research-book/04-static-publishing-and-delivery.md +39 -0
  62. package/docs/books/jsgui3-bundling-research-book/05-current-limits-and-size-bloat-vectors.md +25 -0
  63. package/docs/books/jsgui3-bundling-research-book/06-unused-module-elimination-strategy.md +77 -0
  64. package/docs/books/jsgui3-bundling-research-book/07-jsgui3-html-control-and-mixin-pruning.md +63 -0
  65. package/docs/books/jsgui3-bundling-research-book/08-test-and-verification-methodology.md +43 -0
  66. package/docs/books/jsgui3-bundling-research-book/09-roadmap-and-rollout.md +42 -0
  67. package/docs/books/jsgui3-bundling-research-book/10-further-research-strategies-and-upgrades.md +211 -0
  68. package/docs/books/jsgui3-bundling-research-book/README.md +35 -0
  69. package/docs/bundling-system-deep-dive.md +9 -4
  70. package/docs/comparison-report-express-plex-cpanel.md +549 -0
  71. package/docs/comprehensive-documentation.md +49 -18
  72. package/docs/configuration-reference.md +152 -27
  73. package/docs/core/README.md +19 -0
  74. package/docs/core/jsgui3-server-core-book/00-table-of-contents.md +21 -0
  75. package/docs/core/jsgui3-server-core-book/01-startup-readiness-state-machine.md +41 -0
  76. package/docs/core/jsgui3-server-core-book/02-resource-abstraction-and-lifecycle.md +92 -0
  77. package/docs/core/jsgui3-server-core-book/03-resource-pool-and-event-topology.md +47 -0
  78. package/docs/core/jsgui3-server-core-book/04-sse-publisher-semantics.md +41 -0
  79. package/docs/core/jsgui3-server-core-book/05-serve-factory-resource-wiring.md +46 -0
  80. package/docs/core/jsgui3-server-core-book/06-e2e-testing-methodology.md +48 -0
  81. package/docs/core/jsgui3-server-core-book/07-defect-detection-and-hardening-loop.md +47 -0
  82. package/docs/designs/server-admin-interface-aero.svg +611 -0
  83. package/docs/publishers-guide.md +59 -4
  84. package/docs/resources-guide.md +184 -35
  85. package/docs/simple-server-api-design.md +72 -17
  86. package/docs/system-architecture.md +18 -14
  87. package/docs/troubleshooting.md +84 -53
  88. package/examples/controls/15) window, observable SSE/server.js +6 -1
  89. package/examples/controls/19) window, auto observable ui/server.js +9 -0
  90. package/examples/controls/20) window, task manager app/README.md +133 -0
  91. package/examples/controls/20) window, task manager app/client.js +797 -0
  92. package/examples/controls/20) window, task manager app/server.js +178 -0
  93. package/examples/controls/6) window, color_palette/client.js +165 -68
  94. package/examples/controls/9) window, date picker/client.js +362 -76
  95. package/examples/controls/9b) window, shared data.model mirrored date pickers/client.js +104 -83
  96. package/examples/jsgui3-html/06) theming/client.js +22 -1
  97. package/examples/jsgui3-html/10) binding-debugger/client.js +137 -1
  98. package/http/responders/static/Static_Route_HTTP_Responder.js +52 -34
  99. package/lab/experiments/capture-color-controls.js +196 -0
  100. package/lab/results/screenshots/color-controls/full_page.png +0 -0
  101. package/lab/results/screenshots/color-controls/section_1_color_grid_12x12.png +0 -0
  102. package/lab/results/screenshots/color-controls/section_2_color_grid_4x2.png +0 -0
  103. package/lab/results/screenshots/color-controls/section_3_color_palette.png +0 -0
  104. package/lab/results/screenshots/color-controls/section_4_palette_comparison.png +0 -0
  105. package/lab/results/screenshots/color-controls/section_5_raw_swatches.png +0 -0
  106. package/lab/results/screenshots/color-controls/section_6_optimized_crayola.png +0 -0
  107. package/lab/results/screenshots/color-controls/section_7_pastel_palette.png +0 -0
  108. package/lab/results/screenshots/color-controls/section_8_extended_144.png +0 -0
  109. package/lab/screenshot-utils.js +248 -0
  110. package/module.js +12 -0
  111. package/package.json +12 -2
  112. package/publishers/Publishers.js +4 -3
  113. package/publishers/helpers/assigners/static-compressed-response-buffers/Single_Control_Webpage_Server_Static_Compressed_Response_Buffers_Assigner.js +5 -5
  114. package/publishers/http-sse-publisher.js +341 -0
  115. package/resources/process-resource.js +950 -0
  116. package/resources/processors/bundlers/js/esbuild/Advanced_JS_Bundler_Using_ESBuild.js +129 -33
  117. package/resources/processors/bundlers/js/esbuild/Core_JS_Non_Minifying_Bundler_Using_ESBuild.js +18 -7
  118. package/resources/processors/bundlers/js/esbuild/JSGUI3_HTML_Control_Optimizer.js +829 -0
  119. package/resources/remote-process-resource.js +355 -0
  120. package/resources/server-resource-pool.js +354 -41
  121. package/serve-factory.js +442 -259
  122. package/server.js +288 -13
  123. package/tests/README.md +71 -4
  124. package/tests/admin-ui-jsgui-controls.test.js +581 -0
  125. package/tests/admin-ui-render.test.js +24 -0
  126. package/tests/assigners.test.js +56 -40
  127. package/tests/bundling-default-control-elimination.puppeteer.test.js +260 -0
  128. package/tests/configuration-validation.test.js +21 -18
  129. package/tests/content-analysis.test.js +7 -6
  130. package/tests/control-optimizer-cache-behavior.test.js +52 -0
  131. package/tests/control-scan-manifest-regression.test.js +144 -0
  132. package/tests/end-to-end.test.js +15 -14
  133. package/tests/error-handling.test.js +222 -179
  134. package/tests/fixtures/bundling-default-button-client.js +37 -0
  135. package/tests/fixtures/bundling-default-window-client.js +34 -0
  136. package/tests/fixtures/control_scan_manifest_expectations.json +48 -0
  137. package/tests/fixtures/resource-monitor-client.js +319 -0
  138. package/tests/helpers/puppeteer-e2e-harness.js +317 -0
  139. package/tests/http-sse-publisher.test.js +136 -0
  140. package/tests/performance.test.js +69 -65
  141. package/tests/process-resource.test.js +138 -0
  142. package/tests/publishers.test.js +7 -7
  143. package/tests/remote-process-resource.test.js +160 -0
  144. package/tests/sass-controls.e2e.test.js +7 -1
  145. package/tests/serve-resources.test.js +270 -0
  146. package/tests/serve.test.js +120 -50
  147. package/tests/server-resource-pool.test.js +106 -0
  148. package/tests/small-controls-bundle-size.test.js +252 -0
  149. package/tests/test-runner.js +14 -1
  150. package/tests/window-examples.puppeteer.test.js +204 -1
  151. package/tests/window-resource-integration.puppeteer.test.js +585 -0
  152. package/tests/temp_invalid.js +0 -7
  153. package/tests/temp_invalid_utf8.js +0 -1
  154. package/tests/temp_malformed.js +0 -10
@@ -0,0 +1,1399 @@
1
+ 'use strict';
2
+
3
+ const jsgui = require('jsgui3-client');
4
+ const { controls, Control } = jsgui;
5
+ const Active_HTML_Document = require('../../../controls/Active_HTML_Document');
6
+ const Group_Box = require('./group_box');
7
+ const Stat_Card = require('./stat_card');
8
+
9
+ /**
10
+ * Admin_Shell — root page control for the Admin UI v1 dashboard.
11
+ *
12
+ * Server-side: composes a toolbar, sidebar, and content area with
13
+ * placeholder stat cards. Client-side: fetches /api/admin/v1/status
14
+ * and opens an SSE connection to /api/admin/v1/events for live
15
+ * heartbeat updates.
16
+ *
17
+ * Book reference: Chapter 6 — Implementation Patterns (Admin Shell)
18
+ * Layers: B (View Composition) + D (Concrete Render)
19
+ */
20
+ class Admin_Shell extends Active_HTML_Document {
21
+ constructor(spec = {}) {
22
+ spec.__type_name = spec.__type_name || 'admin_shell';
23
+ super(spec);
24
+ const { context } = this;
25
+
26
+ this._nav_items = [];
27
+ this._tab_items = [];
28
+ this._section_labels = Object.create(null);
29
+ this._custom_nav_items = [];
30
+ this._custom_nav_separator = null;
31
+ this._custom_sections_list = [];
32
+ this._client_bootstrapped = false;
33
+
34
+ if (typeof this.body.add_class === 'function') {
35
+ this.body.add_class('admin-shell');
36
+ }
37
+
38
+ const compose = () => {
39
+ // ─── Sidebar ─────────────────────────────────────
40
+ const sidebar = new controls.div({ context, 'class': 'as-sidebar' });
41
+ this.body.add(sidebar);
42
+ this._sidebar = sidebar;
43
+
44
+ const brand = new controls.div({ context, 'class': 'as-brand' });
45
+ const brand_icon = new controls.span({ context, 'class': 'as-brand-icon' });
46
+ brand_icon.add('\u2699\uFE0F');
47
+ brand.add(brand_icon);
48
+ const brand_text = new controls.span({ context, 'class': 'as-brand-text' });
49
+ brand_text.add('jsgui3 Admin');
50
+ brand.add(brand_text);
51
+ sidebar.add(brand);
52
+
53
+ const nav = new controls.div({ context, 'class': 'as-nav' });
54
+ sidebar.add(nav);
55
+ this._nav = nav;
56
+
57
+ this._add_nav_item(nav, 'Dashboard', 'dashboard', true);
58
+ this._add_nav_item(nav, 'Resources', 'resources');
59
+ this._add_nav_item(nav, 'Routes', 'routes');
60
+ this._add_nav_item(nav, 'Settings', 'settings');
61
+
62
+ // Server version label at bottom
63
+ const footer = new controls.div({ context, 'class': 'as-sidebar-footer' });
64
+ const version_label = new controls.span({ context, 'class': 'as-version' });
65
+ version_label.add('jsgui3-server');
66
+ footer.add(version_label);
67
+ sidebar.add(footer);
68
+
69
+ // ─── Main Area ───────────────────────────────────
70
+ const main = new controls.div({ context, 'class': 'as-main' });
71
+ this.body.add(main);
72
+
73
+ // Toolbar
74
+ const toolbar = new controls.div({ context, 'class': 'as-toolbar' });
75
+ main.add(toolbar);
76
+
77
+ // Hamburger button (hidden on desktop, visible on tablet/phone)
78
+ const hamburger = new controls.button({ context, 'class': 'as-hamburger' });
79
+ hamburger.dom.attributes.type = 'button';
80
+ hamburger.dom.attributes['aria-label'] = 'Toggle navigation';
81
+ hamburger.add('\u2630');
82
+ toolbar.add(hamburger);
83
+ this._hamburger = hamburger;
84
+
85
+ const page_title = new controls.h2({ context, 'class': 'as-page-title' });
86
+ page_title.add('Dashboard');
87
+ toolbar.add(page_title);
88
+ this._page_title = page_title;
89
+
90
+ // Status indicator
91
+ const status_dot = new controls.span({ context, 'class': 'as-status-dot online' });
92
+ status_dot.add('\u25CF');
93
+ toolbar.add(status_dot);
94
+ this._status_dot = status_dot;
95
+
96
+ // Sidebar overlay backdrop (for tablet/phone)
97
+ const overlay = new controls.div({ context, 'class': 'as-sidebar-overlay' });
98
+ this.body.add(overlay);
99
+ this._overlay = overlay;
100
+
101
+ // Content area
102
+ const content = new controls.div({ context, 'class': 'as-content' });
103
+ main.add(content);
104
+ this._content = content;
105
+
106
+ // Dashboard section (SSR + initial view)
107
+ const dashboard_section = new controls.div({ context, 'class': 'as-section as-section-dashboard' });
108
+ content.add(dashboard_section);
109
+ this._dashboard_section = dashboard_section;
110
+
111
+ // Dynamic section (Resources / Routes / Settings)
112
+ const dynamic_section = new controls.div({ context, 'class': 'as-section as-section-dynamic hidden' });
113
+ content.add(dynamic_section);
114
+ this._dynamic_section = dynamic_section;
115
+
116
+ // ─── Dashboard Cards ─────────────────────────────
117
+ this._compose_dashboard(dashboard_section);
118
+
119
+ // ─── Status Bar ──────────────────────────────────
120
+ const status_bar = new controls.div({ context, 'class': 'as-statusbar' });
121
+ main.add(status_bar);
122
+ const status_text = new controls.span({ context, 'class': 'as-statusbar-text' });
123
+ status_text.add('Connecting...');
124
+ status_bar.add(status_text);
125
+ this._status_text = status_text;
126
+
127
+ this._active_section = 'dashboard';
128
+
129
+ // ─── Bottom Tab Bar (phone navigation) ──────────
130
+ const bottom_bar = new controls.div({ context, 'class': 'as-bottom-bar' });
131
+ this.body.add(bottom_bar);
132
+
133
+ const tab_items = [
134
+ { label: '\u2302', section: 'dashboard', text: 'Home' },
135
+ { label: '\u25A6', section: 'resources', text: 'Resources' },
136
+ { label: '\u2194', section: 'routes', text: 'Routes' },
137
+ { label: '\u2699', section: 'settings', text: 'Settings' }
138
+ ];
139
+ tab_items.forEach(item => {
140
+ this._add_tab_item(bottom_bar, item, item.section === 'dashboard');
141
+ });
142
+
143
+ hamburger.on('click', () => this._toggle_sidebar());
144
+ overlay.on('click', () => this._close_sidebar());
145
+ };
146
+
147
+ if (!spec.el) {
148
+ compose();
149
+ }
150
+ }
151
+
152
+ _add_nav_item(nav, label, id, active, options = {}) {
153
+ const { context } = this;
154
+ const item = new controls.div({
155
+ context,
156
+ 'class': 'as-nav-item' + (active ? ' active' : '')
157
+ });
158
+ item.dom.attributes['data-section'] = id;
159
+ const icon_text = options.icon ? options.icon + ' ' : '';
160
+ item.add(icon_text + label);
161
+ nav.add(item);
162
+
163
+ const nav_item_info = {
164
+ id,
165
+ label: options.title || label,
166
+ control: item,
167
+ is_custom: !!options.is_custom
168
+ };
169
+ this._section_labels[id] = nav_item_info.label;
170
+ this._nav_items.push(nav_item_info);
171
+ if (nav_item_info.is_custom) {
172
+ this._custom_nav_items.push(nav_item_info);
173
+ }
174
+
175
+ item.on('click', () => {
176
+ this._activate_section_from_nav(id, nav_item_info.label);
177
+ });
178
+
179
+ return item;
180
+ }
181
+
182
+ _add_tab_item(bottom_bar, item, active) {
183
+ const { context } = this;
184
+ const tab = new controls.div({ context, 'class': 'as-tab-item' + (active ? ' active' : '') });
185
+ tab.dom.attributes['data-section'] = item.section;
186
+
187
+ const icon = new controls.span({ context, 'class': 'as-tab-icon' });
188
+ icon.add(item.label);
189
+ tab.add(icon);
190
+
191
+ const label = new controls.span({ context, 'class': 'as-tab-label' });
192
+ label.add(item.text);
193
+ tab.add(label);
194
+ bottom_bar.add(tab);
195
+
196
+ this._tab_items.push({
197
+ id: item.section,
198
+ label: item.text,
199
+ control: tab
200
+ });
201
+
202
+ tab.on('click', () => {
203
+ this._activate_section_from_tab(item.section, item.text);
204
+ });
205
+ }
206
+
207
+ _compose_dashboard(container) {
208
+ const { context } = this;
209
+
210
+ // Server Info group
211
+ const server_group = new Group_Box({ context, title: 'Server', 'class': 'group-box' });
212
+ container.add(server_group);
213
+
214
+ this._card_uptime = new Stat_Card({
215
+ context, label: 'Uptime', value: '—', detail: 'loading...', accent: '#4facfe', 'class': 'stat_card'
216
+ });
217
+ server_group.inner.add(this._card_uptime);
218
+
219
+ this._card_pid = new Stat_Card({
220
+ context, label: 'PID', value: '—', detail: 'Process ID', accent: '#00d2ff', 'class': 'stat_card'
221
+ });
222
+ server_group.inner.add(this._card_pid);
223
+
224
+ this._card_node = new Stat_Card({
225
+ context, label: 'Node.js', value: '—', detail: 'Runtime version', accent: '#43e97b', 'class': 'stat_card'
226
+ });
227
+ server_group.inner.add(this._card_node);
228
+
229
+ // Resources group
230
+ const resources_group = new Group_Box({ context, title: 'Resources', 'class': 'group-box' });
231
+ container.add(resources_group);
232
+
233
+ this._card_resources = new Stat_Card({
234
+ context, label: 'Total Resources', value: '—', detail: 'loading...', accent: '#fa709a', 'class': 'stat_card'
235
+ });
236
+ resources_group.inner.add(this._card_resources);
237
+
238
+ this._card_routes = new Stat_Card({
239
+ context, label: 'Routes', value: '—', detail: 'Registered endpoints', accent: '#fee140', 'class': 'stat_card'
240
+ });
241
+ resources_group.inner.add(this._card_routes);
242
+
243
+ // Memory group
244
+ const memory_group = new Group_Box({ context, title: 'Memory', 'class': 'group-box' });
245
+ container.add(memory_group);
246
+
247
+ this._card_heap = new Stat_Card({
248
+ context, label: 'Heap Used', value: '—', detail: 'loading...', accent: '#a18cd1', 'class': 'stat_card'
249
+ });
250
+ memory_group.inner.add(this._card_heap);
251
+
252
+ this._card_rss = new Stat_Card({
253
+ context, label: 'RSS', value: '—', detail: 'Resident set size', accent: '#fbc2eb', 'class': 'stat_card'
254
+ });
255
+ memory_group.inner.add(this._card_rss);
256
+
257
+ // Telemetry group
258
+ const telemetry_group = new Group_Box({ context, title: 'Telemetry', 'class': 'group-box' });
259
+ container.add(telemetry_group);
260
+
261
+ this._card_requests = new Stat_Card({
262
+ context, label: 'Requests', value: '0', detail: '0 req/min', accent: '#f093fb', 'class': 'stat_card'
263
+ });
264
+ telemetry_group.inner.add(this._card_requests);
265
+ }
266
+
267
+ // ─── Client-Side Activation ──────────────────────────────
268
+
269
+ activate() {
270
+ if (!this.__active) {
271
+ super.activate();
272
+ }
273
+
274
+ if (!this._client_bootstrapped) {
275
+ this._client_bootstrapped = true;
276
+ this._update_layout_mode();
277
+ if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') {
278
+ if (!this._bound_resize_handler) {
279
+ this._bound_resize_handler = () => this._update_layout_mode();
280
+ }
281
+ window.addEventListener('resize', this._bound_resize_handler);
282
+ }
283
+
284
+ this._fetch_status();
285
+ this._connect_sse();
286
+ this._load_custom_sections();
287
+ }
288
+ }
289
+
290
+ _update_layout_mode() {
291
+ const has_window = typeof window !== 'undefined';
292
+ const w = has_window && typeof window.innerWidth === 'number'
293
+ ? window.innerWidth
294
+ : 1024;
295
+ let mode = 'desktop';
296
+ if (w <= 480) mode = 'phone';
297
+ else if (w <= 768) mode = 'tablet';
298
+ if (this.body && this.body.dom && this.body.dom.attributes) {
299
+ this.body.dom.attributes['data-layout-mode'] = mode;
300
+ }
301
+ if (this.body && this.body.el && typeof this.body.el.setAttribute === 'function') {
302
+ this.body.el.setAttribute('data-layout-mode', mode);
303
+ }
304
+ this._layout_mode = mode;
305
+ }
306
+
307
+ _toggle_sidebar() {
308
+ const sidebar = this._sidebar;
309
+ const overlay = this._overlay;
310
+ if (!sidebar) return;
311
+ const is_open = typeof sidebar.has_class === 'function' && sidebar.has_class('as-sidebar-open');
312
+ if (is_open) {
313
+ this._close_sidebar();
314
+ } else {
315
+ if (typeof sidebar.add_class === 'function') {
316
+ sidebar.add_class('as-sidebar-open');
317
+ }
318
+ if (overlay && typeof overlay.add_class === 'function') {
319
+ overlay.add_class('active');
320
+ }
321
+ }
322
+ }
323
+
324
+ _close_sidebar() {
325
+ if (this._sidebar && typeof this._sidebar.remove_class === 'function') {
326
+ this._sidebar.remove_class('as-sidebar-open');
327
+ }
328
+ if (this._overlay && typeof this._overlay.remove_class === 'function') {
329
+ this._overlay.remove_class('active');
330
+ }
331
+ }
332
+
333
+ _activate_section_from_nav(section_id, section_label) {
334
+ this._set_active_nav(section_id);
335
+ if (String(section_id).indexOf('custom:') === 0) {
336
+ this._set_active_tab(null);
337
+ } else {
338
+ this._set_active_tab(section_id);
339
+ }
340
+ this._set_page_title(section_label || this._get_section_label(section_id));
341
+ this._close_sidebar();
342
+ this._select_section(section_id);
343
+ }
344
+
345
+ _activate_section_from_tab(section_id, section_label) {
346
+ this._set_active_tab(section_id);
347
+ this._set_active_nav(section_id);
348
+ this._set_page_title(section_label || this._get_section_label(section_id));
349
+ this._select_section(section_id);
350
+ }
351
+
352
+ _set_active_nav(section_id) {
353
+ this._nav_items.forEach((nav_item) => {
354
+ if (!nav_item.control) return;
355
+ if (nav_item.id === section_id) {
356
+ nav_item.control.add_class('active');
357
+ } else {
358
+ nav_item.control.remove_class('active');
359
+ }
360
+ });
361
+ }
362
+
363
+ _set_active_tab(section_id) {
364
+ this._tab_items.forEach((tab_item) => {
365
+ if (!tab_item.control) return;
366
+ if (section_id != null && tab_item.id === section_id) {
367
+ tab_item.control.add_class('active');
368
+ } else {
369
+ tab_item.control.remove_class('active');
370
+ }
371
+ });
372
+ }
373
+
374
+ _get_section_label(section_id) {
375
+ return this._section_labels[section_id] || section_id;
376
+ }
377
+
378
+ _set_page_title(text) {
379
+ this._set_control_text(this._page_title, text);
380
+ }
381
+
382
+ _select_section(section) {
383
+ this._active_section = section;
384
+
385
+ const dashboard_section = this._dashboard_section;
386
+ const dynamic_section = this._dynamic_section;
387
+ if (!dashboard_section || !dynamic_section) return Promise.resolve();
388
+
389
+ if (section === 'dashboard') {
390
+ dashboard_section.remove_class('hidden');
391
+ dynamic_section.add_class('hidden');
392
+ this._set_status_text('Dashboard view');
393
+ return Promise.resolve();
394
+ }
395
+
396
+ dashboard_section.add_class('hidden');
397
+ dynamic_section.remove_class('hidden');
398
+
399
+ if (section === 'resources') {
400
+ return this._render_resources_section();
401
+ } else if (section === 'routes') {
402
+ return this._render_routes_section();
403
+ } else if (section === 'settings') {
404
+ return this._render_settings_section();
405
+ } else if (section.startsWith('custom:')) {
406
+ const custom_id = section.substring(7);
407
+ const custom = this._custom_sections_list && this._custom_sections_list.find(s => s.id === custom_id);
408
+ if (custom) {
409
+ return this._render_custom_section(custom);
410
+ }
411
+ }
412
+ return Promise.resolve();
413
+ }
414
+
415
+ _set_control_text(control, text) {
416
+ if (!control) return;
417
+ if (typeof control.clear === 'function') {
418
+ control.clear();
419
+ }
420
+ if (typeof control.add === 'function') {
421
+ control.add(String(text == null ? '' : text));
422
+ }
423
+ }
424
+
425
+ _clear_control(control) {
426
+ if (control && typeof control.clear === 'function') {
427
+ control.clear();
428
+ }
429
+ }
430
+
431
+ _render_loading(message) {
432
+ if (!this._dynamic_section) return;
433
+ this._clear_control(this._dynamic_section);
434
+ const panel = this._create_panel();
435
+ const text = new controls.div({ context: this.context, 'class': 'as-muted' });
436
+ text.add(String(message));
437
+ panel.add(text);
438
+ this._dynamic_section.add(panel);
439
+ }
440
+
441
+ _render_error(message, retry_handler) {
442
+ if (!this._dynamic_section) return;
443
+ this._clear_control(this._dynamic_section);
444
+
445
+ const panel = this._create_panel();
446
+ const text = new controls.div({ context: this.context, 'class': 'as-error' });
447
+ text.add(String(message));
448
+ panel.add(text);
449
+
450
+ const retry_btn = new controls.button({ context: this.context, 'class': 'as-retry-btn' });
451
+ retry_btn.dom.attributes.type = 'button';
452
+ retry_btn.add('Retry');
453
+ if (typeof retry_handler === 'function') {
454
+ retry_btn.on('click', retry_handler);
455
+ }
456
+ panel.add(retry_btn);
457
+
458
+ this._dynamic_section.add(panel);
459
+ }
460
+
461
+ _render_empty(message) {
462
+ if (!this._dynamic_section) return;
463
+ this._clear_control(this._dynamic_section);
464
+
465
+ const panel = this._create_panel();
466
+ const text = new controls.div({ context: this.context, 'class': 'as-muted' });
467
+ text.add(String(message));
468
+ panel.add(text);
469
+ this._dynamic_section.add(panel);
470
+ }
471
+
472
+ _fetch_json(url) {
473
+ return fetch(url).then((res) => {
474
+ if (!res.ok) {
475
+ if (res.status === 401) {
476
+ this._redirect_to_login();
477
+ throw new Error('Unauthorized');
478
+ }
479
+ throw new Error('HTTP ' + res.status + ' for ' + url);
480
+ }
481
+ return res.json();
482
+ });
483
+ }
484
+
485
+ _create_tag_control(tag_name, class_name) {
486
+ const spec = {
487
+ context: this.context,
488
+ tagName: tag_name
489
+ };
490
+ if (class_name) {
491
+ spec.class = class_name;
492
+ }
493
+ return new Control(spec);
494
+ }
495
+
496
+ _create_panel(panel_title) {
497
+ const panel = new controls.div({ context: this.context, 'class': 'as-panel' });
498
+ if (panel_title) {
499
+ const title = new controls.h3({ context: this.context, 'class': 'as-panel-title' });
500
+ title.add(String(panel_title));
501
+ panel.add(title);
502
+ }
503
+ return panel;
504
+ }
505
+
506
+ _stringify_value(value) {
507
+ if (value == null) return '';
508
+ if (typeof value === 'object') {
509
+ try {
510
+ return JSON.stringify(value);
511
+ } catch (err) {
512
+ return String(value);
513
+ }
514
+ }
515
+ return String(value);
516
+ }
517
+
518
+ _create_table_panel(panel_title, column_names, rows_data) {
519
+ const panel = this._create_panel(panel_title);
520
+
521
+ const table = this._create_tag_control('table', 'as-table');
522
+ const thead = this._create_tag_control('thead');
523
+ const header_row = this._create_tag_control('tr');
524
+
525
+ column_names.forEach((column_name) => {
526
+ const th = this._create_tag_control('th');
527
+ th.add(String(column_name));
528
+ header_row.add(th);
529
+ });
530
+ thead.add(header_row);
531
+ table.add(thead);
532
+
533
+ const tbody = this._create_tag_control('tbody');
534
+ rows_data.forEach((row_data) => {
535
+ const row = this._create_tag_control('tr');
536
+ row_data.forEach((cell_value) => {
537
+ const td = this._create_tag_control('td');
538
+ td.add(this._stringify_value(cell_value));
539
+ row.add(td);
540
+ });
541
+ tbody.add(row);
542
+ });
543
+ table.add(tbody);
544
+
545
+ panel.add(table);
546
+ return panel;
547
+ }
548
+
549
+ _create_kv_row(key_text, value_text) {
550
+ const row = new controls.div({ context: this.context, 'class': 'as-kv-row' });
551
+ const key = new controls.span({ context: this.context, 'class': 'as-kv-key' });
552
+ key.add(String(key_text));
553
+ const value = new controls.span({ context: this.context, 'class': 'as-kv-value' });
554
+ value.add(this._stringify_value(value_text));
555
+ row.add(key);
556
+ row.add(value);
557
+ return row;
558
+ }
559
+
560
+ _render_resources_section() {
561
+ this._render_loading('Loading resources...');
562
+ this._set_status_text('Loading resources');
563
+
564
+ return this._fetch_json('/api/admin/v1/resources')
565
+ .then((data) => {
566
+ if (!this._dynamic_section) return;
567
+
568
+ const children = (data && data.children) || [];
569
+ if (children.length === 0) {
570
+ this._render_empty('No resources found.');
571
+ return;
572
+ }
573
+
574
+ const rows_data = children.map((resource_item) => {
575
+ return [
576
+ resource_item.name || 'Unnamed',
577
+ resource_item.type || 'Resource',
578
+ resource_item.state || 'unknown'
579
+ ];
580
+ });
581
+
582
+ this._clear_control(this._dynamic_section);
583
+ this._dynamic_section.add(
584
+ this._create_table_panel('Resources', ['Name', 'Type', 'State'], rows_data)
585
+ );
586
+
587
+ this._set_status_text('Loaded resources: ' + children.length);
588
+ })
589
+ .catch((err) => {
590
+ console.error('[Admin] Failed to load resources:', err);
591
+ this._render_error('Failed to load resources.', () => this._render_resources_section());
592
+ this._set_status_text('Error loading resources');
593
+ });
594
+ }
595
+
596
+ _render_routes_section() {
597
+ this._render_loading('Loading routes...');
598
+ this._set_status_text('Loading routes');
599
+
600
+ return this._fetch_json('/api/admin/v1/routes')
601
+ .then((routes) => {
602
+ if (!this._dynamic_section) return;
603
+
604
+ if (!Array.isArray(routes) || routes.length === 0) {
605
+ this._render_empty('No routes found.');
606
+ return;
607
+ }
608
+
609
+ const rows_data = routes.map((route) => {
610
+ return [
611
+ route.path || '',
612
+ route.method || 'GET',
613
+ route.type || 'route',
614
+ route.handler || 'anonymous'
615
+ ];
616
+ });
617
+
618
+ this._clear_control(this._dynamic_section);
619
+ this._dynamic_section.add(
620
+ this._create_table_panel('Routes', ['Path', 'Method', 'Type', 'Handler'], rows_data)
621
+ );
622
+
623
+ this._set_status_text('Loaded routes: ' + routes.length);
624
+ })
625
+ .catch((err) => {
626
+ console.error('[Admin] Failed to load routes:', err);
627
+ this._render_error('Failed to load routes.', () => this._render_routes_section());
628
+ this._set_status_text('Error loading routes');
629
+ });
630
+ }
631
+
632
+ _render_settings_section() {
633
+ this._render_loading('Loading settings snapshot...');
634
+ this._set_status_text('Loading settings snapshot');
635
+
636
+ return this._fetch_json('/api/admin/v1/status')
637
+ .then((status) => {
638
+ if (!this._dynamic_section) return;
639
+
640
+ const server_name = (status && status.server && status.server.name) || 'jsgui3-server';
641
+ const node_version = (status && status.process && status.process.node_version) || 'unknown';
642
+ const platform = (status && status.process && status.process.platform) || 'unknown';
643
+ const arch = (status && status.process && status.process.arch) || 'unknown';
644
+
645
+ const panel = this._create_panel('Settings (Read-only)');
646
+ panel.add(this._create_kv_row('Server Name', server_name));
647
+ panel.add(this._create_kv_row('Node Version', node_version));
648
+ panel.add(this._create_kv_row('Platform', platform + ' / ' + arch));
649
+
650
+ const logout_btn = new controls.button({ context: this.context, 'class': 'as-logout-btn' });
651
+ logout_btn.dom.attributes.type = 'button';
652
+ logout_btn.add('Log Out');
653
+ logout_btn.on('click', () => {
654
+ fetch('/api/admin/v1/auth/logout', {
655
+ method: 'POST',
656
+ credentials: 'same-origin'
657
+ }).finally(() => {
658
+ this._redirect_to_login();
659
+ });
660
+ });
661
+ panel.add(logout_btn);
662
+
663
+ const muted = new controls.div({ context: this.context, 'class': 'as-muted' });
664
+ muted.add('Authentication and write-actions are intentionally not enabled yet.');
665
+ panel.add(muted);
666
+
667
+ this._clear_control(this._dynamic_section);
668
+ this._dynamic_section.add(panel);
669
+
670
+ this._set_status_text('Loaded settings snapshot');
671
+ })
672
+ .catch((err) => {
673
+ console.error('[Admin] Failed to load settings snapshot:', err);
674
+ this._render_error('Failed to load settings snapshot.', () => this._render_settings_section());
675
+ this._set_status_text('Error loading settings snapshot');
676
+ });
677
+ }
678
+
679
+ _redirect_to_login() {
680
+ if (typeof window !== 'undefined' && window.location) {
681
+ window.location.href = '/admin/v1/login';
682
+ }
683
+ }
684
+
685
+ _fetch_status() {
686
+ return this._fetch_json('/api/admin/v1/status')
687
+ .then(data => {
688
+ this._apply_status(data);
689
+ this._set_status_text('Connected — data loaded');
690
+ })
691
+ .catch(err => {
692
+ console.error('[Admin] Failed to fetch status:', err);
693
+ this._set_status_text('Error loading status');
694
+ });
695
+ }
696
+
697
+ _apply_status(data) {
698
+ if (!data) return;
699
+
700
+ const fmt_bytes = (bytes) => {
701
+ if (bytes == null) return '—';
702
+ const units = ['B', 'KB', 'MB', 'GB'];
703
+ let value = bytes;
704
+ let unit_index = 0;
705
+ while (value >= 1024 && unit_index < units.length - 1) {
706
+ value /= 1024;
707
+ unit_index++;
708
+ }
709
+ return value.toFixed(1) + ' ' + units[unit_index];
710
+ };
711
+
712
+ const fmt_uptime = (seconds) => {
713
+ if (seconds == null) return '—';
714
+ const h = Math.floor(seconds / 3600);
715
+ const m = Math.floor((seconds % 3600) / 60);
716
+ const s = seconds % 60;
717
+ if (h > 0) return h + 'h ' + m + 'm';
718
+ if (m > 0) return m + 'm ' + s + 's';
719
+ return s + 's';
720
+ };
721
+
722
+ // Process info
723
+ if (data.process) {
724
+ const p = data.process;
725
+ if (this._card_uptime) {
726
+ this._card_uptime.set_value(fmt_uptime(p.uptime));
727
+ this._card_uptime.set_detail('PID ' + p.pid);
728
+ }
729
+ if (this._card_pid) {
730
+ this._card_pid.set_value(String(p.pid));
731
+ this._card_pid.set_detail(p.platform + ' / ' + p.arch);
732
+ }
733
+ if (this._card_node) {
734
+ this._card_node.set_value(p.node_version);
735
+ this._card_node.set_detail(p.platform);
736
+ }
737
+ if (data.process.memory) {
738
+ const mem = data.process.memory;
739
+ if (this._card_heap) {
740
+ this._card_heap.set_value(fmt_bytes(mem.heap_used));
741
+ this._card_heap.set_detail(fmt_bytes(mem.heap_total) + ' total');
742
+ }
743
+ if (this._card_rss) {
744
+ this._card_rss.set_value(fmt_bytes(mem.rss));
745
+ this._card_rss.set_detail('External: ' + fmt_bytes(mem.external));
746
+ }
747
+ }
748
+ }
749
+
750
+ // Pool
751
+ if (data.pool) {
752
+ if (this._card_resources) {
753
+ this._card_resources.set_value(String(data.pool.total || 0));
754
+ const running = data.pool.running || 0;
755
+ const stopped = data.pool.stopped || 0;
756
+ this._card_resources.set_detail(running + ' running, ' + stopped + ' stopped');
757
+ }
758
+ }
759
+
760
+ // Routes
761
+ if (data.routes) {
762
+ if (this._card_routes) {
763
+ this._card_routes.set_value(String(data.routes.total || 0));
764
+ }
765
+ }
766
+
767
+ // Telemetry
768
+ if (data.telemetry) {
769
+ if (this._card_requests) {
770
+ this._card_requests.set_value(String(data.telemetry.request_count || 0));
771
+ this._card_requests.set_detail((data.telemetry.requests_per_minute || 0) + ' req/min');
772
+ }
773
+ }
774
+ }
775
+
776
+ _connect_sse() {
777
+ this._sse_backoff = 1000; // start at 1s
778
+ this._sse_max_backoff = 30000; // cap at 30s
779
+ this._open_sse();
780
+ }
781
+
782
+ _open_sse() {
783
+ try {
784
+ if (this._event_source) {
785
+ this._event_source.close();
786
+ this._event_source = null;
787
+ }
788
+
789
+ const es = new EventSource('/api/admin/v1/events');
790
+ this._event_source = es;
791
+
792
+ es.addEventListener('heartbeat', (e) => {
793
+ try {
794
+ const data = JSON.parse(e.data);
795
+ this._apply_heartbeat(data);
796
+ } catch (err) {
797
+ console.warn('[Admin] Bad heartbeat data:', err);
798
+ }
799
+ });
800
+
801
+ es.addEventListener('open', () => {
802
+ // Reset backoff on successful connection
803
+ this._sse_backoff = 1000;
804
+ this._set_status_text('Live — SSE connected');
805
+ if (this._status_dot) {
806
+ this._status_dot.add_class('online');
807
+ this._status_dot.remove_class('offline');
808
+ }
809
+ });
810
+
811
+ es.addEventListener('error', () => {
812
+ this._set_status_text('SSE disconnected — reconnecting...');
813
+ if (this._status_dot) {
814
+ this._status_dot.add_class('offline');
815
+ this._status_dot.remove_class('online');
816
+ }
817
+
818
+ // Close and reconnect with backoff
819
+ es.close();
820
+ this._event_source = null;
821
+ const delay = this._sse_backoff;
822
+ this._sse_backoff = Math.min(this._sse_backoff * 2, this._sse_max_backoff);
823
+ console.log('[Admin] SSE reconnecting in ' + delay + 'ms');
824
+ this._sse_reconnect_timer = setTimeout(() => this._open_sse(), delay);
825
+ });
826
+ } catch (err) {
827
+ console.error('[Admin] SSE not available:', err);
828
+ }
829
+ }
830
+
831
+ _apply_heartbeat(data) {
832
+ if (!data) return;
833
+
834
+ const fmt_bytes = (bytes) => {
835
+ if (bytes == null) return '—';
836
+ const units = ['B', 'KB', 'MB', 'GB'];
837
+ let value = bytes;
838
+ let unit_index = 0;
839
+ while (value >= 1024 && unit_index < units.length - 1) {
840
+ value /= 1024;
841
+ unit_index++;
842
+ }
843
+ return value.toFixed(1) + ' ' + units[unit_index];
844
+ };
845
+
846
+ const fmt_uptime = (seconds) => {
847
+ if (seconds == null) return '—';
848
+ const h = Math.floor(seconds / 3600);
849
+ const m = Math.floor((seconds % 3600) / 60);
850
+ const s = seconds % 60;
851
+ if (h > 0) return h + 'h ' + m + 'm';
852
+ if (m > 0) return m + 'm ' + s + 's';
853
+ return s + 's';
854
+ };
855
+
856
+ // Update cards with heartbeat data
857
+ if (this._card_uptime) {
858
+ this._card_uptime.set_value(fmt_uptime(data.uptime));
859
+ this._card_uptime.set_detail('PID ' + data.pid);
860
+ }
861
+
862
+ if (data.memory) {
863
+ if (this._card_heap) {
864
+ this._card_heap.set_value(fmt_bytes(data.memory.heap_used));
865
+ this._card_heap.set_detail(fmt_bytes(data.memory.heap_total) + ' total');
866
+ }
867
+ if (this._card_rss) {
868
+ this._card_rss.set_value(fmt_bytes(data.memory.rss));
869
+ }
870
+ }
871
+
872
+ if (this._card_requests) {
873
+ this._card_requests.set_value(String(data.request_count || 0));
874
+ this._card_requests.set_detail((data.requests_per_minute || 0) + ' req/min');
875
+ }
876
+
877
+ if (data.pool_summary && this._card_resources) {
878
+ this._card_resources.set_value(String(data.pool_summary.total || 0));
879
+ const running = data.pool_summary.running || 0;
880
+ this._card_resources.set_detail(running + ' running');
881
+ }
882
+
883
+ if (data.route_count != null && this._card_routes) {
884
+ this._card_routes.set_value(String(data.route_count));
885
+ }
886
+
887
+ // Update status bar timestamp
888
+ const now = new Date();
889
+ const hh = String(now.getHours()).padStart(2, '0');
890
+ const mm = String(now.getMinutes()).padStart(2, '0');
891
+ const ss = String(now.getSeconds()).padStart(2, '0');
892
+ this._set_status_text('Live — last update ' + hh + ':' + mm + ':' + ss);
893
+ }
894
+
895
+ _set_status_text(text) {
896
+ this._set_control_text(this._status_text, text);
897
+ }
898
+
899
+ // ─── Custom Sections (Extensibility) ─────────────────
900
+
901
+ /**
902
+ * Fetch custom section metadata from the server and dynamically
903
+ * add nav items to the sidebar. Each custom section gets its own
904
+ * click handler that fetches data from its api_path and renders
905
+ * it using automatic table/key-value rendering.
906
+ */
907
+ _load_custom_sections() {
908
+ this._custom_sections_list = [];
909
+
910
+ return this._fetch_json('/api/admin/v1/custom-sections')
911
+ .then(sections => {
912
+ this._remove_custom_nav_items();
913
+ if (!Array.isArray(sections) || sections.length === 0) return;
914
+
915
+ this._custom_sections_list = sections;
916
+ if (!this._nav) return;
917
+
918
+ this._ensure_custom_nav_separator();
919
+
920
+ sections.forEach(section => {
921
+ const section_id = 'custom:' + section.id;
922
+ const item = this._add_nav_item(
923
+ this._nav,
924
+ section.label || section.id,
925
+ section_id,
926
+ false,
927
+ {
928
+ icon: section.icon || '',
929
+ title: section.label || section.id,
930
+ is_custom: true
931
+ }
932
+ );
933
+ if (this.__active && typeof item.activate === 'function') {
934
+ item.activate();
935
+ }
936
+ });
937
+ })
938
+ .catch(err => {
939
+ console.warn('[Admin] Failed to load custom sections:', err);
940
+ });
941
+ }
942
+
943
+ _remove_custom_nav_items() {
944
+ const custom_ids = this._custom_nav_items.map((custom_item) => custom_item.id);
945
+ this._custom_nav_items.forEach((custom_item) => {
946
+ if (custom_item.control && typeof custom_item.control.remove === 'function') {
947
+ custom_item.control.remove();
948
+ }
949
+ });
950
+ this._custom_nav_items = [];
951
+
952
+ this._nav_items = this._nav_items.filter((nav_item) => !nav_item.is_custom);
953
+ custom_ids.forEach((custom_id) => {
954
+ delete this._section_labels[custom_id];
955
+ });
956
+
957
+ if (this._custom_nav_separator && typeof this._custom_nav_separator.remove === 'function') {
958
+ this._custom_nav_separator.remove();
959
+ this._custom_nav_separator = null;
960
+ }
961
+ }
962
+
963
+ _ensure_custom_nav_separator() {
964
+ if (this._custom_nav_separator || !this._nav) return;
965
+ const separator = new controls.div({ context: this.context, 'class': 'as-nav-separator' });
966
+ this._nav.add(separator);
967
+ this._custom_nav_separator = separator;
968
+ if (this.__active && typeof separator.activate === 'function') {
969
+ separator.activate();
970
+ }
971
+ }
972
+
973
+ /**
974
+ * Render a custom section by fetching its data endpoint.
975
+ * Arrays are rendered as tables, objects as key-value panels,
976
+ * scalars as plain text.
977
+ */
978
+ _render_custom_section(section) {
979
+ if (!this._dashboard_section || !this._dynamic_section) return Promise.resolve();
980
+
981
+ this._dashboard_section.add_class('hidden');
982
+ this._dynamic_section.remove_class('hidden');
983
+
984
+ this._render_loading('Loading ' + section.label + '...');
985
+ this._set_status_text('Loading ' + section.label);
986
+
987
+ return this._fetch_json(section.api_path)
988
+ .then(data => {
989
+ this._render_custom_section_data(section, data);
990
+ this._set_status_text('Loaded ' + section.label);
991
+ })
992
+ .catch(err => {
993
+ console.error('[Admin] Failed to load custom section:', err);
994
+ this._render_error('Failed to load ' + section.label + '.', () => this._render_custom_section(section));
995
+ this._set_status_text('Error loading ' + section.label);
996
+ });
997
+ }
998
+
999
+ /**
1000
+ * Auto-render data based on shape:
1001
+ * - Array of objects → table
1002
+ * - Object → key-value panel
1003
+ * - Scalar → plain text
1004
+ */
1005
+ _render_custom_section_data(section, data) {
1006
+ if (!this._dynamic_section) return;
1007
+
1008
+ if (Array.isArray(data)) {
1009
+ if (data.length === 0) {
1010
+ this._render_empty('No data for ' + section.label + '.');
1011
+ return;
1012
+ }
1013
+ const normalized_first_row = (data[0] && typeof data[0] === 'object')
1014
+ ? data[0]
1015
+ : { value: data[0] };
1016
+ const keys = Object.keys(normalized_first_row);
1017
+ const rows_data = data.map((row) => {
1018
+ const normalized_row = (row && typeof row === 'object') ? row : { value: row };
1019
+ return keys.map((key) => normalized_row[key]);
1020
+ });
1021
+
1022
+ this._clear_control(this._dynamic_section);
1023
+ this._dynamic_section.add(
1024
+ this._create_table_panel(section.label, keys, rows_data)
1025
+ );
1026
+ } else if (data && typeof data === 'object') {
1027
+ const panel = this._create_panel(section.label);
1028
+ Object.entries(data).forEach((entry) => {
1029
+ panel.add(this._create_kv_row(entry[0], entry[1]));
1030
+ });
1031
+ this._clear_control(this._dynamic_section);
1032
+ this._dynamic_section.add(panel);
1033
+ } else {
1034
+ const panel = this._create_panel();
1035
+ const text = new controls.div({ context: this.context, 'class': 'as-muted' });
1036
+ text.add(this._stringify_value(data));
1037
+ panel.add(text);
1038
+ this._clear_control(this._dynamic_section);
1039
+ this._dynamic_section.add(panel);
1040
+ }
1041
+ }
1042
+ }
1043
+
1044
+ Admin_Shell.css = `
1045
+ /* ─── Reset & Root ───────────────────────────────────────── */
1046
+ * { box-sizing: border-box; margin: 0; padding: 0; }
1047
+ body {
1048
+ background: #1a1a2e;
1049
+ color: #e0e0e0;
1050
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
1051
+ height: 100vh;
1052
+ overflow: hidden;
1053
+ }
1054
+
1055
+ /* ─── Shell Grid ─────────────────────────────────────────── */
1056
+ .admin-shell {
1057
+ display: grid;
1058
+ grid-template-columns: 240px 1fr;
1059
+ height: 100%;
1060
+ }
1061
+
1062
+ /* ─── Sidebar ────────────────────────────────────────────── */
1063
+ .as-sidebar {
1064
+ background: #16213e;
1065
+ border-right: 1px solid #2a2a4a;
1066
+ display: flex;
1067
+ flex-direction: column;
1068
+ overflow: hidden;
1069
+ }
1070
+ .as-brand {
1071
+ padding: 20px;
1072
+ font-size: 1.15rem;
1073
+ font-weight: 700;
1074
+ color: #fff;
1075
+ border-bottom: 1px solid #2a2a4a;
1076
+ display: flex;
1077
+ align-items: center;
1078
+ gap: 10px;
1079
+ }
1080
+ .as-brand-icon {
1081
+ font-size: 1.3rem;
1082
+ }
1083
+ .as-nav {
1084
+ flex: 1;
1085
+ padding: 16px 0;
1086
+ overflow-y: auto;
1087
+ }
1088
+ .as-nav-item {
1089
+ padding: 11px 24px;
1090
+ cursor: pointer;
1091
+ border-left: 3px solid transparent;
1092
+ transition: all 0.15s ease;
1093
+ color: #8888aa;
1094
+ font-size: 0.9rem;
1095
+ user-select: none;
1096
+ }
1097
+ .as-nav-item:hover {
1098
+ background: rgba(255,255,255,0.04);
1099
+ color: #c0c0d0;
1100
+ }
1101
+ .as-nav-item.active {
1102
+ background: rgba(79, 172, 254, 0.08);
1103
+ color: #4facfe;
1104
+ border-left-color: #4facfe;
1105
+ font-weight: 500;
1106
+ }
1107
+ .as-sidebar-footer {
1108
+ padding: 14px 20px;
1109
+ border-top: 1px solid #2a2a4a;
1110
+ }
1111
+ .as-version {
1112
+ font-size: 0.7rem;
1113
+ color: #555570;
1114
+ }
1115
+
1116
+ /* ─── Custom Section Separator ──────────────────────── */
1117
+ .as-nav-separator {
1118
+ height: 1px;
1119
+ background: #2a2a4a;
1120
+ margin: 10px 16px;
1121
+ }
1122
+
1123
+ /* ─── Main Area ──────────────────────────────────────────── */
1124
+ .as-main {
1125
+ display: flex;
1126
+ flex-direction: column;
1127
+ min-width: 0;
1128
+ }
1129
+ .as-toolbar {
1130
+ height: 56px;
1131
+ border-bottom: 1px solid #2a2a4a;
1132
+ display: flex;
1133
+ align-items: center;
1134
+ justify-content: space-between;
1135
+ padding: 0 28px;
1136
+ background: #16213e;
1137
+ flex-shrink: 0;
1138
+ }
1139
+ .as-page-title {
1140
+ font-size: 1.1rem;
1141
+ font-weight: 500;
1142
+ color: #fff;
1143
+ }
1144
+ .as-status-dot {
1145
+ font-size: 0.65rem;
1146
+ }
1147
+ .as-status-dot.online { color: #43e97b; }
1148
+ .as-status-dot.offline { color: #fa709a; }
1149
+
1150
+ /* ─── Content ────────────────────────────────────────────── */
1151
+ .as-content {
1152
+ flex: 1;
1153
+ overflow-y: auto;
1154
+ padding: 28px;
1155
+ }
1156
+ .as-section.hidden {
1157
+ display: none;
1158
+ }
1159
+ .as-panel {
1160
+ background: #20203a;
1161
+ border: 1px solid #2f2f52;
1162
+ border-radius: 8px;
1163
+ padding: 16px;
1164
+ }
1165
+ .as-panel-title {
1166
+ color: #fff;
1167
+ font-size: 0.95rem;
1168
+ margin-bottom: 12px;
1169
+ }
1170
+ .as-table {
1171
+ width: 100%;
1172
+ border-collapse: collapse;
1173
+ font-size: 0.82rem;
1174
+ }
1175
+ .as-table th,
1176
+ .as-table td {
1177
+ text-align: left;
1178
+ border-bottom: 1px solid #303058;
1179
+ padding: 8px 10px;
1180
+ color: #d4d4e8;
1181
+ vertical-align: top;
1182
+ }
1183
+ .as-table th {
1184
+ color: #9fa4c8;
1185
+ font-weight: 600;
1186
+ }
1187
+ .as-muted {
1188
+ color: #8a8fb0;
1189
+ font-size: 0.8rem;
1190
+ }
1191
+ .as-error {
1192
+ color: #ff8c9d;
1193
+ margin-bottom: 10px;
1194
+ font-size: 0.82rem;
1195
+ }
1196
+ .as-retry-btn {
1197
+ border: 1px solid #4facfe;
1198
+ background: #1f3552;
1199
+ color: #b7ddff;
1200
+ border-radius: 6px;
1201
+ padding: 6px 10px;
1202
+ cursor: pointer;
1203
+ }
1204
+ .as-retry-btn:hover {
1205
+ background: #294770;
1206
+ }
1207
+ .as-logout-btn {
1208
+ margin-top: 12px;
1209
+ border: 1px solid #ff8c9d;
1210
+ background: #5a2230;
1211
+ color: #ffd3da;
1212
+ border-radius: 6px;
1213
+ padding: 6px 10px;
1214
+ cursor: pointer;
1215
+ }
1216
+ .as-logout-btn:hover {
1217
+ background: #743041;
1218
+ }
1219
+ .as-kv-row {
1220
+ display: flex;
1221
+ justify-content: space-between;
1222
+ gap: 12px;
1223
+ padding: 7px 0;
1224
+ border-bottom: 1px solid #303058;
1225
+ font-size: 0.82rem;
1226
+ }
1227
+ .as-kv-key {
1228
+ color: #9fa4c8;
1229
+ }
1230
+ .as-kv-value {
1231
+ color: #d4d4e8;
1232
+ }
1233
+
1234
+ /* ─── Status Bar ─────────────────────────────────────────── */
1235
+ .as-statusbar {
1236
+ height: 28px;
1237
+ border-top: 1px solid #2a2a4a;
1238
+ background: #141428;
1239
+ display: flex;
1240
+ align-items: center;
1241
+ padding: 0 20px;
1242
+ flex-shrink: 0;
1243
+ }
1244
+ .as-statusbar-text {
1245
+ font-size: 0.7rem;
1246
+ color: #555570;
1247
+ }
1248
+
1249
+ /* ─── Hamburger Button ───────────────────────────────────── */
1250
+ .as-hamburger {
1251
+ display: none;
1252
+ background: none;
1253
+ border: 1px solid #3a3a5a;
1254
+ color: #c0c0d0;
1255
+ font-size: 1.3rem;
1256
+ padding: 6px 10px;
1257
+ border-radius: 6px;
1258
+ cursor: pointer;
1259
+ line-height: 1;
1260
+ margin-right: 12px;
1261
+ min-width: 44px;
1262
+ min-height: 44px;
1263
+ align-items: center;
1264
+ justify-content: center;
1265
+ }
1266
+ .as-hamburger:hover {
1267
+ background: rgba(255,255,255,0.06);
1268
+ }
1269
+
1270
+ /* ─── Sidebar Overlay Backdrop ───────────────────────────── */
1271
+ .as-sidebar-overlay {
1272
+ display: none;
1273
+ position: fixed;
1274
+ inset: 0;
1275
+ background: rgba(0,0,0,0.5);
1276
+ z-index: 98;
1277
+ }
1278
+ .as-sidebar-overlay.active {
1279
+ display: block;
1280
+ }
1281
+
1282
+ /* ─── Bottom Tab Bar (phone) ─────────────────────────────── */
1283
+ .as-bottom-bar {
1284
+ display: none;
1285
+ position: fixed;
1286
+ bottom: 0;
1287
+ left: 0;
1288
+ right: 0;
1289
+ height: 56px;
1290
+ background: #16213e;
1291
+ border-top: 1px solid #2a2a4a;
1292
+ z-index: 100;
1293
+ justify-content: space-around;
1294
+ align-items: stretch;
1295
+ }
1296
+ .as-tab-item {
1297
+ flex: 1;
1298
+ display: flex;
1299
+ flex-direction: column;
1300
+ align-items: center;
1301
+ justify-content: center;
1302
+ cursor: pointer;
1303
+ color: #6a6a8a;
1304
+ font-size: 0.65rem;
1305
+ padding: 4px 0;
1306
+ min-height: 44px;
1307
+ user-select: none;
1308
+ transition: color 0.15s;
1309
+ }
1310
+ .as-tab-item.active {
1311
+ color: #4facfe;
1312
+ }
1313
+ .as-tab-icon {
1314
+ font-size: 1.2rem;
1315
+ line-height: 1;
1316
+ margin-bottom: 2px;
1317
+ }
1318
+ .as-tab-label {
1319
+ font-size: 0.6rem;
1320
+ letter-spacing: 0.03em;
1321
+ }
1322
+
1323
+ /* ─── Responsive: Tablet — sidebar overlay (Chapter 2, Layer C) ── */
1324
+ @media (max-width: 768px) {
1325
+ .admin-shell {
1326
+ grid-template-columns: 1fr;
1327
+ }
1328
+ .as-hamburger {
1329
+ display: flex;
1330
+ }
1331
+ .as-sidebar {
1332
+ display: none;
1333
+ position: fixed;
1334
+ top: 0;
1335
+ left: 0;
1336
+ bottom: 0;
1337
+ width: 260px;
1338
+ z-index: 99;
1339
+ box-shadow: 4px 0 20px rgba(0,0,0,0.4);
1340
+ }
1341
+ .as-sidebar.as-sidebar-open {
1342
+ display: flex;
1343
+ }
1344
+ .as-nav-item {
1345
+ padding: 14px 24px;
1346
+ min-height: 44px;
1347
+ }
1348
+ }
1349
+
1350
+ /* ─── Responsive: Phone — bottom tab bar + dense content ───── */
1351
+ @media (max-width: 480px) {
1352
+ .as-bottom-bar {
1353
+ display: flex;
1354
+ }
1355
+ .as-content {
1356
+ padding: 16px;
1357
+ padding-bottom: 72px;
1358
+ }
1359
+ .as-statusbar {
1360
+ bottom: 56px;
1361
+ position: fixed;
1362
+ left: 0;
1363
+ right: 0;
1364
+ z-index: 99;
1365
+ }
1366
+ .as-toolbar {
1367
+ padding: 0 12px;
1368
+ }
1369
+ .as-page-title {
1370
+ font-size: 0.95rem;
1371
+ }
1372
+ .stat_card {
1373
+ min-width: 140px;
1374
+ max-width: none;
1375
+ flex: 1 1 140px;
1376
+ padding: 14px 16px;
1377
+ }
1378
+ .stat-value {
1379
+ font-size: 1.4rem;
1380
+ }
1381
+ .as-table {
1382
+ font-size: 0.75rem;
1383
+ }
1384
+ .as-table th,
1385
+ .as-table td {
1386
+ padding: 6px 6px;
1387
+ }
1388
+ .group-box {
1389
+ padding: 14px 10px 10px;
1390
+ margin-bottom: 14px;
1391
+ }
1392
+ .group-box-content {
1393
+ gap: 10px;
1394
+ }
1395
+ }
1396
+ `;
1397
+
1398
+ controls.Admin_Shell = Admin_Shell;
1399
+ module.exports = jsgui;