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,581 @@
1
+ const assert = require('assert');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const { describe, it } = require('mocha');
5
+ const { Page_Context } = require('jsgui3-html');
6
+
7
+ const admin_ui = require('../admin-ui/client');
8
+ const admin_ui_v1 = require('../admin-ui/v1/client');
9
+
10
+ const Admin_Page = admin_ui.controls && admin_ui.controls.Admin_Page;
11
+ const Admin_Shell = admin_ui_v1.controls && admin_ui_v1.controls.Admin_Shell;
12
+
13
+ const create_page_context = () => new Page_Context({});
14
+
15
+ const create_admin_page = () => {
16
+ return new Admin_Page({
17
+ context: create_page_context()
18
+ });
19
+ };
20
+
21
+ const create_admin_shell = () => {
22
+ return new Admin_Shell({
23
+ context: create_page_context()
24
+ });
25
+ };
26
+
27
+ const wait_for_turn = () => new Promise((resolve) => setImmediate(resolve));
28
+
29
+ const file_contains_pattern = (file_path, pattern) => {
30
+ const source_text = fs.readFileSync(file_path, 'utf8');
31
+ return pattern.test(source_text);
32
+ };
33
+
34
+ const get_child_controls = (control) => {
35
+ if (!control || !control.content || !Array.isArray(control.content._arr)) {
36
+ return [];
37
+ }
38
+ return control.content._arr;
39
+ };
40
+
41
+ const control_has_class = (control, class_name) => {
42
+ const class_attribute = control && control.dom && control.dom.attributes && control.dom.attributes.class;
43
+ if (!class_attribute) return false;
44
+ return String(class_attribute).split(/\s+/).includes(class_name);
45
+ };
46
+
47
+ const find_control_by_class = (root_control, class_name) => {
48
+ if (!root_control) return null;
49
+ if (control_has_class(root_control, class_name)) {
50
+ return root_control;
51
+ }
52
+ const child_controls = get_child_controls(root_control);
53
+ for (const child_control of child_controls) {
54
+ const found_control = find_control_by_class(child_control, class_name);
55
+ if (found_control) return found_control;
56
+ }
57
+ return null;
58
+ };
59
+
60
+ describe('Admin UI jsgui control integration', function() {
61
+ it('keeps admin UI modules free of direct DOM selection and HTML string patching', () => {
62
+ const files_to_check = [
63
+ path.join(__dirname, '..', 'admin-ui', 'client.js'),
64
+ path.join(__dirname, '..', 'admin-ui', 'v1', 'controls', 'admin_shell.js')
65
+ ];
66
+
67
+ const forbidden_patterns = [
68
+ /\bdocument\./,
69
+ /querySelector/,
70
+ /innerHTML/,
71
+ /createElement/,
72
+ /appendChild/,
73
+ /\.classList\b/
74
+ ];
75
+
76
+ files_to_check.forEach((file_path) => {
77
+ forbidden_patterns.forEach((pattern) => {
78
+ assert.strictEqual(
79
+ file_contains_pattern(file_path, pattern),
80
+ false,
81
+ `Forbidden DOM pattern ${pattern} found in ${file_path}`
82
+ );
83
+ });
84
+ });
85
+ });
86
+
87
+ it('updates Admin_Page active menu state and title through control APIs', () => {
88
+ const admin_page = create_admin_page();
89
+ admin_page._activate_menu_item('resources');
90
+
91
+ const resources_item = admin_page._menu_items.find((item) => item.id === 'resources');
92
+ const overview_item = admin_page._menu_items.find((item) => item.id === 'overview');
93
+
94
+ assert(resources_item && resources_item.control.has_class('active'));
95
+ assert(overview_item && !overview_item.control.has_class('active'));
96
+
97
+ const title_html = admin_page.page_title.all_html_render();
98
+ assert(title_html.includes('Resources'));
99
+ });
100
+
101
+ it('responds to interactive menu click events on Admin_Page controls', () => {
102
+ const admin_page = create_admin_page();
103
+ const settings_item = admin_page._menu_items.find((item) => item.id === 'settings');
104
+ const overview_item = admin_page._menu_items.find((item) => item.id === 'overview');
105
+
106
+ settings_item.control.raise('click');
107
+
108
+ assert(settings_item.control.has_class('active'));
109
+ assert(!overview_item.control.has_class('active'));
110
+ assert.strictEqual(admin_page._active_section, 'settings');
111
+ assert(admin_page.page_title.all_html_render().includes('Settings'));
112
+ });
113
+
114
+ it('renders resources table content using jsgui controls', async () => {
115
+ const admin_shell = create_admin_shell();
116
+ admin_shell._fetch_json = () => Promise.resolve({
117
+ children: [
118
+ { name: 'Resource Alpha', type: 'Publisher', state: 'running' },
119
+ { name: 'Resource Beta', type: 'Resource', state: 'stopped' }
120
+ ]
121
+ });
122
+
123
+ await admin_shell._render_resources_section();
124
+
125
+ const dynamic_html = admin_shell._dynamic_section.all_html_render();
126
+ const status_html = admin_shell._status_text.all_html_render();
127
+
128
+ assert(dynamic_html.includes('Resources'));
129
+ assert(dynamic_html.includes('Resource Alpha'));
130
+ assert(dynamic_html.includes('Publisher'));
131
+ assert(dynamic_html.includes('<table'));
132
+ assert(status_html.includes('Loaded resources: 2'));
133
+ });
134
+
135
+ it('renders routes table content using jsgui controls', async () => {
136
+ const admin_shell = create_admin_shell();
137
+ admin_shell._fetch_json = () => Promise.resolve([
138
+ { path: '/api/one', method: 'GET', type: 'api', handler: 'handler_one' },
139
+ { path: '/api/two', method: 'POST', type: 'api', handler: 'handler_two' }
140
+ ]);
141
+
142
+ await admin_shell._render_routes_section();
143
+
144
+ const dynamic_html = admin_shell._dynamic_section.all_html_render();
145
+ const status_html = admin_shell._status_text.all_html_render();
146
+
147
+ assert(dynamic_html.includes('Routes'));
148
+ assert(dynamic_html.includes('Path'));
149
+ assert(dynamic_html.includes('handler_two'));
150
+ assert(dynamic_html.includes('<table'));
151
+ assert(status_html.includes('Loaded routes: 2'));
152
+ });
153
+
154
+ it('renders settings snapshot with key-value rows and logout button control', async () => {
155
+ const admin_shell = create_admin_shell();
156
+ admin_shell._fetch_json = () => Promise.resolve({
157
+ server: { name: 'Example Server' },
158
+ process: {
159
+ node_version: 'v22.0.0',
160
+ platform: 'linux',
161
+ arch: 'x64'
162
+ }
163
+ });
164
+
165
+ await admin_shell._render_settings_section();
166
+
167
+ const dynamic_html = admin_shell._dynamic_section.all_html_render();
168
+ assert(dynamic_html.includes('Settings (Read-only)'));
169
+ assert(dynamic_html.includes('Example Server'));
170
+ assert(dynamic_html.includes('v22.0.0'));
171
+ assert(dynamic_html.includes('as-logout-btn'));
172
+ });
173
+
174
+ it('renders custom section data for array, object, and scalar payloads', () => {
175
+ const admin_shell = create_admin_shell();
176
+ const section = { id: 'custom_data', label: 'Custom Data' };
177
+
178
+ admin_shell._render_custom_section_data(section, [
179
+ { key: 'alpha', value: 1 },
180
+ { key: 'beta', value: 2 }
181
+ ]);
182
+ let dynamic_html = admin_shell._dynamic_section.all_html_render();
183
+ assert(dynamic_html.includes('<table'));
184
+ assert(dynamic_html.includes('alpha'));
185
+ assert(dynamic_html.includes('value'));
186
+
187
+ admin_shell._render_custom_section_data(section, {
188
+ enabled: true,
189
+ retries: 3
190
+ });
191
+ dynamic_html = admin_shell._dynamic_section.all_html_render();
192
+ assert(dynamic_html.includes('enabled'));
193
+ assert(dynamic_html.includes('retries'));
194
+ assert(dynamic_html.includes('as-kv-row'));
195
+
196
+ admin_shell._render_custom_section_data(section, 'plain text');
197
+ dynamic_html = admin_shell._dynamic_section.all_html_render();
198
+ assert(dynamic_html.includes('plain text'));
199
+ assert(dynamic_html.includes('as-muted'));
200
+ });
201
+
202
+ it('loads custom sections into sidebar with separator and custom metadata', async () => {
203
+ const admin_shell = create_admin_shell();
204
+ admin_shell._fetch_json = () => Promise.resolve([
205
+ { id: 'logs', label: 'Logs', icon: 'L', api_path: '/api/admin/v1/logs' },
206
+ { id: 'jobs', label: 'Jobs', icon: 'J', api_path: '/api/admin/v1/jobs' }
207
+ ]);
208
+
209
+ await admin_shell._load_custom_sections();
210
+
211
+ const nav_html = admin_shell._nav.all_html_render();
212
+ assert.strictEqual(admin_shell._custom_nav_items.length, 2);
213
+ assert(admin_shell._section_labels['custom:logs']);
214
+ assert(admin_shell._section_labels['custom:jobs']);
215
+ assert(nav_html.includes('as-nav-separator'));
216
+ assert(nav_html.includes('L Logs'));
217
+ assert(nav_html.includes('J Jobs'));
218
+ });
219
+
220
+ it('syncs nav and tab state when a nav item click is raised', async () => {
221
+ const admin_shell = create_admin_shell();
222
+ let selected_section = null;
223
+ admin_shell._select_section = (section_id) => {
224
+ selected_section = section_id;
225
+ return Promise.resolve();
226
+ };
227
+
228
+ admin_shell._sidebar.add_class('as-sidebar-open');
229
+ admin_shell._overlay.add_class('active');
230
+
231
+ const resources_nav = admin_shell._nav_items.find((item) => item.id === 'resources');
232
+ const resources_tab = admin_shell._tab_items.find((item) => item.id === 'resources');
233
+ const dashboard_tab = admin_shell._tab_items.find((item) => item.id === 'dashboard');
234
+
235
+ resources_nav.control.raise('click');
236
+ await wait_for_turn();
237
+
238
+ assert.strictEqual(selected_section, 'resources');
239
+ assert(resources_nav.control.has_class('active'));
240
+ assert(resources_tab.control.has_class('active'));
241
+ assert(!dashboard_tab.control.has_class('active'));
242
+ assert(admin_shell._page_title.all_html_render().includes('Resources'));
243
+ assert(!admin_shell._sidebar.has_class('as-sidebar-open'));
244
+ assert(!admin_shell._overlay.has_class('active'));
245
+ });
246
+
247
+ it('syncs nav and tab state when a tab item click is raised', async () => {
248
+ const admin_shell = create_admin_shell();
249
+ let selected_section = null;
250
+ admin_shell._select_section = (section_id) => {
251
+ selected_section = section_id;
252
+ return Promise.resolve();
253
+ };
254
+
255
+ const routes_tab = admin_shell._tab_items.find((item) => item.id === 'routes');
256
+ const routes_nav = admin_shell._nav_items.find((item) => item.id === 'routes');
257
+ const dashboard_nav = admin_shell._nav_items.find((item) => item.id === 'dashboard');
258
+
259
+ routes_tab.control.raise('click');
260
+ await wait_for_turn();
261
+
262
+ assert.strictEqual(selected_section, 'routes');
263
+ assert(routes_tab.control.has_class('active'));
264
+ assert(routes_nav.control.has_class('active'));
265
+ assert(!dashboard_nav.control.has_class('active'));
266
+ assert(admin_shell._page_title.all_html_render().includes('Routes'));
267
+ });
268
+
269
+ it('opens and closes the sidebar through hamburger and overlay interactions', () => {
270
+ const admin_shell = create_admin_shell();
271
+
272
+ assert(!admin_shell._sidebar.has_class('as-sidebar-open'));
273
+ assert(!admin_shell._overlay.has_class('active'));
274
+
275
+ admin_shell._hamburger.raise('click');
276
+ assert(admin_shell._sidebar.has_class('as-sidebar-open'));
277
+ assert(admin_shell._overlay.has_class('active'));
278
+
279
+ admin_shell._overlay.raise('click');
280
+ assert(!admin_shell._sidebar.has_class('as-sidebar-open'));
281
+ assert(!admin_shell._overlay.has_class('active'));
282
+ });
283
+
284
+ it('activates custom nav item interaction and clears bottom tabs', async () => {
285
+ const admin_shell = create_admin_shell();
286
+ admin_shell._fetch_json = () => Promise.resolve([
287
+ { id: 'logs', label: 'Logs', icon: 'L', api_path: '/api/admin/v1/logs' }
288
+ ]);
289
+
290
+ await admin_shell._load_custom_sections();
291
+
292
+ let selected_section = null;
293
+ admin_shell._select_section = (section_id) => {
294
+ selected_section = section_id;
295
+ return Promise.resolve();
296
+ };
297
+
298
+ const custom_nav_item = admin_shell._custom_nav_items[0];
299
+ custom_nav_item.control.raise('click');
300
+ await wait_for_turn();
301
+
302
+ assert.strictEqual(selected_section, 'custom:logs');
303
+ assert(custom_nav_item.control.has_class('active'));
304
+ assert(admin_shell._page_title.all_html_render().includes('Logs'));
305
+ admin_shell._tab_items.forEach((tab_item) => {
306
+ assert(!tab_item.control.has_class('active'));
307
+ });
308
+ });
309
+
310
+ it('fires retry interaction from rendered error panel button', () => {
311
+ const admin_shell = create_admin_shell();
312
+ let retry_count = 0;
313
+
314
+ admin_shell._render_error('Load failed', () => {
315
+ retry_count += 1;
316
+ });
317
+
318
+ const retry_button = find_control_by_class(admin_shell._dynamic_section, 'as-retry-btn');
319
+ assert(retry_button, 'Expected retry button control in error panel');
320
+
321
+ retry_button.raise('click');
322
+ assert.strictEqual(retry_count, 1);
323
+ });
324
+
325
+ it('fires logout interaction and redirect through settings panel button', async () => {
326
+ const admin_shell = create_admin_shell();
327
+ admin_shell._fetch_json = () => Promise.resolve({
328
+ server: { name: 'Example Server' },
329
+ process: { node_version: 'v22.0.0', platform: 'linux', arch: 'x64' }
330
+ });
331
+
332
+ const original_fetch = global.fetch;
333
+ let logout_request = null;
334
+ global.fetch = (url, options) => {
335
+ logout_request = { url, options };
336
+ return Promise.resolve({ ok: true });
337
+ };
338
+
339
+ let redirect_count = 0;
340
+ admin_shell._redirect_to_login = () => {
341
+ redirect_count += 1;
342
+ };
343
+
344
+ try {
345
+ await admin_shell._render_settings_section();
346
+
347
+ const logout_button = find_control_by_class(admin_shell._dynamic_section, 'as-logout-btn');
348
+ assert(logout_button, 'Expected logout button control in settings panel');
349
+
350
+ logout_button.raise('click');
351
+ await wait_for_turn();
352
+
353
+ assert(logout_request);
354
+ assert.strictEqual(logout_request.url, '/api/admin/v1/auth/logout');
355
+ assert.strictEqual(logout_request.options.method, 'POST');
356
+ assert.strictEqual(logout_request.options.credentials, 'same-origin');
357
+ assert.strictEqual(redirect_count, 1);
358
+ } finally {
359
+ global.fetch = original_fetch;
360
+ }
361
+ });
362
+
363
+ it('updates layout mode from window width and stores mode attribute', () => {
364
+ const admin_shell = create_admin_shell();
365
+ const original_window = global.window;
366
+ global.window = { innerWidth: 460 };
367
+
368
+ try {
369
+ admin_shell._update_layout_mode();
370
+ assert.strictEqual(admin_shell._layout_mode, 'phone');
371
+ assert.strictEqual(admin_shell.body.dom.attributes['data-layout-mode'], 'phone');
372
+
373
+ global.window.innerWidth = 700;
374
+ admin_shell._update_layout_mode();
375
+ assert.strictEqual(admin_shell._layout_mode, 'tablet');
376
+ assert.strictEqual(admin_shell.body.dom.attributes['data-layout-mode'], 'tablet');
377
+
378
+ global.window.innerWidth = 1200;
379
+ admin_shell._update_layout_mode();
380
+ assert.strictEqual(admin_shell._layout_mode, 'desktop');
381
+ assert.strictEqual(admin_shell.body.dom.attributes['data-layout-mode'], 'desktop');
382
+ } finally {
383
+ global.window = original_window;
384
+ }
385
+ });
386
+
387
+ it('runs activate startup interactions only once and wires resize listener', () => {
388
+ const admin_shell = create_admin_shell();
389
+
390
+ let status_calls = 0;
391
+ let sse_calls = 0;
392
+ let custom_calls = 0;
393
+ admin_shell._fetch_status = () => {
394
+ status_calls += 1;
395
+ return Promise.resolve();
396
+ };
397
+ admin_shell._connect_sse = () => {
398
+ sse_calls += 1;
399
+ };
400
+ admin_shell._load_custom_sections = () => {
401
+ custom_calls += 1;
402
+ return Promise.resolve();
403
+ };
404
+
405
+ const resize_listener_calls = [];
406
+ const original_window = global.window;
407
+ global.window = {
408
+ innerWidth: 1024,
409
+ addEventListener: (event_name, handler) => {
410
+ resize_listener_calls.push({ event_name, handler });
411
+ }
412
+ };
413
+
414
+ try {
415
+ admin_shell.activate();
416
+ admin_shell.activate();
417
+
418
+ assert.strictEqual(status_calls, 1);
419
+ assert.strictEqual(sse_calls, 1);
420
+ assert.strictEqual(custom_calls, 1);
421
+ assert.strictEqual(resize_listener_calls.length, 1);
422
+ assert.strictEqual(resize_listener_calls[0].event_name, 'resize');
423
+ } finally {
424
+ global.window = original_window;
425
+ }
426
+ });
427
+
428
+ it('handles interactive SSE open, heartbeat, and error flows', () => {
429
+ const admin_shell = create_admin_shell();
430
+ const original_event_source = global.EventSource;
431
+ const original_set_timeout = global.setTimeout;
432
+
433
+ const sse_instances = [];
434
+ const timeout_calls = [];
435
+ let heartbeat_payload = null;
436
+ admin_shell._apply_heartbeat = (payload) => {
437
+ heartbeat_payload = payload;
438
+ };
439
+
440
+ class Fake_Event_Source {
441
+ constructor(url) {
442
+ this.url = url;
443
+ this.listeners = Object.create(null);
444
+ this.closed = false;
445
+ sse_instances.push(this);
446
+ }
447
+
448
+ addEventListener(event_name, handler) {
449
+ this.listeners[event_name] = handler;
450
+ }
451
+
452
+ close() {
453
+ this.closed = true;
454
+ }
455
+
456
+ emit(event_name, event_data) {
457
+ if (typeof this.listeners[event_name] === 'function') {
458
+ this.listeners[event_name](event_data);
459
+ }
460
+ }
461
+ }
462
+
463
+ global.EventSource = Fake_Event_Source;
464
+ global.setTimeout = (handler, delay) => {
465
+ timeout_calls.push({ handler, delay });
466
+ return timeout_calls.length;
467
+ };
468
+
469
+ try {
470
+ admin_shell._connect_sse();
471
+ assert.strictEqual(sse_instances.length, 1);
472
+ assert.strictEqual(sse_instances[0].url, '/api/admin/v1/events');
473
+
474
+ sse_instances[0].emit('open', {});
475
+ assert(admin_shell._status_dot.has_class('online'));
476
+ assert(!admin_shell._status_dot.has_class('offline'));
477
+ assert(admin_shell._status_text.all_html_render().includes('SSE connected'));
478
+
479
+ sse_instances[0].emit('heartbeat', {
480
+ data: JSON.stringify({
481
+ uptime: 360,
482
+ pid: 3001,
483
+ memory: {
484
+ heap_used: 1024 * 1024 * 8,
485
+ heap_total: 1024 * 1024 * 16,
486
+ rss: 1024 * 1024 * 24
487
+ },
488
+ request_count: 11,
489
+ requests_per_minute: 4,
490
+ pool_summary: { total: 6, running: 5 },
491
+ route_count: 9
492
+ })
493
+ });
494
+
495
+ assert(heartbeat_payload);
496
+ assert.strictEqual(heartbeat_payload.request_count, 11);
497
+ assert.strictEqual(heartbeat_payload.route_count, 9);
498
+
499
+ sse_instances[0].emit('error', {});
500
+ assert(admin_shell._status_dot.has_class('offline'));
501
+ assert(!admin_shell._status_dot.has_class('online'));
502
+ assert.strictEqual(timeout_calls.length, 1);
503
+ assert.strictEqual(timeout_calls[0].delay, 1000);
504
+ assert.strictEqual(admin_shell._sse_backoff, 2000);
505
+ assert.strictEqual(admin_shell._event_source, null);
506
+ assert(sse_instances[0].closed);
507
+
508
+ timeout_calls[0].handler();
509
+ assert.strictEqual(sse_instances.length, 2);
510
+ } finally {
511
+ global.EventSource = original_event_source;
512
+ global.setTimeout = original_set_timeout;
513
+ }
514
+ });
515
+
516
+ it('redirects on unauthorized fetch_json responses', async () => {
517
+ const admin_shell = create_admin_shell();
518
+ const original_fetch = global.fetch;
519
+ let redirect_count = 0;
520
+
521
+ global.fetch = () => Promise.resolve({
522
+ ok: false,
523
+ status: 401
524
+ });
525
+ admin_shell._redirect_to_login = () => {
526
+ redirect_count += 1;
527
+ };
528
+
529
+ try {
530
+ await assert.rejects(
531
+ () => admin_shell._fetch_json('/api/admin/v1/status'),
532
+ /Unauthorized/
533
+ );
534
+ assert.strictEqual(redirect_count, 1);
535
+ } finally {
536
+ global.fetch = original_fetch;
537
+ }
538
+ });
539
+
540
+ it('replaces previously loaded custom sections on refresh', async () => {
541
+ const admin_shell = create_admin_shell();
542
+ let call_index = 0;
543
+ const responses = [
544
+ [{ id: 'logs', label: 'Logs', icon: 'L', api_path: '/api/admin/v1/logs' }],
545
+ [{ id: 'metrics', label: 'Metrics', icon: 'M', api_path: '/api/admin/v1/metrics' }]
546
+ ];
547
+
548
+ admin_shell._fetch_json = () => {
549
+ const response = responses[Math.min(call_index, responses.length - 1)];
550
+ call_index += 1;
551
+ return Promise.resolve(response);
552
+ };
553
+
554
+ await admin_shell._load_custom_sections();
555
+ await admin_shell._load_custom_sections();
556
+
557
+ const nav_html = admin_shell._nav.all_html_render();
558
+ assert.strictEqual(admin_shell._custom_nav_items.length, 1);
559
+ assert(nav_html.includes('M Metrics'));
560
+ assert(!nav_html.includes('L Logs'));
561
+ });
562
+
563
+ it('toggles dashboard and dynamic sections through section selection logic', async () => {
564
+ const admin_shell = create_admin_shell();
565
+
566
+ let resources_called = 0;
567
+ admin_shell._render_resources_section = () => {
568
+ resources_called += 1;
569
+ return Promise.resolve();
570
+ };
571
+
572
+ await admin_shell._select_section('resources');
573
+ assert.strictEqual(resources_called, 1);
574
+ assert(admin_shell._dashboard_section.has_class('hidden'));
575
+ assert(!admin_shell._dynamic_section.has_class('hidden'));
576
+
577
+ await admin_shell._select_section('dashboard');
578
+ assert(!admin_shell._dashboard_section.has_class('hidden'));
579
+ assert(admin_shell._dynamic_section.has_class('hidden'));
580
+ });
581
+ });
@@ -0,0 +1,24 @@
1
+ const assert = require('assert');
2
+ const { describe, it } = require('mocha');
3
+ const { Page_Context } = require('jsgui3-html');
4
+
5
+ const admin_ui = require('../admin-ui/client');
6
+
7
+ describe('Admin UI rendering', function() {
8
+ it('renders Admin_Page without overriding core content collection', () => {
9
+ const Admin_Page = admin_ui.controls && admin_ui.controls.Admin_Page;
10
+ assert.strictEqual(typeof Admin_Page, 'function', 'Expected Admin_Page control export');
11
+
12
+ const page_context = new Page_Context({});
13
+ const admin_page = new Admin_Page({
14
+ context: page_context
15
+ });
16
+
17
+ assert(admin_page.content && typeof admin_page.content.length === 'function', 'Expected control content collection to remain intact');
18
+
19
+ const html_output = admin_page.all_html_render();
20
+ assert.strictEqual(typeof html_output, 'string');
21
+ assert(html_output.includes('admin-content'));
22
+ assert(html_output.includes('jsgui3 Admin'));
23
+ });
24
+ });