jsgui3-server 0.0.150 → 0.0.152

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 (86) hide show
  1. package/.github/instructions/copilot.instructions.md +1 -0
  2. package/AGENTS.md +2 -0
  3. package/README.md +89 -13
  4. package/admin-ui/v1/controls/admin_shell.js +702 -669
  5. package/admin-ui/v1/server.js +14 -1
  6. package/docs/api-reference.md +504 -306
  7. package/docs/books/creating-a-new-admin-ui/README.md +20 -20
  8. package/docs/books/website-design/01-introduction.md +73 -0
  9. package/docs/books/website-design/02-current-state.md +195 -0
  10. package/docs/books/website-design/03-base-class.md +181 -0
  11. package/docs/books/website-design/04-webpage.md +307 -0
  12. package/docs/books/website-design/05-website.md +456 -0
  13. package/docs/books/website-design/06-pages-storage.md +170 -0
  14. package/docs/books/website-design/07-api-layer.md +285 -0
  15. package/docs/books/website-design/08-server-integration.md +271 -0
  16. package/docs/books/website-design/09-cross-agent-review.md +190 -0
  17. package/docs/books/website-design/10-open-questions.md +196 -0
  18. package/docs/books/website-design/11-converged-recommendation.md +205 -0
  19. package/docs/books/website-design/12-content-model.md +395 -0
  20. package/docs/books/website-design/13-webpage-module-spec.md +404 -0
  21. package/docs/books/website-design/14-website-module-spec.md +541 -0
  22. package/docs/books/website-design/15-multi-repo-plan.md +275 -0
  23. package/docs/books/website-design/16-minimal-first.md +203 -0
  24. package/docs/books/website-design/17-implementation-report-codex.md +81 -0
  25. package/docs/books/website-design/README.md +43 -0
  26. package/docs/comprehensive-documentation.md +220 -220
  27. package/docs/configuration-reference.md +281 -204
  28. package/docs/middleware-guide.md +236 -0
  29. package/docs/proposals/jsgui3-website-and-webpage-design-jsgui3-server-support.md +257 -0
  30. package/docs/proposals/jsgui3-website-and-webpage-design-review.md +73 -0
  31. package/docs/proposals/jsgui3-website-and-webpage-design.md +732 -0
  32. package/docs/swagger.md +316 -0
  33. package/docs/system-architecture.md +24 -18
  34. package/examples/controls/1) window/server.js +6 -1
  35. package/examples/controls/21) mvvm and declarative api/check.js +94 -0
  36. package/examples/controls/21) mvvm and declarative api/check_output.txt +25 -0
  37. package/examples/controls/21) mvvm and declarative api/check_output_2.txt +27 -0
  38. package/examples/controls/21) mvvm and declarative api/client.js +241 -0
  39. declarative api/e2e-screenshot-1-name-change.png +0 -0
  40. declarative api/e2e-screenshot-2-toggled.png +0 -0
  41. declarative api/e2e-screenshot-3-final.png +0 -0
  42. declarative api/e2e-screenshot-final.png +0 -0
  43. package/examples/controls/21) mvvm and declarative api/e2e-test.js +175 -0
  44. package/examples/controls/21) mvvm and declarative api/out.html +1 -0
  45. package/examples/controls/21) mvvm and declarative api/page_out.html +1 -0
  46. package/examples/controls/21) mvvm and declarative api/server.js +18 -0
  47. package/examples/data-views/01) query-endpoint/server.js +61 -0
  48. package/labs/website-design/001-base-class-overhead/check.js +162 -0
  49. package/labs/website-design/002-pages-storage/check.js +244 -0
  50. package/labs/website-design/002-pages-storage/results.txt +0 -0
  51. package/labs/website-design/003-type-detection/check.js +193 -0
  52. package/labs/website-design/003-type-detection/results.txt +0 -0
  53. package/labs/website-design/004-two-stage-validation/check.js +314 -0
  54. package/labs/website-design/004-two-stage-validation/results.txt +0 -0
  55. package/labs/website-design/005-normalize-input/check.js +303 -0
  56. package/labs/website-design/006-serve-website-spike/check.js +290 -0
  57. package/labs/website-design/README.md +34 -0
  58. package/labs/website-design/manifest.json +68 -0
  59. package/labs/website-design/run-all.js +60 -0
  60. package/middleware/compression.js +217 -0
  61. package/middleware/index.js +15 -0
  62. package/middleware/json-body.js +126 -0
  63. package/module.js +3 -0
  64. package/openapi.js +474 -0
  65. package/package.json +11 -8
  66. package/publishers/Publishers.js +6 -5
  67. package/publishers/http-function-publisher.js +135 -126
  68. package/publishers/http-webpage-publisher.js +89 -11
  69. package/publishers/query-publisher.js +116 -0
  70. package/publishers/swagger-publisher.js +203 -0
  71. package/publishers/swagger-ui.js +578 -0
  72. package/resources/adapters/array-adapter.js +143 -0
  73. package/resources/query-resource.js +131 -0
  74. package/serve-factory.js +756 -18
  75. package/server.js +502 -123
  76. package/tests/README.md +23 -1
  77. package/tests/admin-ui-jsgui-controls.test.js +16 -1
  78. package/tests/helpers/playwright-e2e-harness.js +326 -0
  79. package/tests/openapi.test.js +319 -0
  80. package/tests/playwright-smoke.test.js +134 -0
  81. package/tests/publish-enhancements.test.js +673 -0
  82. package/tests/query-publisher.test.js +430 -0
  83. package/tests/quick-json-body-test.js +169 -0
  84. package/tests/serve.test.js +425 -122
  85. package/tests/swagger-publisher.test.js +1076 -0
  86. package/tests/test-runner.js +1 -0
@@ -17,29 +17,29 @@ const Stat_Card = require('./stat_card');
17
17
  * Book reference: Chapter 6 — Implementation Patterns (Admin Shell)
18
18
  * Layers: B (View Composition) + D (Concrete Render)
19
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
- }
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
37
 
38
38
  const compose = () => {
39
- // ─── Sidebar ─────────────────────────────────────
40
- const sidebar = new controls.div({ context, 'class': 'as-sidebar' });
41
- this.body.add(sidebar);
42
- this._sidebar = sidebar;
39
+ // ─── Sidebar ─────────────────────────────────────
40
+ const sidebar = new controls.div({ context, 'class': 'as-sidebar' });
41
+ this.body.add(sidebar);
42
+ this._sidebar = sidebar;
43
43
 
44
44
  const brand = new controls.div({ context, 'class': 'as-brand' });
45
45
  const brand_icon = new controls.span({ context, 'class': 'as-brand-icon' });
@@ -50,11 +50,11 @@ class Admin_Shell extends Active_HTML_Document {
50
50
  brand.add(brand_text);
51
51
  sidebar.add(brand);
52
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);
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
58
  this._add_nav_item(nav, 'Resources', 'resources');
59
59
  this._add_nav_item(nav, 'Routes', 'routes');
60
60
  this._add_nav_item(nav, 'Settings', 'settings');
@@ -75,12 +75,12 @@ class Admin_Shell extends Active_HTML_Document {
75
75
  main.add(toolbar);
76
76
 
77
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;
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
84
 
85
85
  const page_title = new controls.h2({ context, 'class': 'as-page-title' });
86
86
  page_title.add('Dashboard');
@@ -94,9 +94,9 @@ class Admin_Shell extends Active_HTML_Document {
94
94
  this._status_dot = status_dot;
95
95
 
96
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;
97
+ const overlay = new controls.div({ context, 'class': 'as-sidebar-overlay' });
98
+ this.body.add(overlay);
99
+ this._overlay = overlay;
100
100
 
101
101
  // Content area
102
102
  const content = new controls.div({ context, 'class': 'as-content' });
@@ -130,82 +130,82 @@ class Admin_Shell extends Active_HTML_Document {
130
130
  const bottom_bar = new controls.div({ context, 'class': 'as-bottom-bar' });
131
131
  this.body.add(bottom_bar);
132
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;
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
209
 
210
210
  // Server Info group
211
211
  const server_group = new Group_Box({ context, title: 'Server', 'class': 'group-box' });
@@ -258,437 +258,470 @@ class Admin_Shell extends Active_HTML_Document {
258
258
  const telemetry_group = new Group_Box({ context, title: 'Telemetry', 'class': 'group-box' });
259
259
  container.add(telemetry_group);
260
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);
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
591
  this._render_error('Failed to load resources.', () => this._render_resources_section());
592
592
  this._set_status_text('Error loading resources');
593
593
  });
594
594
  }
595
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);
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
627
  this._render_error('Failed to load routes.', () => this._render_routes_section());
628
628
  this._set_status_text('Error loading routes');
629
629
  });
630
630
  }
631
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);
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 primary_endpoint = (status && status.server && status.server.primary_endpoint) || '';
651
+ const listening_endpoints = (status && status.server && status.server.listening_endpoints) || [];
652
+ const startup_diagnostics = (status && status.server && status.server.startup_diagnostics) || null;
653
+
654
+ const startup_panel = this._create_panel('Startup & Network');
655
+ startup_panel.add(this._create_kv_row('Primary Endpoint', primary_endpoint));
656
+ startup_panel.add(this._create_kv_row('Listening Endpoints', listening_endpoints.length));
657
+
658
+ if (Array.isArray(listening_endpoints) && listening_endpoints.length > 0) {
659
+ const endpoints_list = new controls.div({ context: this.context, 'class': 'as-kv-stack' });
660
+ listening_endpoints.forEach((endpoint, index) => {
661
+ const endpoint_url = endpoint && endpoint.url ? endpoint.url : '';
662
+ endpoints_list.add(this._create_kv_row('Endpoint ' + (index + 1), endpoint_url));
663
+ });
664
+ startup_panel.add(endpoints_list);
665
+ }
666
+
667
+ if (startup_diagnostics) {
668
+ startup_panel.add(this._create_kv_row('Requested Port', startup_diagnostics.requested_port));
669
+ if (startup_diagnostics.fallback_port) {
670
+ startup_panel.add(this._create_kv_row('Fallback Port', startup_diagnostics.fallback_port));
671
+ }
672
+ if (startup_diagnostics.fallback_host) {
673
+ startup_panel.add(this._create_kv_row('Fallback Host', startup_diagnostics.fallback_host));
674
+ }
675
+ const attempted = Array.isArray(startup_diagnostics.addresses_attempted)
676
+ ? startup_diagnostics.addresses_attempted.join(', ')
677
+ : '';
678
+ startup_panel.add(this._create_kv_row('Addresses Attempted', attempted || '—'));
679
+ }
680
+
681
+ panel.add(startup_panel);
682
+
683
+ const logout_btn = new controls.button({ context: this.context, 'class': 'as-logout-btn' });
684
+ logout_btn.dom.attributes.type = 'button';
685
+ logout_btn.add('Log Out');
686
+ logout_btn.on('click', () => {
687
+ fetch('/api/admin/v1/auth/logout', {
688
+ method: 'POST',
689
+ credentials: 'same-origin'
690
+ }).finally(() => {
691
+ this._redirect_to_login();
692
+ });
693
+ });
694
+ panel.add(logout_btn);
695
+
696
+ const muted = new controls.div({ context: this.context, 'class': 'as-muted' });
697
+ muted.add('Authentication and write-actions are intentionally not enabled yet.');
698
+ panel.add(muted);
699
+
700
+ this._clear_control(this._dynamic_section);
701
+ this._dynamic_section.add(panel);
702
+
703
+ this._set_status_text('Loaded settings snapshot');
704
+ })
705
+ .catch((err) => {
706
+ console.error('[Admin] Failed to load settings snapshot:', err);
674
707
  this._render_error('Failed to load settings snapshot.', () => this._render_settings_section());
675
708
  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 => {
709
+ });
710
+ }
711
+
712
+ _redirect_to_login() {
713
+ if (typeof window !== 'undefined' && window.location) {
714
+ window.location.href = '/admin/v1/login';
715
+ }
716
+ }
717
+
718
+ _fetch_status() {
719
+ return this._fetch_json('/api/admin/v1/status')
720
+ .then(data => {
721
+ this._apply_status(data);
722
+ this._set_status_text('Connected — data loaded');
723
+ })
724
+ .catch(err => {
692
725
  console.error('[Admin] Failed to fetch status:', err);
693
726
  this._set_status_text('Error loading status');
694
727
  });
@@ -798,22 +831,22 @@ class Admin_Shell extends Active_HTML_Document {
798
831
  }
799
832
  });
800
833
 
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
- }
834
+ es.addEventListener('open', () => {
835
+ // Reset backoff on successful connection
836
+ this._sse_backoff = 1000;
837
+ this._set_status_text('Live — SSE connected');
838
+ if (this._status_dot) {
839
+ this._status_dot.add_class('online');
840
+ this._status_dot.remove_class('offline');
841
+ }
842
+ });
843
+
844
+ es.addEventListener('error', () => {
845
+ this._set_status_text('SSE disconnected — reconnecting...');
846
+ if (this._status_dot) {
847
+ this._status_dot.add_class('offline');
848
+ this._status_dot.remove_class('online');
849
+ }
817
850
 
818
851
  // Close and reconnect with backoff
819
852
  es.close();
@@ -892,9 +925,9 @@ class Admin_Shell extends Active_HTML_Document {
892
925
  this._set_status_text('Live — last update ' + hh + ':' + mm + ':' + ss);
893
926
  }
894
927
 
895
- _set_status_text(text) {
896
- this._set_control_text(this._status_text, text);
897
- }
928
+ _set_status_text(text) {
929
+ this._set_control_text(this._status_text, text);
930
+ }
898
931
 
899
932
  // ─── Custom Sections (Extensibility) ─────────────────
900
933
 
@@ -904,91 +937,91 @@ class Admin_Shell extends Active_HTML_Document {
904
937
  * click handler that fetches data from its api_path and renders
905
938
  * it using automatic table/key-value rendering.
906
939
  */
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
- }
940
+ _load_custom_sections() {
941
+ this._custom_sections_list = [];
942
+
943
+ return this._fetch_json('/api/admin/v1/custom-sections')
944
+ .then(sections => {
945
+ this._remove_custom_nav_items();
946
+ if (!Array.isArray(sections) || sections.length === 0) return;
947
+
948
+ this._custom_sections_list = sections;
949
+ if (!this._nav) return;
950
+
951
+ this._ensure_custom_nav_separator();
952
+
953
+ sections.forEach(section => {
954
+ const section_id = 'custom:' + section.id;
955
+ const item = this._add_nav_item(
956
+ this._nav,
957
+ section.label || section.id,
958
+ section_id,
959
+ false,
960
+ {
961
+ icon: section.icon || '',
962
+ title: section.label || section.id,
963
+ is_custom: true
964
+ }
965
+ );
966
+ if (this.__active && typeof item.activate === 'function') {
967
+ item.activate();
968
+ }
969
+ });
970
+ })
971
+ .catch(err => {
972
+ console.warn('[Admin] Failed to load custom sections:', err);
973
+ });
974
+ }
975
+
976
+ _remove_custom_nav_items() {
977
+ const custom_ids = this._custom_nav_items.map((custom_item) => custom_item.id);
978
+ this._custom_nav_items.forEach((custom_item) => {
979
+ if (custom_item.control && typeof custom_item.control.remove === 'function') {
980
+ custom_item.control.remove();
981
+ }
982
+ });
983
+ this._custom_nav_items = [];
984
+
985
+ this._nav_items = this._nav_items.filter((nav_item) => !nav_item.is_custom);
986
+ custom_ids.forEach((custom_id) => {
987
+ delete this._section_labels[custom_id];
988
+ });
989
+
990
+ if (this._custom_nav_separator && typeof this._custom_nav_separator.remove === 'function') {
991
+ this._custom_nav_separator.remove();
992
+ this._custom_nav_separator = null;
993
+ }
994
+ }
995
+
996
+ _ensure_custom_nav_separator() {
997
+ if (this._custom_nav_separator || !this._nav) return;
998
+ const separator = new controls.div({ context: this.context, 'class': 'as-nav-separator' });
999
+ this._nav.add(separator);
1000
+ this._custom_nav_separator = separator;
1001
+ if (this.__active && typeof separator.activate === 'function') {
1002
+ separator.activate();
1003
+ }
1004
+ }
972
1005
 
973
1006
  /**
974
1007
  * Render a custom section by fetching its data endpoint.
975
1008
  * Arrays are rendered as tables, objects as key-value panels,
976
1009
  * scalars as plain text.
977
1010
  */
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
- })
1011
+ _render_custom_section(section) {
1012
+ if (!this._dashboard_section || !this._dynamic_section) return Promise.resolve();
1013
+
1014
+ this._dashboard_section.add_class('hidden');
1015
+ this._dynamic_section.remove_class('hidden');
1016
+
1017
+ this._render_loading('Loading ' + section.label + '...');
1018
+ this._set_status_text('Loading ' + section.label);
1019
+
1020
+ return this._fetch_json(section.api_path)
1021
+ .then(data => {
1022
+ this._render_custom_section_data(section, data);
1023
+ this._set_status_text('Loaded ' + section.label);
1024
+ })
992
1025
  .catch(err => {
993
1026
  console.error('[Admin] Failed to load custom section:', err);
994
1027
  this._render_error('Failed to load ' + section.label + '.', () => this._render_custom_section(section));
@@ -999,47 +1032,47 @@ class Admin_Shell extends Active_HTML_Document {
999
1032
  /**
1000
1033
  * Auto-render data based on shape:
1001
1034
  * - 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
- }
1035
+ * - Object → key-value panel
1036
+ * - Scalar → plain text
1037
+ */
1038
+ _render_custom_section_data(section, data) {
1039
+ if (!this._dynamic_section) return;
1040
+
1041
+ if (Array.isArray(data)) {
1042
+ if (data.length === 0) {
1043
+ this._render_empty('No data for ' + section.label + '.');
1044
+ return;
1045
+ }
1046
+ const normalized_first_row = (data[0] && typeof data[0] === 'object')
1047
+ ? data[0]
1048
+ : { value: data[0] };
1049
+ const keys = Object.keys(normalized_first_row);
1050
+ const rows_data = data.map((row) => {
1051
+ const normalized_row = (row && typeof row === 'object') ? row : { value: row };
1052
+ return keys.map((key) => normalized_row[key]);
1053
+ });
1054
+
1055
+ this._clear_control(this._dynamic_section);
1056
+ this._dynamic_section.add(
1057
+ this._create_table_panel(section.label, keys, rows_data)
1058
+ );
1059
+ } else if (data && typeof data === 'object') {
1060
+ const panel = this._create_panel(section.label);
1061
+ Object.entries(data).forEach((entry) => {
1062
+ panel.add(this._create_kv_row(entry[0], entry[1]));
1063
+ });
1064
+ this._clear_control(this._dynamic_section);
1065
+ this._dynamic_section.add(panel);
1066
+ } else {
1067
+ const panel = this._create_panel();
1068
+ const text = new controls.div({ context: this.context, 'class': 'as-muted' });
1069
+ text.add(this._stringify_value(data));
1070
+ panel.add(text);
1071
+ this._clear_control(this._dynamic_section);
1072
+ this._dynamic_section.add(panel);
1073
+ }
1074
+ }
1075
+ }
1043
1076
 
1044
1077
  Admin_Shell.css = `
1045
1078
  /* ─── Reset & Root ───────────────────────────────────────── */