jsgui3-server 0.0.150 → 0.0.151
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 +68 -13
- package/admin-ui/v1/controls/admin_shell.js +669 -669
- package/docs/api-reference.md +383 -303
- package/docs/books/creating-a-new-admin-ui/README.md +20 -20
- package/docs/comprehensive-documentation.md +220 -220
- package/docs/configuration-reference.md +227 -204
- package/docs/middleware-guide.md +236 -0
- package/docs/system-architecture.md +24 -18
- package/middleware/compression.js +217 -0
- package/middleware/index.js +15 -0
- package/module.js +3 -0
- package/package.json +1 -1
- package/serve-factory.js +28 -0
- package/server.js +81 -20
|
@@ -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,437 @@ class Admin_Shell extends Active_HTML_Document {
|
|
|
258
258
|
const telemetry_group = new Group_Box({ context, title: 'Telemetry', 'class': 'group-box' });
|
|
259
259
|
container.add(telemetry_group);
|
|
260
260
|
|
|
261
|
-
this._card_requests = new Stat_Card({
|
|
262
|
-
context, label: 'Requests', value: '0', detail: '0 req/min', accent: '#f093fb', 'class': 'stat_card'
|
|
263
|
-
});
|
|
264
|
-
telemetry_group.inner.add(this._card_requests);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// ─── Client-Side Activation ──────────────────────────────
|
|
268
|
-
|
|
269
|
-
activate() {
|
|
270
|
-
if (!this.__active) {
|
|
271
|
-
super.activate();
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (!this._client_bootstrapped) {
|
|
275
|
-
this._client_bootstrapped = true;
|
|
276
|
-
this._update_layout_mode();
|
|
277
|
-
if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') {
|
|
278
|
-
if (!this._bound_resize_handler) {
|
|
279
|
-
this._bound_resize_handler = () => this._update_layout_mode();
|
|
280
|
-
}
|
|
281
|
-
window.addEventListener('resize', this._bound_resize_handler);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
this._fetch_status();
|
|
285
|
-
this._connect_sse();
|
|
286
|
-
this._load_custom_sections();
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
_update_layout_mode() {
|
|
291
|
-
const has_window = typeof window !== 'undefined';
|
|
292
|
-
const w = has_window && typeof window.innerWidth === 'number'
|
|
293
|
-
? window.innerWidth
|
|
294
|
-
: 1024;
|
|
295
|
-
let mode = 'desktop';
|
|
296
|
-
if (w <= 480) mode = 'phone';
|
|
297
|
-
else if (w <= 768) mode = 'tablet';
|
|
298
|
-
if (this.body && this.body.dom && this.body.dom.attributes) {
|
|
299
|
-
this.body.dom.attributes['data-layout-mode'] = mode;
|
|
300
|
-
}
|
|
301
|
-
if (this.body && this.body.el && typeof this.body.el.setAttribute === 'function') {
|
|
302
|
-
this.body.el.setAttribute('data-layout-mode', mode);
|
|
303
|
-
}
|
|
304
|
-
this._layout_mode = mode;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
_toggle_sidebar() {
|
|
308
|
-
const sidebar = this._sidebar;
|
|
309
|
-
const overlay = this._overlay;
|
|
310
|
-
if (!sidebar) return;
|
|
311
|
-
const is_open = typeof sidebar.has_class === 'function' && sidebar.has_class('as-sidebar-open');
|
|
312
|
-
if (is_open) {
|
|
313
|
-
this._close_sidebar();
|
|
314
|
-
} else {
|
|
315
|
-
if (typeof sidebar.add_class === 'function') {
|
|
316
|
-
sidebar.add_class('as-sidebar-open');
|
|
317
|
-
}
|
|
318
|
-
if (overlay && typeof overlay.add_class === 'function') {
|
|
319
|
-
overlay.add_class('active');
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
_close_sidebar() {
|
|
325
|
-
if (this._sidebar && typeof this._sidebar.remove_class === 'function') {
|
|
326
|
-
this._sidebar.remove_class('as-sidebar-open');
|
|
327
|
-
}
|
|
328
|
-
if (this._overlay && typeof this._overlay.remove_class === 'function') {
|
|
329
|
-
this._overlay.remove_class('active');
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
_activate_section_from_nav(section_id, section_label) {
|
|
334
|
-
this._set_active_nav(section_id);
|
|
335
|
-
if (String(section_id).indexOf('custom:') === 0) {
|
|
336
|
-
this._set_active_tab(null);
|
|
337
|
-
} else {
|
|
338
|
-
this._set_active_tab(section_id);
|
|
339
|
-
}
|
|
340
|
-
this._set_page_title(section_label || this._get_section_label(section_id));
|
|
341
|
-
this._close_sidebar();
|
|
342
|
-
this._select_section(section_id);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
_activate_section_from_tab(section_id, section_label) {
|
|
346
|
-
this._set_active_tab(section_id);
|
|
347
|
-
this._set_active_nav(section_id);
|
|
348
|
-
this._set_page_title(section_label || this._get_section_label(section_id));
|
|
349
|
-
this._select_section(section_id);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
_set_active_nav(section_id) {
|
|
353
|
-
this._nav_items.forEach((nav_item) => {
|
|
354
|
-
if (!nav_item.control) return;
|
|
355
|
-
if (nav_item.id === section_id) {
|
|
356
|
-
nav_item.control.add_class('active');
|
|
357
|
-
} else {
|
|
358
|
-
nav_item.control.remove_class('active');
|
|
359
|
-
}
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
_set_active_tab(section_id) {
|
|
364
|
-
this._tab_items.forEach((tab_item) => {
|
|
365
|
-
if (!tab_item.control) return;
|
|
366
|
-
if (section_id != null && tab_item.id === section_id) {
|
|
367
|
-
tab_item.control.add_class('active');
|
|
368
|
-
} else {
|
|
369
|
-
tab_item.control.remove_class('active');
|
|
370
|
-
}
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
_get_section_label(section_id) {
|
|
375
|
-
return this._section_labels[section_id] || section_id;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
_set_page_title(text) {
|
|
379
|
-
this._set_control_text(this._page_title, text);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
_select_section(section) {
|
|
383
|
-
this._active_section = section;
|
|
384
|
-
|
|
385
|
-
const dashboard_section = this._dashboard_section;
|
|
386
|
-
const dynamic_section = this._dynamic_section;
|
|
387
|
-
if (!dashboard_section || !dynamic_section) return Promise.resolve();
|
|
388
|
-
|
|
389
|
-
if (section === 'dashboard') {
|
|
390
|
-
dashboard_section.remove_class('hidden');
|
|
391
|
-
dynamic_section.add_class('hidden');
|
|
392
|
-
this._set_status_text('Dashboard view');
|
|
393
|
-
return Promise.resolve();
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
dashboard_section.add_class('hidden');
|
|
397
|
-
dynamic_section.remove_class('hidden');
|
|
398
|
-
|
|
399
|
-
if (section === 'resources') {
|
|
400
|
-
return this._render_resources_section();
|
|
401
|
-
} else if (section === 'routes') {
|
|
402
|
-
return this._render_routes_section();
|
|
403
|
-
} else if (section === 'settings') {
|
|
404
|
-
return this._render_settings_section();
|
|
405
|
-
} else if (section.startsWith('custom:')) {
|
|
406
|
-
const custom_id = section.substring(7);
|
|
407
|
-
const custom = this._custom_sections_list && this._custom_sections_list.find(s => s.id === custom_id);
|
|
408
|
-
if (custom) {
|
|
409
|
-
return this._render_custom_section(custom);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
return Promise.resolve();
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
_set_control_text(control, text) {
|
|
416
|
-
if (!control) return;
|
|
417
|
-
if (typeof control.clear === 'function') {
|
|
418
|
-
control.clear();
|
|
419
|
-
}
|
|
420
|
-
if (typeof control.add === 'function') {
|
|
421
|
-
control.add(String(text == null ? '' : text));
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
_clear_control(control) {
|
|
426
|
-
if (control && typeof control.clear === 'function') {
|
|
427
|
-
control.clear();
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
_render_loading(message) {
|
|
432
|
-
if (!this._dynamic_section) return;
|
|
433
|
-
this._clear_control(this._dynamic_section);
|
|
434
|
-
const panel = this._create_panel();
|
|
435
|
-
const text = new controls.div({ context: this.context, 'class': 'as-muted' });
|
|
436
|
-
text.add(String(message));
|
|
437
|
-
panel.add(text);
|
|
438
|
-
this._dynamic_section.add(panel);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
_render_error(message, retry_handler) {
|
|
442
|
-
if (!this._dynamic_section) return;
|
|
443
|
-
this._clear_control(this._dynamic_section);
|
|
444
|
-
|
|
445
|
-
const panel = this._create_panel();
|
|
446
|
-
const text = new controls.div({ context: this.context, 'class': 'as-error' });
|
|
447
|
-
text.add(String(message));
|
|
448
|
-
panel.add(text);
|
|
449
|
-
|
|
450
|
-
const retry_btn = new controls.button({ context: this.context, 'class': 'as-retry-btn' });
|
|
451
|
-
retry_btn.dom.attributes.type = 'button';
|
|
452
|
-
retry_btn.add('Retry');
|
|
453
|
-
if (typeof retry_handler === 'function') {
|
|
454
|
-
retry_btn.on('click', retry_handler);
|
|
455
|
-
}
|
|
456
|
-
panel.add(retry_btn);
|
|
457
|
-
|
|
458
|
-
this._dynamic_section.add(panel);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
_render_empty(message) {
|
|
462
|
-
if (!this._dynamic_section) return;
|
|
463
|
-
this._clear_control(this._dynamic_section);
|
|
464
|
-
|
|
465
|
-
const panel = this._create_panel();
|
|
466
|
-
const text = new controls.div({ context: this.context, 'class': 'as-muted' });
|
|
467
|
-
text.add(String(message));
|
|
468
|
-
panel.add(text);
|
|
469
|
-
this._dynamic_section.add(panel);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
_fetch_json(url) {
|
|
473
|
-
return fetch(url).then((res) => {
|
|
474
|
-
if (!res.ok) {
|
|
475
|
-
if (res.status === 401) {
|
|
476
|
-
this._redirect_to_login();
|
|
477
|
-
throw new Error('Unauthorized');
|
|
478
|
-
}
|
|
479
|
-
throw new Error('HTTP ' + res.status + ' for ' + url);
|
|
480
|
-
}
|
|
481
|
-
return res.json();
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
_create_tag_control(tag_name, class_name) {
|
|
486
|
-
const spec = {
|
|
487
|
-
context: this.context,
|
|
488
|
-
tagName: tag_name
|
|
489
|
-
};
|
|
490
|
-
if (class_name) {
|
|
491
|
-
spec.class = class_name;
|
|
492
|
-
}
|
|
493
|
-
return new Control(spec);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
_create_panel(panel_title) {
|
|
497
|
-
const panel = new controls.div({ context: this.context, 'class': 'as-panel' });
|
|
498
|
-
if (panel_title) {
|
|
499
|
-
const title = new controls.h3({ context: this.context, 'class': 'as-panel-title' });
|
|
500
|
-
title.add(String(panel_title));
|
|
501
|
-
panel.add(title);
|
|
502
|
-
}
|
|
503
|
-
return panel;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
_stringify_value(value) {
|
|
507
|
-
if (value == null) return '';
|
|
508
|
-
if (typeof value === 'object') {
|
|
509
|
-
try {
|
|
510
|
-
return JSON.stringify(value);
|
|
511
|
-
} catch (err) {
|
|
512
|
-
return String(value);
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
return String(value);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
_create_table_panel(panel_title, column_names, rows_data) {
|
|
519
|
-
const panel = this._create_panel(panel_title);
|
|
520
|
-
|
|
521
|
-
const table = this._create_tag_control('table', 'as-table');
|
|
522
|
-
const thead = this._create_tag_control('thead');
|
|
523
|
-
const header_row = this._create_tag_control('tr');
|
|
524
|
-
|
|
525
|
-
column_names.forEach((column_name) => {
|
|
526
|
-
const th = this._create_tag_control('th');
|
|
527
|
-
th.add(String(column_name));
|
|
528
|
-
header_row.add(th);
|
|
529
|
-
});
|
|
530
|
-
thead.add(header_row);
|
|
531
|
-
table.add(thead);
|
|
532
|
-
|
|
533
|
-
const tbody = this._create_tag_control('tbody');
|
|
534
|
-
rows_data.forEach((row_data) => {
|
|
535
|
-
const row = this._create_tag_control('tr');
|
|
536
|
-
row_data.forEach((cell_value) => {
|
|
537
|
-
const td = this._create_tag_control('td');
|
|
538
|
-
td.add(this._stringify_value(cell_value));
|
|
539
|
-
row.add(td);
|
|
540
|
-
});
|
|
541
|
-
tbody.add(row);
|
|
542
|
-
});
|
|
543
|
-
table.add(tbody);
|
|
544
|
-
|
|
545
|
-
panel.add(table);
|
|
546
|
-
return panel;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
_create_kv_row(key_text, value_text) {
|
|
550
|
-
const row = new controls.div({ context: this.context, 'class': 'as-kv-row' });
|
|
551
|
-
const key = new controls.span({ context: this.context, 'class': 'as-kv-key' });
|
|
552
|
-
key.add(String(key_text));
|
|
553
|
-
const value = new controls.span({ context: this.context, 'class': 'as-kv-value' });
|
|
554
|
-
value.add(this._stringify_value(value_text));
|
|
555
|
-
row.add(key);
|
|
556
|
-
row.add(value);
|
|
557
|
-
return row;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
_render_resources_section() {
|
|
561
|
-
this._render_loading('Loading resources...');
|
|
562
|
-
this._set_status_text('Loading resources');
|
|
563
|
-
|
|
564
|
-
return this._fetch_json('/api/admin/v1/resources')
|
|
565
|
-
.then((data) => {
|
|
566
|
-
if (!this._dynamic_section) return;
|
|
567
|
-
|
|
568
|
-
const children = (data && data.children) || [];
|
|
569
|
-
if (children.length === 0) {
|
|
570
|
-
this._render_empty('No resources found.');
|
|
571
|
-
return;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
const rows_data = children.map((resource_item) => {
|
|
575
|
-
return [
|
|
576
|
-
resource_item.name || 'Unnamed',
|
|
577
|
-
resource_item.type || 'Resource',
|
|
578
|
-
resource_item.state || 'unknown'
|
|
579
|
-
];
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
this._clear_control(this._dynamic_section);
|
|
583
|
-
this._dynamic_section.add(
|
|
584
|
-
this._create_table_panel('Resources', ['Name', 'Type', 'State'], rows_data)
|
|
585
|
-
);
|
|
586
|
-
|
|
587
|
-
this._set_status_text('Loaded resources: ' + children.length);
|
|
588
|
-
})
|
|
589
|
-
.catch((err) => {
|
|
590
|
-
console.error('[Admin] Failed to load resources:', err);
|
|
261
|
+
this._card_requests = new Stat_Card({
|
|
262
|
+
context, label: 'Requests', value: '0', detail: '0 req/min', accent: '#f093fb', 'class': 'stat_card'
|
|
263
|
+
});
|
|
264
|
+
telemetry_group.inner.add(this._card_requests);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ─── Client-Side Activation ──────────────────────────────
|
|
268
|
+
|
|
269
|
+
activate() {
|
|
270
|
+
if (!this.__active) {
|
|
271
|
+
super.activate();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!this._client_bootstrapped) {
|
|
275
|
+
this._client_bootstrapped = true;
|
|
276
|
+
this._update_layout_mode();
|
|
277
|
+
if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') {
|
|
278
|
+
if (!this._bound_resize_handler) {
|
|
279
|
+
this._bound_resize_handler = () => this._update_layout_mode();
|
|
280
|
+
}
|
|
281
|
+
window.addEventListener('resize', this._bound_resize_handler);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
this._fetch_status();
|
|
285
|
+
this._connect_sse();
|
|
286
|
+
this._load_custom_sections();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
_update_layout_mode() {
|
|
291
|
+
const has_window = typeof window !== 'undefined';
|
|
292
|
+
const w = has_window && typeof window.innerWidth === 'number'
|
|
293
|
+
? window.innerWidth
|
|
294
|
+
: 1024;
|
|
295
|
+
let mode = 'desktop';
|
|
296
|
+
if (w <= 480) mode = 'phone';
|
|
297
|
+
else if (w <= 768) mode = 'tablet';
|
|
298
|
+
if (this.body && this.body.dom && this.body.dom.attributes) {
|
|
299
|
+
this.body.dom.attributes['data-layout-mode'] = mode;
|
|
300
|
+
}
|
|
301
|
+
if (this.body && this.body.el && typeof this.body.el.setAttribute === 'function') {
|
|
302
|
+
this.body.el.setAttribute('data-layout-mode', mode);
|
|
303
|
+
}
|
|
304
|
+
this._layout_mode = mode;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
_toggle_sidebar() {
|
|
308
|
+
const sidebar = this._sidebar;
|
|
309
|
+
const overlay = this._overlay;
|
|
310
|
+
if (!sidebar) return;
|
|
311
|
+
const is_open = typeof sidebar.has_class === 'function' && sidebar.has_class('as-sidebar-open');
|
|
312
|
+
if (is_open) {
|
|
313
|
+
this._close_sidebar();
|
|
314
|
+
} else {
|
|
315
|
+
if (typeof sidebar.add_class === 'function') {
|
|
316
|
+
sidebar.add_class('as-sidebar-open');
|
|
317
|
+
}
|
|
318
|
+
if (overlay && typeof overlay.add_class === 'function') {
|
|
319
|
+
overlay.add_class('active');
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
_close_sidebar() {
|
|
325
|
+
if (this._sidebar && typeof this._sidebar.remove_class === 'function') {
|
|
326
|
+
this._sidebar.remove_class('as-sidebar-open');
|
|
327
|
+
}
|
|
328
|
+
if (this._overlay && typeof this._overlay.remove_class === 'function') {
|
|
329
|
+
this._overlay.remove_class('active');
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
_activate_section_from_nav(section_id, section_label) {
|
|
334
|
+
this._set_active_nav(section_id);
|
|
335
|
+
if (String(section_id).indexOf('custom:') === 0) {
|
|
336
|
+
this._set_active_tab(null);
|
|
337
|
+
} else {
|
|
338
|
+
this._set_active_tab(section_id);
|
|
339
|
+
}
|
|
340
|
+
this._set_page_title(section_label || this._get_section_label(section_id));
|
|
341
|
+
this._close_sidebar();
|
|
342
|
+
this._select_section(section_id);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
_activate_section_from_tab(section_id, section_label) {
|
|
346
|
+
this._set_active_tab(section_id);
|
|
347
|
+
this._set_active_nav(section_id);
|
|
348
|
+
this._set_page_title(section_label || this._get_section_label(section_id));
|
|
349
|
+
this._select_section(section_id);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
_set_active_nav(section_id) {
|
|
353
|
+
this._nav_items.forEach((nav_item) => {
|
|
354
|
+
if (!nav_item.control) return;
|
|
355
|
+
if (nav_item.id === section_id) {
|
|
356
|
+
nav_item.control.add_class('active');
|
|
357
|
+
} else {
|
|
358
|
+
nav_item.control.remove_class('active');
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
_set_active_tab(section_id) {
|
|
364
|
+
this._tab_items.forEach((tab_item) => {
|
|
365
|
+
if (!tab_item.control) return;
|
|
366
|
+
if (section_id != null && tab_item.id === section_id) {
|
|
367
|
+
tab_item.control.add_class('active');
|
|
368
|
+
} else {
|
|
369
|
+
tab_item.control.remove_class('active');
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
_get_section_label(section_id) {
|
|
375
|
+
return this._section_labels[section_id] || section_id;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
_set_page_title(text) {
|
|
379
|
+
this._set_control_text(this._page_title, text);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
_select_section(section) {
|
|
383
|
+
this._active_section = section;
|
|
384
|
+
|
|
385
|
+
const dashboard_section = this._dashboard_section;
|
|
386
|
+
const dynamic_section = this._dynamic_section;
|
|
387
|
+
if (!dashboard_section || !dynamic_section) return Promise.resolve();
|
|
388
|
+
|
|
389
|
+
if (section === 'dashboard') {
|
|
390
|
+
dashboard_section.remove_class('hidden');
|
|
391
|
+
dynamic_section.add_class('hidden');
|
|
392
|
+
this._set_status_text('Dashboard view');
|
|
393
|
+
return Promise.resolve();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
dashboard_section.add_class('hidden');
|
|
397
|
+
dynamic_section.remove_class('hidden');
|
|
398
|
+
|
|
399
|
+
if (section === 'resources') {
|
|
400
|
+
return this._render_resources_section();
|
|
401
|
+
} else if (section === 'routes') {
|
|
402
|
+
return this._render_routes_section();
|
|
403
|
+
} else if (section === 'settings') {
|
|
404
|
+
return this._render_settings_section();
|
|
405
|
+
} else if (section.startsWith('custom:')) {
|
|
406
|
+
const custom_id = section.substring(7);
|
|
407
|
+
const custom = this._custom_sections_list && this._custom_sections_list.find(s => s.id === custom_id);
|
|
408
|
+
if (custom) {
|
|
409
|
+
return this._render_custom_section(custom);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return Promise.resolve();
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
_set_control_text(control, text) {
|
|
416
|
+
if (!control) return;
|
|
417
|
+
if (typeof control.clear === 'function') {
|
|
418
|
+
control.clear();
|
|
419
|
+
}
|
|
420
|
+
if (typeof control.add === 'function') {
|
|
421
|
+
control.add(String(text == null ? '' : text));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
_clear_control(control) {
|
|
426
|
+
if (control && typeof control.clear === 'function') {
|
|
427
|
+
control.clear();
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
_render_loading(message) {
|
|
432
|
+
if (!this._dynamic_section) return;
|
|
433
|
+
this._clear_control(this._dynamic_section);
|
|
434
|
+
const panel = this._create_panel();
|
|
435
|
+
const text = new controls.div({ context: this.context, 'class': 'as-muted' });
|
|
436
|
+
text.add(String(message));
|
|
437
|
+
panel.add(text);
|
|
438
|
+
this._dynamic_section.add(panel);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
_render_error(message, retry_handler) {
|
|
442
|
+
if (!this._dynamic_section) return;
|
|
443
|
+
this._clear_control(this._dynamic_section);
|
|
444
|
+
|
|
445
|
+
const panel = this._create_panel();
|
|
446
|
+
const text = new controls.div({ context: this.context, 'class': 'as-error' });
|
|
447
|
+
text.add(String(message));
|
|
448
|
+
panel.add(text);
|
|
449
|
+
|
|
450
|
+
const retry_btn = new controls.button({ context: this.context, 'class': 'as-retry-btn' });
|
|
451
|
+
retry_btn.dom.attributes.type = 'button';
|
|
452
|
+
retry_btn.add('Retry');
|
|
453
|
+
if (typeof retry_handler === 'function') {
|
|
454
|
+
retry_btn.on('click', retry_handler);
|
|
455
|
+
}
|
|
456
|
+
panel.add(retry_btn);
|
|
457
|
+
|
|
458
|
+
this._dynamic_section.add(panel);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
_render_empty(message) {
|
|
462
|
+
if (!this._dynamic_section) return;
|
|
463
|
+
this._clear_control(this._dynamic_section);
|
|
464
|
+
|
|
465
|
+
const panel = this._create_panel();
|
|
466
|
+
const text = new controls.div({ context: this.context, 'class': 'as-muted' });
|
|
467
|
+
text.add(String(message));
|
|
468
|
+
panel.add(text);
|
|
469
|
+
this._dynamic_section.add(panel);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
_fetch_json(url) {
|
|
473
|
+
return fetch(url).then((res) => {
|
|
474
|
+
if (!res.ok) {
|
|
475
|
+
if (res.status === 401) {
|
|
476
|
+
this._redirect_to_login();
|
|
477
|
+
throw new Error('Unauthorized');
|
|
478
|
+
}
|
|
479
|
+
throw new Error('HTTP ' + res.status + ' for ' + url);
|
|
480
|
+
}
|
|
481
|
+
return res.json();
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
_create_tag_control(tag_name, class_name) {
|
|
486
|
+
const spec = {
|
|
487
|
+
context: this.context,
|
|
488
|
+
tagName: tag_name
|
|
489
|
+
};
|
|
490
|
+
if (class_name) {
|
|
491
|
+
spec.class = class_name;
|
|
492
|
+
}
|
|
493
|
+
return new Control(spec);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
_create_panel(panel_title) {
|
|
497
|
+
const panel = new controls.div({ context: this.context, 'class': 'as-panel' });
|
|
498
|
+
if (panel_title) {
|
|
499
|
+
const title = new controls.h3({ context: this.context, 'class': 'as-panel-title' });
|
|
500
|
+
title.add(String(panel_title));
|
|
501
|
+
panel.add(title);
|
|
502
|
+
}
|
|
503
|
+
return panel;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
_stringify_value(value) {
|
|
507
|
+
if (value == null) return '';
|
|
508
|
+
if (typeof value === 'object') {
|
|
509
|
+
try {
|
|
510
|
+
return JSON.stringify(value);
|
|
511
|
+
} catch (err) {
|
|
512
|
+
return String(value);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return String(value);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
_create_table_panel(panel_title, column_names, rows_data) {
|
|
519
|
+
const panel = this._create_panel(panel_title);
|
|
520
|
+
|
|
521
|
+
const table = this._create_tag_control('table', 'as-table');
|
|
522
|
+
const thead = this._create_tag_control('thead');
|
|
523
|
+
const header_row = this._create_tag_control('tr');
|
|
524
|
+
|
|
525
|
+
column_names.forEach((column_name) => {
|
|
526
|
+
const th = this._create_tag_control('th');
|
|
527
|
+
th.add(String(column_name));
|
|
528
|
+
header_row.add(th);
|
|
529
|
+
});
|
|
530
|
+
thead.add(header_row);
|
|
531
|
+
table.add(thead);
|
|
532
|
+
|
|
533
|
+
const tbody = this._create_tag_control('tbody');
|
|
534
|
+
rows_data.forEach((row_data) => {
|
|
535
|
+
const row = this._create_tag_control('tr');
|
|
536
|
+
row_data.forEach((cell_value) => {
|
|
537
|
+
const td = this._create_tag_control('td');
|
|
538
|
+
td.add(this._stringify_value(cell_value));
|
|
539
|
+
row.add(td);
|
|
540
|
+
});
|
|
541
|
+
tbody.add(row);
|
|
542
|
+
});
|
|
543
|
+
table.add(tbody);
|
|
544
|
+
|
|
545
|
+
panel.add(table);
|
|
546
|
+
return panel;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
_create_kv_row(key_text, value_text) {
|
|
550
|
+
const row = new controls.div({ context: this.context, 'class': 'as-kv-row' });
|
|
551
|
+
const key = new controls.span({ context: this.context, 'class': 'as-kv-key' });
|
|
552
|
+
key.add(String(key_text));
|
|
553
|
+
const value = new controls.span({ context: this.context, 'class': 'as-kv-value' });
|
|
554
|
+
value.add(this._stringify_value(value_text));
|
|
555
|
+
row.add(key);
|
|
556
|
+
row.add(value);
|
|
557
|
+
return row;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
_render_resources_section() {
|
|
561
|
+
this._render_loading('Loading resources...');
|
|
562
|
+
this._set_status_text('Loading resources');
|
|
563
|
+
|
|
564
|
+
return this._fetch_json('/api/admin/v1/resources')
|
|
565
|
+
.then((data) => {
|
|
566
|
+
if (!this._dynamic_section) return;
|
|
567
|
+
|
|
568
|
+
const children = (data && data.children) || [];
|
|
569
|
+
if (children.length === 0) {
|
|
570
|
+
this._render_empty('No resources found.');
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const rows_data = children.map((resource_item) => {
|
|
575
|
+
return [
|
|
576
|
+
resource_item.name || 'Unnamed',
|
|
577
|
+
resource_item.type || 'Resource',
|
|
578
|
+
resource_item.state || 'unknown'
|
|
579
|
+
];
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
this._clear_control(this._dynamic_section);
|
|
583
|
+
this._dynamic_section.add(
|
|
584
|
+
this._create_table_panel('Resources', ['Name', 'Type', 'State'], rows_data)
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
this._set_status_text('Loaded resources: ' + children.length);
|
|
588
|
+
})
|
|
589
|
+
.catch((err) => {
|
|
590
|
+
console.error('[Admin] Failed to load resources:', err);
|
|
591
591
|
this._render_error('Failed to load resources.', () => this._render_resources_section());
|
|
592
592
|
this._set_status_text('Error loading resources');
|
|
593
593
|
});
|
|
594
594
|
}
|
|
595
595
|
|
|
596
|
-
_render_routes_section() {
|
|
597
|
-
this._render_loading('Loading routes...');
|
|
598
|
-
this._set_status_text('Loading routes');
|
|
599
|
-
|
|
600
|
-
return this._fetch_json('/api/admin/v1/routes')
|
|
601
|
-
.then((routes) => {
|
|
602
|
-
if (!this._dynamic_section) return;
|
|
603
|
-
|
|
604
|
-
if (!Array.isArray(routes) || routes.length === 0) {
|
|
605
|
-
this._render_empty('No routes found.');
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
const rows_data = routes.map((route) => {
|
|
610
|
-
return [
|
|
611
|
-
route.path || '',
|
|
612
|
-
route.method || 'GET',
|
|
613
|
-
route.type || 'route',
|
|
614
|
-
route.handler || 'anonymous'
|
|
615
|
-
];
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
this._clear_control(this._dynamic_section);
|
|
619
|
-
this._dynamic_section.add(
|
|
620
|
-
this._create_table_panel('Routes', ['Path', 'Method', 'Type', 'Handler'], rows_data)
|
|
621
|
-
);
|
|
622
|
-
|
|
623
|
-
this._set_status_text('Loaded routes: ' + routes.length);
|
|
624
|
-
})
|
|
625
|
-
.catch((err) => {
|
|
626
|
-
console.error('[Admin] Failed to load routes:', err);
|
|
596
|
+
_render_routes_section() {
|
|
597
|
+
this._render_loading('Loading routes...');
|
|
598
|
+
this._set_status_text('Loading routes');
|
|
599
|
+
|
|
600
|
+
return this._fetch_json('/api/admin/v1/routes')
|
|
601
|
+
.then((routes) => {
|
|
602
|
+
if (!this._dynamic_section) return;
|
|
603
|
+
|
|
604
|
+
if (!Array.isArray(routes) || routes.length === 0) {
|
|
605
|
+
this._render_empty('No routes found.');
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const rows_data = routes.map((route) => {
|
|
610
|
+
return [
|
|
611
|
+
route.path || '',
|
|
612
|
+
route.method || 'GET',
|
|
613
|
+
route.type || 'route',
|
|
614
|
+
route.handler || 'anonymous'
|
|
615
|
+
];
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
this._clear_control(this._dynamic_section);
|
|
619
|
+
this._dynamic_section.add(
|
|
620
|
+
this._create_table_panel('Routes', ['Path', 'Method', 'Type', 'Handler'], rows_data)
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
this._set_status_text('Loaded routes: ' + routes.length);
|
|
624
|
+
})
|
|
625
|
+
.catch((err) => {
|
|
626
|
+
console.error('[Admin] Failed to load routes:', err);
|
|
627
627
|
this._render_error('Failed to load routes.', () => this._render_routes_section());
|
|
628
628
|
this._set_status_text('Error loading routes');
|
|
629
629
|
});
|
|
630
630
|
}
|
|
631
631
|
|
|
632
|
-
_render_settings_section() {
|
|
633
|
-
this._render_loading('Loading settings snapshot...');
|
|
634
|
-
this._set_status_text('Loading settings snapshot');
|
|
635
|
-
|
|
636
|
-
return this._fetch_json('/api/admin/v1/status')
|
|
637
|
-
.then((status) => {
|
|
638
|
-
if (!this._dynamic_section) return;
|
|
639
|
-
|
|
640
|
-
const server_name = (status && status.server && status.server.name) || 'jsgui3-server';
|
|
641
|
-
const node_version = (status && status.process && status.process.node_version) || 'unknown';
|
|
642
|
-
const platform = (status && status.process && status.process.platform) || 'unknown';
|
|
643
|
-
const arch = (status && status.process && status.process.arch) || 'unknown';
|
|
644
|
-
|
|
645
|
-
const panel = this._create_panel('Settings (Read-only)');
|
|
646
|
-
panel.add(this._create_kv_row('Server Name', server_name));
|
|
647
|
-
panel.add(this._create_kv_row('Node Version', node_version));
|
|
648
|
-
panel.add(this._create_kv_row('Platform', platform + ' / ' + arch));
|
|
649
|
-
|
|
650
|
-
const logout_btn = new controls.button({ context: this.context, 'class': 'as-logout-btn' });
|
|
651
|
-
logout_btn.dom.attributes.type = 'button';
|
|
652
|
-
logout_btn.add('Log Out');
|
|
653
|
-
logout_btn.on('click', () => {
|
|
654
|
-
fetch('/api/admin/v1/auth/logout', {
|
|
655
|
-
method: 'POST',
|
|
656
|
-
credentials: 'same-origin'
|
|
657
|
-
}).finally(() => {
|
|
658
|
-
this._redirect_to_login();
|
|
659
|
-
});
|
|
660
|
-
});
|
|
661
|
-
panel.add(logout_btn);
|
|
662
|
-
|
|
663
|
-
const muted = new controls.div({ context: this.context, 'class': 'as-muted' });
|
|
664
|
-
muted.add('Authentication and write-actions are intentionally not enabled yet.');
|
|
665
|
-
panel.add(muted);
|
|
666
|
-
|
|
667
|
-
this._clear_control(this._dynamic_section);
|
|
668
|
-
this._dynamic_section.add(panel);
|
|
669
|
-
|
|
670
|
-
this._set_status_text('Loaded settings snapshot');
|
|
671
|
-
})
|
|
672
|
-
.catch((err) => {
|
|
673
|
-
console.error('[Admin] Failed to load settings snapshot:', err);
|
|
632
|
+
_render_settings_section() {
|
|
633
|
+
this._render_loading('Loading settings snapshot...');
|
|
634
|
+
this._set_status_text('Loading settings snapshot');
|
|
635
|
+
|
|
636
|
+
return this._fetch_json('/api/admin/v1/status')
|
|
637
|
+
.then((status) => {
|
|
638
|
+
if (!this._dynamic_section) return;
|
|
639
|
+
|
|
640
|
+
const server_name = (status && status.server && status.server.name) || 'jsgui3-server';
|
|
641
|
+
const node_version = (status && status.process && status.process.node_version) || 'unknown';
|
|
642
|
+
const platform = (status && status.process && status.process.platform) || 'unknown';
|
|
643
|
+
const arch = (status && status.process && status.process.arch) || 'unknown';
|
|
644
|
+
|
|
645
|
+
const panel = this._create_panel('Settings (Read-only)');
|
|
646
|
+
panel.add(this._create_kv_row('Server Name', server_name));
|
|
647
|
+
panel.add(this._create_kv_row('Node Version', node_version));
|
|
648
|
+
panel.add(this._create_kv_row('Platform', platform + ' / ' + arch));
|
|
649
|
+
|
|
650
|
+
const logout_btn = new controls.button({ context: this.context, 'class': 'as-logout-btn' });
|
|
651
|
+
logout_btn.dom.attributes.type = 'button';
|
|
652
|
+
logout_btn.add('Log Out');
|
|
653
|
+
logout_btn.on('click', () => {
|
|
654
|
+
fetch('/api/admin/v1/auth/logout', {
|
|
655
|
+
method: 'POST',
|
|
656
|
+
credentials: 'same-origin'
|
|
657
|
+
}).finally(() => {
|
|
658
|
+
this._redirect_to_login();
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
panel.add(logout_btn);
|
|
662
|
+
|
|
663
|
+
const muted = new controls.div({ context: this.context, 'class': 'as-muted' });
|
|
664
|
+
muted.add('Authentication and write-actions are intentionally not enabled yet.');
|
|
665
|
+
panel.add(muted);
|
|
666
|
+
|
|
667
|
+
this._clear_control(this._dynamic_section);
|
|
668
|
+
this._dynamic_section.add(panel);
|
|
669
|
+
|
|
670
|
+
this._set_status_text('Loaded settings snapshot');
|
|
671
|
+
})
|
|
672
|
+
.catch((err) => {
|
|
673
|
+
console.error('[Admin] Failed to load settings snapshot:', err);
|
|
674
674
|
this._render_error('Failed to load settings snapshot.', () => this._render_settings_section());
|
|
675
675
|
this._set_status_text('Error loading settings snapshot');
|
|
676
|
-
});
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
_redirect_to_login() {
|
|
680
|
-
if (typeof window !== 'undefined' && window.location) {
|
|
681
|
-
window.location.href = '/admin/v1/login';
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
_fetch_status() {
|
|
686
|
-
return this._fetch_json('/api/admin/v1/status')
|
|
687
|
-
.then(data => {
|
|
688
|
-
this._apply_status(data);
|
|
689
|
-
this._set_status_text('Connected — data loaded');
|
|
690
|
-
})
|
|
691
|
-
.catch(err => {
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
_redirect_to_login() {
|
|
680
|
+
if (typeof window !== 'undefined' && window.location) {
|
|
681
|
+
window.location.href = '/admin/v1/login';
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
_fetch_status() {
|
|
686
|
+
return this._fetch_json('/api/admin/v1/status')
|
|
687
|
+
.then(data => {
|
|
688
|
+
this._apply_status(data);
|
|
689
|
+
this._set_status_text('Connected — data loaded');
|
|
690
|
+
})
|
|
691
|
+
.catch(err => {
|
|
692
692
|
console.error('[Admin] Failed to fetch status:', err);
|
|
693
693
|
this._set_status_text('Error loading status');
|
|
694
694
|
});
|
|
@@ -798,22 +798,22 @@ class Admin_Shell extends Active_HTML_Document {
|
|
|
798
798
|
}
|
|
799
799
|
});
|
|
800
800
|
|
|
801
|
-
es.addEventListener('open', () => {
|
|
802
|
-
// Reset backoff on successful connection
|
|
803
|
-
this._sse_backoff = 1000;
|
|
804
|
-
this._set_status_text('Live — SSE connected');
|
|
805
|
-
if (this._status_dot) {
|
|
806
|
-
this._status_dot.add_class('online');
|
|
807
|
-
this._status_dot.remove_class('offline');
|
|
808
|
-
}
|
|
809
|
-
});
|
|
810
|
-
|
|
811
|
-
es.addEventListener('error', () => {
|
|
812
|
-
this._set_status_text('SSE disconnected — reconnecting...');
|
|
813
|
-
if (this._status_dot) {
|
|
814
|
-
this._status_dot.add_class('offline');
|
|
815
|
-
this._status_dot.remove_class('online');
|
|
816
|
-
}
|
|
801
|
+
es.addEventListener('open', () => {
|
|
802
|
+
// Reset backoff on successful connection
|
|
803
|
+
this._sse_backoff = 1000;
|
|
804
|
+
this._set_status_text('Live — SSE connected');
|
|
805
|
+
if (this._status_dot) {
|
|
806
|
+
this._status_dot.add_class('online');
|
|
807
|
+
this._status_dot.remove_class('offline');
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
es.addEventListener('error', () => {
|
|
812
|
+
this._set_status_text('SSE disconnected — reconnecting...');
|
|
813
|
+
if (this._status_dot) {
|
|
814
|
+
this._status_dot.add_class('offline');
|
|
815
|
+
this._status_dot.remove_class('online');
|
|
816
|
+
}
|
|
817
817
|
|
|
818
818
|
// Close and reconnect with backoff
|
|
819
819
|
es.close();
|
|
@@ -892,9 +892,9 @@ class Admin_Shell extends Active_HTML_Document {
|
|
|
892
892
|
this._set_status_text('Live — last update ' + hh + ':' + mm + ':' + ss);
|
|
893
893
|
}
|
|
894
894
|
|
|
895
|
-
_set_status_text(text) {
|
|
896
|
-
this._set_control_text(this._status_text, text);
|
|
897
|
-
}
|
|
895
|
+
_set_status_text(text) {
|
|
896
|
+
this._set_control_text(this._status_text, text);
|
|
897
|
+
}
|
|
898
898
|
|
|
899
899
|
// ─── Custom Sections (Extensibility) ─────────────────
|
|
900
900
|
|
|
@@ -904,91 +904,91 @@ class Admin_Shell extends Active_HTML_Document {
|
|
|
904
904
|
* click handler that fetches data from its api_path and renders
|
|
905
905
|
* it using automatic table/key-value rendering.
|
|
906
906
|
*/
|
|
907
|
-
_load_custom_sections() {
|
|
908
|
-
this._custom_sections_list = [];
|
|
909
|
-
|
|
910
|
-
return this._fetch_json('/api/admin/v1/custom-sections')
|
|
911
|
-
.then(sections => {
|
|
912
|
-
this._remove_custom_nav_items();
|
|
913
|
-
if (!Array.isArray(sections) || sections.length === 0) return;
|
|
914
|
-
|
|
915
|
-
this._custom_sections_list = sections;
|
|
916
|
-
if (!this._nav) return;
|
|
917
|
-
|
|
918
|
-
this._ensure_custom_nav_separator();
|
|
919
|
-
|
|
920
|
-
sections.forEach(section => {
|
|
921
|
-
const section_id = 'custom:' + section.id;
|
|
922
|
-
const item = this._add_nav_item(
|
|
923
|
-
this._nav,
|
|
924
|
-
section.label || section.id,
|
|
925
|
-
section_id,
|
|
926
|
-
false,
|
|
927
|
-
{
|
|
928
|
-
icon: section.icon || '',
|
|
929
|
-
title: section.label || section.id,
|
|
930
|
-
is_custom: true
|
|
931
|
-
}
|
|
932
|
-
);
|
|
933
|
-
if (this.__active && typeof item.activate === 'function') {
|
|
934
|
-
item.activate();
|
|
935
|
-
}
|
|
936
|
-
});
|
|
937
|
-
})
|
|
938
|
-
.catch(err => {
|
|
939
|
-
console.warn('[Admin] Failed to load custom sections:', err);
|
|
940
|
-
});
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
_remove_custom_nav_items() {
|
|
944
|
-
const custom_ids = this._custom_nav_items.map((custom_item) => custom_item.id);
|
|
945
|
-
this._custom_nav_items.forEach((custom_item) => {
|
|
946
|
-
if (custom_item.control && typeof custom_item.control.remove === 'function') {
|
|
947
|
-
custom_item.control.remove();
|
|
948
|
-
}
|
|
949
|
-
});
|
|
950
|
-
this._custom_nav_items = [];
|
|
951
|
-
|
|
952
|
-
this._nav_items = this._nav_items.filter((nav_item) => !nav_item.is_custom);
|
|
953
|
-
custom_ids.forEach((custom_id) => {
|
|
954
|
-
delete this._section_labels[custom_id];
|
|
955
|
-
});
|
|
956
|
-
|
|
957
|
-
if (this._custom_nav_separator && typeof this._custom_nav_separator.remove === 'function') {
|
|
958
|
-
this._custom_nav_separator.remove();
|
|
959
|
-
this._custom_nav_separator = null;
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
_ensure_custom_nav_separator() {
|
|
964
|
-
if (this._custom_nav_separator || !this._nav) return;
|
|
965
|
-
const separator = new controls.div({ context: this.context, 'class': 'as-nav-separator' });
|
|
966
|
-
this._nav.add(separator);
|
|
967
|
-
this._custom_nav_separator = separator;
|
|
968
|
-
if (this.__active && typeof separator.activate === 'function') {
|
|
969
|
-
separator.activate();
|
|
970
|
-
}
|
|
971
|
-
}
|
|
907
|
+
_load_custom_sections() {
|
|
908
|
+
this._custom_sections_list = [];
|
|
909
|
+
|
|
910
|
+
return this._fetch_json('/api/admin/v1/custom-sections')
|
|
911
|
+
.then(sections => {
|
|
912
|
+
this._remove_custom_nav_items();
|
|
913
|
+
if (!Array.isArray(sections) || sections.length === 0) return;
|
|
914
|
+
|
|
915
|
+
this._custom_sections_list = sections;
|
|
916
|
+
if (!this._nav) return;
|
|
917
|
+
|
|
918
|
+
this._ensure_custom_nav_separator();
|
|
919
|
+
|
|
920
|
+
sections.forEach(section => {
|
|
921
|
+
const section_id = 'custom:' + section.id;
|
|
922
|
+
const item = this._add_nav_item(
|
|
923
|
+
this._nav,
|
|
924
|
+
section.label || section.id,
|
|
925
|
+
section_id,
|
|
926
|
+
false,
|
|
927
|
+
{
|
|
928
|
+
icon: section.icon || '',
|
|
929
|
+
title: section.label || section.id,
|
|
930
|
+
is_custom: true
|
|
931
|
+
}
|
|
932
|
+
);
|
|
933
|
+
if (this.__active && typeof item.activate === 'function') {
|
|
934
|
+
item.activate();
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
})
|
|
938
|
+
.catch(err => {
|
|
939
|
+
console.warn('[Admin] Failed to load custom sections:', err);
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
_remove_custom_nav_items() {
|
|
944
|
+
const custom_ids = this._custom_nav_items.map((custom_item) => custom_item.id);
|
|
945
|
+
this._custom_nav_items.forEach((custom_item) => {
|
|
946
|
+
if (custom_item.control && typeof custom_item.control.remove === 'function') {
|
|
947
|
+
custom_item.control.remove();
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
this._custom_nav_items = [];
|
|
951
|
+
|
|
952
|
+
this._nav_items = this._nav_items.filter((nav_item) => !nav_item.is_custom);
|
|
953
|
+
custom_ids.forEach((custom_id) => {
|
|
954
|
+
delete this._section_labels[custom_id];
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
if (this._custom_nav_separator && typeof this._custom_nav_separator.remove === 'function') {
|
|
958
|
+
this._custom_nav_separator.remove();
|
|
959
|
+
this._custom_nav_separator = null;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
_ensure_custom_nav_separator() {
|
|
964
|
+
if (this._custom_nav_separator || !this._nav) return;
|
|
965
|
+
const separator = new controls.div({ context: this.context, 'class': 'as-nav-separator' });
|
|
966
|
+
this._nav.add(separator);
|
|
967
|
+
this._custom_nav_separator = separator;
|
|
968
|
+
if (this.__active && typeof separator.activate === 'function') {
|
|
969
|
+
separator.activate();
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
972
|
|
|
973
973
|
/**
|
|
974
974
|
* Render a custom section by fetching its data endpoint.
|
|
975
975
|
* Arrays are rendered as tables, objects as key-value panels,
|
|
976
976
|
* scalars as plain text.
|
|
977
977
|
*/
|
|
978
|
-
_render_custom_section(section) {
|
|
979
|
-
if (!this._dashboard_section || !this._dynamic_section) return Promise.resolve();
|
|
980
|
-
|
|
981
|
-
this._dashboard_section.add_class('hidden');
|
|
982
|
-
this._dynamic_section.remove_class('hidden');
|
|
983
|
-
|
|
984
|
-
this._render_loading('Loading ' + section.label + '...');
|
|
985
|
-
this._set_status_text('Loading ' + section.label);
|
|
986
|
-
|
|
987
|
-
return this._fetch_json(section.api_path)
|
|
988
|
-
.then(data => {
|
|
989
|
-
this._render_custom_section_data(section, data);
|
|
990
|
-
this._set_status_text('Loaded ' + section.label);
|
|
991
|
-
})
|
|
978
|
+
_render_custom_section(section) {
|
|
979
|
+
if (!this._dashboard_section || !this._dynamic_section) return Promise.resolve();
|
|
980
|
+
|
|
981
|
+
this._dashboard_section.add_class('hidden');
|
|
982
|
+
this._dynamic_section.remove_class('hidden');
|
|
983
|
+
|
|
984
|
+
this._render_loading('Loading ' + section.label + '...');
|
|
985
|
+
this._set_status_text('Loading ' + section.label);
|
|
986
|
+
|
|
987
|
+
return this._fetch_json(section.api_path)
|
|
988
|
+
.then(data => {
|
|
989
|
+
this._render_custom_section_data(section, data);
|
|
990
|
+
this._set_status_text('Loaded ' + section.label);
|
|
991
|
+
})
|
|
992
992
|
.catch(err => {
|
|
993
993
|
console.error('[Admin] Failed to load custom section:', err);
|
|
994
994
|
this._render_error('Failed to load ' + section.label + '.', () => this._render_custom_section(section));
|
|
@@ -999,47 +999,47 @@ class Admin_Shell extends Active_HTML_Document {
|
|
|
999
999
|
/**
|
|
1000
1000
|
* Auto-render data based on shape:
|
|
1001
1001
|
* - Array of objects → table
|
|
1002
|
-
* - Object → key-value panel
|
|
1003
|
-
* - Scalar → plain text
|
|
1004
|
-
*/
|
|
1005
|
-
_render_custom_section_data(section, data) {
|
|
1006
|
-
if (!this._dynamic_section) return;
|
|
1007
|
-
|
|
1008
|
-
if (Array.isArray(data)) {
|
|
1009
|
-
if (data.length === 0) {
|
|
1010
|
-
this._render_empty('No data for ' + section.label + '.');
|
|
1011
|
-
return;
|
|
1012
|
-
}
|
|
1013
|
-
const normalized_first_row = (data[0] && typeof data[0] === 'object')
|
|
1014
|
-
? data[0]
|
|
1015
|
-
: { value: data[0] };
|
|
1016
|
-
const keys = Object.keys(normalized_first_row);
|
|
1017
|
-
const rows_data = data.map((row) => {
|
|
1018
|
-
const normalized_row = (row && typeof row === 'object') ? row : { value: row };
|
|
1019
|
-
return keys.map((key) => normalized_row[key]);
|
|
1020
|
-
});
|
|
1021
|
-
|
|
1022
|
-
this._clear_control(this._dynamic_section);
|
|
1023
|
-
this._dynamic_section.add(
|
|
1024
|
-
this._create_table_panel(section.label, keys, rows_data)
|
|
1025
|
-
);
|
|
1026
|
-
} else if (data && typeof data === 'object') {
|
|
1027
|
-
const panel = this._create_panel(section.label);
|
|
1028
|
-
Object.entries(data).forEach((entry) => {
|
|
1029
|
-
panel.add(this._create_kv_row(entry[0], entry[1]));
|
|
1030
|
-
});
|
|
1031
|
-
this._clear_control(this._dynamic_section);
|
|
1032
|
-
this._dynamic_section.add(panel);
|
|
1033
|
-
} else {
|
|
1034
|
-
const panel = this._create_panel();
|
|
1035
|
-
const text = new controls.div({ context: this.context, 'class': 'as-muted' });
|
|
1036
|
-
text.add(this._stringify_value(data));
|
|
1037
|
-
panel.add(text);
|
|
1038
|
-
this._clear_control(this._dynamic_section);
|
|
1039
|
-
this._dynamic_section.add(panel);
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1002
|
+
* - Object → key-value panel
|
|
1003
|
+
* - Scalar → plain text
|
|
1004
|
+
*/
|
|
1005
|
+
_render_custom_section_data(section, data) {
|
|
1006
|
+
if (!this._dynamic_section) return;
|
|
1007
|
+
|
|
1008
|
+
if (Array.isArray(data)) {
|
|
1009
|
+
if (data.length === 0) {
|
|
1010
|
+
this._render_empty('No data for ' + section.label + '.');
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
const normalized_first_row = (data[0] && typeof data[0] === 'object')
|
|
1014
|
+
? data[0]
|
|
1015
|
+
: { value: data[0] };
|
|
1016
|
+
const keys = Object.keys(normalized_first_row);
|
|
1017
|
+
const rows_data = data.map((row) => {
|
|
1018
|
+
const normalized_row = (row && typeof row === 'object') ? row : { value: row };
|
|
1019
|
+
return keys.map((key) => normalized_row[key]);
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
this._clear_control(this._dynamic_section);
|
|
1023
|
+
this._dynamic_section.add(
|
|
1024
|
+
this._create_table_panel(section.label, keys, rows_data)
|
|
1025
|
+
);
|
|
1026
|
+
} else if (data && typeof data === 'object') {
|
|
1027
|
+
const panel = this._create_panel(section.label);
|
|
1028
|
+
Object.entries(data).forEach((entry) => {
|
|
1029
|
+
panel.add(this._create_kv_row(entry[0], entry[1]));
|
|
1030
|
+
});
|
|
1031
|
+
this._clear_control(this._dynamic_section);
|
|
1032
|
+
this._dynamic_section.add(panel);
|
|
1033
|
+
} else {
|
|
1034
|
+
const panel = this._create_panel();
|
|
1035
|
+
const text = new controls.div({ context: this.context, 'class': 'as-muted' });
|
|
1036
|
+
text.add(this._stringify_value(data));
|
|
1037
|
+
panel.add(text);
|
|
1038
|
+
this._clear_control(this._dynamic_section);
|
|
1039
|
+
this._dynamic_section.add(panel);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
1043
|
|
|
1044
1044
|
Admin_Shell.css = `
|
|
1045
1045
|
/* ─── Reset & Root ───────────────────────────────────────── */
|