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.
- package/.github/instructions/copilot.instructions.md +1 -0
- package/AGENTS.md +2 -0
- package/README.md +89 -13
- package/admin-ui/v1/controls/admin_shell.js +702 -669
- package/admin-ui/v1/server.js +14 -1
- package/docs/api-reference.md +504 -306
- package/docs/books/creating-a-new-admin-ui/README.md +20 -20
- package/docs/books/website-design/01-introduction.md +73 -0
- package/docs/books/website-design/02-current-state.md +195 -0
- package/docs/books/website-design/03-base-class.md +181 -0
- package/docs/books/website-design/04-webpage.md +307 -0
- package/docs/books/website-design/05-website.md +456 -0
- package/docs/books/website-design/06-pages-storage.md +170 -0
- package/docs/books/website-design/07-api-layer.md +285 -0
- package/docs/books/website-design/08-server-integration.md +271 -0
- package/docs/books/website-design/09-cross-agent-review.md +190 -0
- package/docs/books/website-design/10-open-questions.md +196 -0
- package/docs/books/website-design/11-converged-recommendation.md +205 -0
- package/docs/books/website-design/12-content-model.md +395 -0
- package/docs/books/website-design/13-webpage-module-spec.md +404 -0
- package/docs/books/website-design/14-website-module-spec.md +541 -0
- package/docs/books/website-design/15-multi-repo-plan.md +275 -0
- package/docs/books/website-design/16-minimal-first.md +203 -0
- package/docs/books/website-design/17-implementation-report-codex.md +81 -0
- package/docs/books/website-design/README.md +43 -0
- package/docs/comprehensive-documentation.md +220 -220
- package/docs/configuration-reference.md +281 -204
- package/docs/middleware-guide.md +236 -0
- package/docs/proposals/jsgui3-website-and-webpage-design-jsgui3-server-support.md +257 -0
- package/docs/proposals/jsgui3-website-and-webpage-design-review.md +73 -0
- package/docs/proposals/jsgui3-website-and-webpage-design.md +732 -0
- package/docs/swagger.md +316 -0
- package/docs/system-architecture.md +24 -18
- package/examples/controls/1) window/server.js +6 -1
- package/examples/controls/21) mvvm and declarative api/check.js +94 -0
- package/examples/controls/21) mvvm and declarative api/check_output.txt +25 -0
- package/examples/controls/21) mvvm and declarative api/check_output_2.txt +27 -0
- package/examples/controls/21) mvvm and declarative api/client.js +241 -0
- declarative api/e2e-screenshot-1-name-change.png +0 -0
- declarative api/e2e-screenshot-2-toggled.png +0 -0
- declarative api/e2e-screenshot-3-final.png +0 -0
- declarative api/e2e-screenshot-final.png +0 -0
- package/examples/controls/21) mvvm and declarative api/e2e-test.js +175 -0
- package/examples/controls/21) mvvm and declarative api/out.html +1 -0
- package/examples/controls/21) mvvm and declarative api/page_out.html +1 -0
- package/examples/controls/21) mvvm and declarative api/server.js +18 -0
- package/examples/data-views/01) query-endpoint/server.js +61 -0
- package/labs/website-design/001-base-class-overhead/check.js +162 -0
- package/labs/website-design/002-pages-storage/check.js +244 -0
- package/labs/website-design/002-pages-storage/results.txt +0 -0
- package/labs/website-design/003-type-detection/check.js +193 -0
- package/labs/website-design/003-type-detection/results.txt +0 -0
- package/labs/website-design/004-two-stage-validation/check.js +314 -0
- package/labs/website-design/004-two-stage-validation/results.txt +0 -0
- package/labs/website-design/005-normalize-input/check.js +303 -0
- package/labs/website-design/006-serve-website-spike/check.js +290 -0
- package/labs/website-design/README.md +34 -0
- package/labs/website-design/manifest.json +68 -0
- package/labs/website-design/run-all.js +60 -0
- package/middleware/compression.js +217 -0
- package/middleware/index.js +15 -0
- package/middleware/json-body.js +126 -0
- package/module.js +3 -0
- package/openapi.js +474 -0
- package/package.json +11 -8
- package/publishers/Publishers.js +6 -5
- package/publishers/http-function-publisher.js +135 -126
- package/publishers/http-webpage-publisher.js +89 -11
- package/publishers/query-publisher.js +116 -0
- package/publishers/swagger-publisher.js +203 -0
- package/publishers/swagger-ui.js +578 -0
- package/resources/adapters/array-adapter.js +143 -0
- package/resources/query-resource.js +131 -0
- package/serve-factory.js +756 -18
- package/server.js +502 -123
- package/tests/README.md +23 -1
- package/tests/admin-ui-jsgui-controls.test.js +16 -1
- package/tests/helpers/playwright-e2e-harness.js +326 -0
- package/tests/openapi.test.js +319 -0
- package/tests/playwright-smoke.test.js +134 -0
- package/tests/publish-enhancements.test.js +673 -0
- package/tests/query-publisher.test.js +430 -0
- package/tests/quick-json-body-test.js +169 -0
- package/tests/serve.test.js +425 -122
- package/tests/swagger-publisher.test.js +1076 -0
- 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
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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 ───────────────────────────────────────── */
|