jsgui3-server 0.0.149 → 0.0.150
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/agents/Mobile Developer.agent.md +89 -0
- package/AGENTS.md +4 -0
- package/README.md +130 -0
- package/admin-ui/client.js +73 -43
- package/admin-ui/v1/admin_auth_service.js +197 -0
- package/admin-ui/v1/admin_user_store.js +71 -0
- package/admin-ui/v1/client.js +17 -0
- package/admin-ui/v1/controls/admin_shell.js +1399 -0
- package/admin-ui/v1/controls/group_box.js +84 -0
- package/admin-ui/v1/controls/stat_card.js +125 -0
- package/admin-ui/v1/server.js +658 -0
- package/admin-ui/v1/utils/formatters.js +68 -0
- package/docs/admin-extension-guide.md +345 -0
- package/docs/books/adaptive-control-improvements/01-control-candidate-matrix.md +122 -0
- package/docs/books/adaptive-control-improvements/02-tier-1-layout-playbooks.md +207 -0
- package/docs/books/adaptive-control-improvements/03-tier-2-navigation-form-overlay.md +140 -0
- package/docs/books/adaptive-control-improvements/04-cross-cutting-platform-functionality.md +141 -0
- package/docs/books/adaptive-control-improvements/05-styling-theming-density-upgrades.md +114 -0
- package/docs/books/adaptive-control-improvements/06-testing-quality-gates.md +97 -0
- package/docs/books/adaptive-control-improvements/07-delivery-roadmap-and-ownership.md +137 -0
- package/docs/books/adaptive-control-improvements/08-appendix-tier1-acceptance-and-pr-templates.md +261 -0
- package/docs/books/adaptive-control-improvements/README.md +66 -0
- package/docs/books/admin-ui-authentication/01-threat-model-and-goals.md +124 -0
- package/docs/books/admin-ui-authentication/02-session-model-and-token-model.md +75 -0
- package/docs/books/admin-ui-authentication/03-auth-middleware-patterns.md +77 -0
- package/docs/books/admin-ui-authentication/README.md +25 -0
- package/docs/books/creating-a-new-admin-ui/01-introduction-and-vision.md +130 -0
- package/docs/books/creating-a-new-admin-ui/02-architecture-and-data-flow.md +298 -0
- package/docs/books/creating-a-new-admin-ui/03-server-introspection.md +381 -0
- package/docs/books/creating-a-new-admin-ui/04-admin-module-adapter-layer.md +592 -0
- package/docs/books/creating-a-new-admin-ui/05-domain-controls-stat-cards-and-gauges.md +513 -0
- package/docs/books/creating-a-new-admin-ui/06-domain-controls-process-manager.md +544 -0
- package/docs/books/creating-a-new-admin-ui/07-domain-controls-resource-pool-inspector.md +493 -0
- package/docs/books/creating-a-new-admin-ui/08-domain-controls-route-table-and-api-explorer.md +586 -0
- package/docs/books/creating-a-new-admin-ui/09-domain-controls-log-viewer-and-activity-feed.md +490 -0
- package/docs/books/creating-a-new-admin-ui/10-domain-controls-build-status-and-bundle-inspector.md +526 -0
- package/docs/books/creating-a-new-admin-ui/11-domain-controls-configuration-panel.md +808 -0
- package/docs/books/creating-a-new-admin-ui/12-admin-shell-layout-sidebar-navigation.md +210 -0
- package/docs/books/creating-a-new-admin-ui/13-telemetry-integration.md +556 -0
- package/docs/books/creating-a-new-admin-ui/14-realtime-sse-observable-integration.md +485 -0
- package/docs/books/creating-a-new-admin-ui/15-styling-theming-aero-design-system.md +521 -0
- package/docs/books/creating-a-new-admin-ui/16-testing-and-quality-assurance.md +147 -0
- package/docs/books/creating-a-new-admin-ui/17-next-steps-process-resource-roadmap.md +356 -0
- package/docs/books/creating-a-new-admin-ui/README.md +68 -0
- package/docs/books/device-adaptive-composition/01-platform-feature-audit.md +177 -0
- package/docs/books/device-adaptive-composition/02-responsive-composition-model.md +187 -0
- package/docs/books/device-adaptive-composition/03-data-model-vs-view-model.md +231 -0
- package/docs/books/device-adaptive-composition/04-styling-theme-breakpoints.md +234 -0
- package/docs/books/device-adaptive-composition/05-showcase-app-multi-device-assessment.md +193 -0
- package/docs/books/device-adaptive-composition/06-implementation-patterns-and-apis.md +346 -0
- package/docs/books/device-adaptive-composition/07-testing-harness-and-quality-gates.md +265 -0
- package/docs/books/device-adaptive-composition/08-roadmap-and-adoption-plan.md +250 -0
- package/docs/books/device-adaptive-composition/README.md +47 -0
- package/docs/comparison-report-express-plex-cpanel.md +549 -0
- package/docs/designs/server-admin-interface-aero.svg +611 -0
- package/docs/troubleshooting.md +84 -53
- package/module.js +16 -11
- package/package.json +1 -1
- package/serve-factory.js +1 -0
- package/server.js +199 -0
- package/tests/README.md +5 -0
- package/tests/admin-ui-jsgui-controls.test.js +581 -0
- package/tests/test-runner.js +1 -0
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-071799b982906680f5fd699d.js +0 -40
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-07352945ad5c92654fcb8b65.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-138a601fadb6191ea314c6fd.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-171f6c381c2cadf2e9fa7087.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-1d973388156b84a04373fac9.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-20e117bc8a10d2cd16234bbe.js +0 -40
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-2b028a82b0e5efddba42425f.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-4518556cd5c7e059e82b22b8.js +0 -40
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-5bac1aa0f213902f718ed74f.js +0 -40
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-5f9996ac7822caf777d92f56.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-60a92c702e65fd9cf748e3ec.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-6164c1f8f738995c541895d2.js +0 -44
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-6718a85eb9e5aa782dd47a05.js +0 -45
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-69e280f14e37aee76a1d4675.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-7570d1b030d44b111ed59c4c.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-7798c9bbd55e510d5039f936.js +0 -42
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-78cd511ea1ef18ecb03d1be5.js +0 -40
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-7d482e0b95bcb5e3c543118b.js +0 -43
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-80e9476d1127c55b40fdb36f.js +0 -40
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-810ced55d5320a3088a05b13.js +0 -40
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-8423565f1a40e329afc8c6cf.js +0 -40
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-900bef783b8cee36506ec282.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-a1a37aff6416fdad74040ddf.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-ad48d5e8eda40f175b4df090.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-aec5a2d963015528c9099462.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-af9d34e0f1722fab9e28c269.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-b818e4015e2f1fe86280b5ab.js +0 -41
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-bcb2541adc70b7aba61768c5.js +0 -44
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-bfe89d2c78ed44f95ed7dd73.js +0 -40
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-c06f04806a1e688e1187110c.js +0 -40
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-c3f3adf904f585afc544b96a.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-d45acb873e1d8e32d5e60f2e.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-db06f132533706f4a0163b8c.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-f660f40d78b135fc8560a862.js +0 -39
- package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-f9dee4ec18a96e09bee06bae.js +0 -39
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Mobile Developer
|
|
3
|
+
|
|
4
|
+
description: Device-adaptive UI specialist for jsgui3-html. Uses the Device-Adaptive Composition book to guide responsive layout, composition, theming, and testing across phone, tablet, and desktop.
|
|
5
|
+
|
|
6
|
+
tools: [vscode, execute, read, agent, edit, search, web, 'playwright/*', todo]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
argument-hint: A responsive/adaptive UI task, layout question, or multi-device composition problem.
|
|
10
|
+
---
|
|
11
|
+
# Mission
|
|
12
|
+
For any work involving responsive layout, multi-device composition, adaptive styling, or mobile/tablet support in jsgui3-html, anchor all decisions in the **Device-Adaptive Composition & Styling** book at `docs/books/device-adaptive-composition/`.
|
|
13
|
+
|
|
14
|
+
# Reference Book — Required Reading
|
|
15
|
+
|
|
16
|
+
Before starting any task, read the relevant chapters:
|
|
17
|
+
|
|
18
|
+
| Chapter | File | When to consult |
|
|
19
|
+
|---------|------|----------------|
|
|
20
|
+
| 1 — Platform Feature Audit | `docs/books/device-adaptive-composition/01-platform-feature-audit.md` | Understanding what layout primitives, tokens, and MVVM infrastructure already exist |
|
|
21
|
+
| 2 — Responsive Composition Model | `docs/books/device-adaptive-composition/02-responsive-composition-model.md` | Designing adaptive shells, choosing between CSS and JS composition, the four-layer model |
|
|
22
|
+
| 3 — Data Model vs View Model | `docs/books/device-adaptive-composition/03-data-model-vs-view-model.md` | Deciding where adaptive state lives (view.data.model, never data.model) |
|
|
23
|
+
| 4 — Styling, Themes, and Breakpoints | `docs/books/device-adaptive-composition/04-styling-theme-breakpoints.md` | Token overrides, density modes, mode attributes, touch target sizing |
|
|
24
|
+
| 5 — Showcase App Assessment | `docs/books/device-adaptive-composition/05-showcase-app-multi-device-assessment.md` | Understanding how the showcase app should adapt per device category |
|
|
25
|
+
| 6 — Implementation Patterns and APIs | `docs/books/device-adaptive-composition/06-implementation-patterns-and-apis.md` | Using View_Environment, compose_adaptive(), responsive params, container-aware utilities |
|
|
26
|
+
| 7 — Testing Harness and Quality Gates | `docs/books/device-adaptive-composition/07-testing-harness-and-quality-gates.md` | Writing viewport-matrix Playwright tests, assertion categories (P0/P1/P2) |
|
|
27
|
+
| 8 — Roadmap and Adoption Plan | `docs/books/device-adaptive-composition/08-roadmap-and-adoption-plan.md` | Phased rollout priorities, what to build vs what to skip |
|
|
28
|
+
|
|
29
|
+
# Non-negotiables
|
|
30
|
+
|
|
31
|
+
- **Consult the book first**: before proposing any adaptive/responsive solution, read the relevant chapter(s) and identify which patterns apply.
|
|
32
|
+
- **Use the four-layer model** (Chapter 2): separate Domain Composition, View Composition, Adaptive Resolution, and Concrete Render concerns.
|
|
33
|
+
- **Keep adaptive state in view models** (Chapter 3): never put layout_mode, density, viewport dimensions, or panel visibility in `data.model`.
|
|
34
|
+
- **Use mode attributes over scattered breakpoints** (Chapter 4): prefer `[data-layout-mode="phone"]` CSS selectors over raw `@media` queries.
|
|
35
|
+
- **Follow the snake_case convention**: all variables, methods, and file names use snake_case per AGENTS.md.
|
|
36
|
+
- **Name the pattern(s)** from the book that you're applying and cite the chapter.
|
|
37
|
+
|
|
38
|
+
# When this applies
|
|
39
|
+
|
|
40
|
+
- Any change involving responsive layout, adaptive composition, or multi-device support
|
|
41
|
+
- Adding or modifying layout primitives (Stack, Drawer, Grid_Gap, Split_Pane)
|
|
42
|
+
- Implementing density modes, touch target sizing, or interaction-mode awareness
|
|
43
|
+
- Writing viewport-matrix Playwright tests
|
|
44
|
+
- Theme profile work involving density or layout-mode token overrides
|
|
45
|
+
- Assessing how a control or app behaves on phone, tablet, or desktop
|
|
46
|
+
|
|
47
|
+
# Key Concepts from the Book
|
|
48
|
+
|
|
49
|
+
## Four-Layer Composition (Chapter 2)
|
|
50
|
+
- **Layer A** (Domain): business data — device-agnostic, lives in `data.model`
|
|
51
|
+
- **Layer B** (View): regions and component hierarchy — expresses adaptive intent
|
|
52
|
+
- **Layer C** (Adaptive Resolution): environment service resolves mode from viewport/input/preferences
|
|
53
|
+
- **Layer D** (Concrete Render): resolved CSS classes, token values, DOM attributes
|
|
54
|
+
|
|
55
|
+
## Environment Contract (Chapters 2, 6)
|
|
56
|
+
```js
|
|
57
|
+
context.view_environment = {
|
|
58
|
+
viewport: { width, height, orientation },
|
|
59
|
+
layout_mode: 'phone' | 'tablet' | 'desktop',
|
|
60
|
+
density_mode: 'compact' | 'cozy' | 'comfortable',
|
|
61
|
+
interaction_mode: 'touch' | 'pointer' | 'hybrid',
|
|
62
|
+
motion_mode: 'normal' | 'reduced'
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
Composition Helper (Chapter 6)
|
|
66
|
+
|
|
67
|
+
compose_adaptive(this, {
|
|
68
|
+
phone: () => this.compose_phone_shell(),
|
|
69
|
+
tablet: () => this.compose_tablet_shell(),
|
|
70
|
+
desktop: () => this.compose_desktop_shell()
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
Viewport Test Matrix (Chapter 7)
|
|
74
|
+
Phone portrait (390×844), Phone landscape (844×390), Tablet portrait (768×1024), Tablet landscape (1024×768), Desktop narrow (1280×720), Desktop wide (1920×1080).
|
|
75
|
+
|
|
76
|
+
If the book doesn't cover it, consult:
|
|
77
|
+
AGENTS.md — project-wide naming conventions, testing patterns, coding style
|
|
78
|
+
docs/accessibility_and_semantics.md — WCAG/ARIA guidance
|
|
79
|
+
control_mixins/keyboard_navigation.js — orientation-aware keyboard handling
|
|
80
|
+
css/jsgui-tokens.css — current token definitions
|
|
81
|
+
controls/organised/AGENT.md — control creation and theming guide
|
|
82
|
+
Output format for adaptive UI tasks
|
|
83
|
+
Include:
|
|
84
|
+
|
|
85
|
+
Book reference: which chapter(s) and pattern(s) apply
|
|
86
|
+
Layer analysis: which of the four layers (A/B/C/D) are affected
|
|
87
|
+
Model placement: what state goes in data.model vs view.data.model vs view.model
|
|
88
|
+
Composition approach: CSS-only (continuous) vs JS composition (discrete) vs both
|
|
89
|
+
Test coverage: which viewport profiles to test, which assertion categories (P0/P1/P2)
|
package/AGENTS.md
CHANGED
|
@@ -36,6 +36,9 @@
|
|
|
36
36
|
- **[docs/agent-development-guide.md](docs/agent-development-guide.md)** - Guide for AI agents working on this codebase
|
|
37
37
|
- **[docs/broken-functionality-tracker.md](docs/broken-functionality-tracker.md)** - Tracker for broken/incomplete functionality
|
|
38
38
|
|
|
39
|
+
### Admin UI
|
|
40
|
+
- **[docs/admin-extension-guide.md](docs/admin-extension-guide.md)** - Admin UI extension API: custom sections, endpoints, plugins, exported classes
|
|
41
|
+
|
|
39
42
|
### Review and Maintenance
|
|
40
43
|
- **[docs/documentation-review/CURRENT_REVIEW.md](docs/documentation-review/CURRENT_REVIEW.md)** - Current documentation review status and known issues
|
|
41
44
|
|
|
@@ -58,6 +61,7 @@
|
|
|
58
61
|
- **Custom control development** → `docs/controls-development.md`
|
|
59
62
|
- **Publisher system** → `docs/publishers-guide.md`
|
|
60
63
|
- **Resource management** → `docs/resources-guide.md`
|
|
64
|
+
- **Admin UI extensions** → `docs/admin-extension-guide.md`
|
|
61
65
|
|
|
62
66
|
### Agent Development
|
|
63
67
|
- **Agentic workflow patterns** → `docs/GUIDE_TO_AGENTIC_WORKFLOWS_BY_GROK.md`
|
package/README.md
CHANGED
|
@@ -168,6 +168,136 @@ sse_publisher.broadcast('resource_update', { running: 3, total: 5 });
|
|
|
168
168
|
|
|
169
169
|
> **Note:** The new `Server.serve()` API is the recommended approach for most use cases. See [Simple Server API Design](docs/simple-server-api-design.md) for complete documentation and advanced features.
|
|
170
170
|
|
|
171
|
+
## Admin UI Dashboard
|
|
172
|
+
|
|
173
|
+
Every jsgui3-server instance includes a built-in admin dashboard at `/admin/v1` with live stats, resource inspection, route listing, and SSE-driven heartbeat updates. The dashboard is session-authenticated (default dev credentials: `admin` / `admin`).
|
|
174
|
+
|
|
175
|
+
The admin shell is implemented with jsgui controls for navigation and dynamic panel rendering (control-first composition), and is covered by the interaction regression suite in `tests/admin-ui-jsgui-controls.test.js`.
|
|
176
|
+
|
|
177
|
+
### Disabling the Admin UI
|
|
178
|
+
|
|
179
|
+
```javascript
|
|
180
|
+
Server.serve({ Ctrl: MyControl, admin: false });
|
|
181
|
+
|
|
182
|
+
// or
|
|
183
|
+
Server.serve({ Ctrl: MyControl, admin: { enabled: false } });
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Adding Custom Sections
|
|
187
|
+
|
|
188
|
+
Custom sections appear in the admin sidebar. When clicked, the shell fetches data from the section's API endpoint and auto-renders it as a table (arrays) or key-value panel (objects).
|
|
189
|
+
|
|
190
|
+
```javascript
|
|
191
|
+
const server = await Server.serve(MyControl);
|
|
192
|
+
|
|
193
|
+
server.admin_v1.add_section({
|
|
194
|
+
id: 'crawlers',
|
|
195
|
+
label: 'Crawlers',
|
|
196
|
+
icon: '\uD83D\uDD77\uFE0F',
|
|
197
|
+
api_path: '/api/admin/v1/crawlers',
|
|
198
|
+
handler: (req, res) => {
|
|
199
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
200
|
+
res.end(JSON.stringify([
|
|
201
|
+
{ name: 'Site A', status: 'running', pages: 1234 },
|
|
202
|
+
{ name: 'Site B', status: 'idle', pages: 0 }
|
|
203
|
+
]));
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Adding Custom Protected Endpoints
|
|
209
|
+
|
|
210
|
+
```javascript
|
|
211
|
+
server.admin_v1.add_endpoint({
|
|
212
|
+
path: '/api/admin/v1/crawlers/start',
|
|
213
|
+
role: 'admin_write',
|
|
214
|
+
handler: (req, res) => {
|
|
215
|
+
// start crawler logic
|
|
216
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
217
|
+
res.end(JSON.stringify({ ok: true }));
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Plugin Pattern
|
|
223
|
+
|
|
224
|
+
```javascript
|
|
225
|
+
server.admin_v1.use((admin) => {
|
|
226
|
+
admin.add_section({
|
|
227
|
+
id: 'logs',
|
|
228
|
+
label: 'Logs',
|
|
229
|
+
icon: '\uD83D\uDCDC',
|
|
230
|
+
api_path: '/api/admin/v1/logs',
|
|
231
|
+
handler: (req, res) => {
|
|
232
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
233
|
+
res.end(JSON.stringify({ recent: ['log1', 'log2'] }));
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Admin UI Regression Test
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
node tests/test-runner.js --test=admin-ui-jsgui-controls.test.js
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Declarative Configuration via `Server.serve()`
|
|
246
|
+
|
|
247
|
+
Custom sections and endpoints can also be declared in the `Server.serve()` call:
|
|
248
|
+
|
|
249
|
+
```javascript
|
|
250
|
+
Server.serve({
|
|
251
|
+
Ctrl: MyControl,
|
|
252
|
+
port: 8080,
|
|
253
|
+
admin: {
|
|
254
|
+
sections: [
|
|
255
|
+
{
|
|
256
|
+
id: 'jobs',
|
|
257
|
+
label: 'Jobs',
|
|
258
|
+
icon: '\u2699\uFE0F',
|
|
259
|
+
api_path: '/api/admin/v1/jobs',
|
|
260
|
+
handler: (req, res) => {
|
|
261
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
262
|
+
res.end(JSON.stringify([{ name: 'nightly-sync', status: 'complete' }]));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
],
|
|
266
|
+
endpoints: [
|
|
267
|
+
{
|
|
268
|
+
path: '/api/admin/v1/jobs/run',
|
|
269
|
+
role: 'admin_write',
|
|
270
|
+
handler: (req, res) => {
|
|
271
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
272
|
+
res.end(JSON.stringify({ ok: true }));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
]
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Exported Admin Classes
|
|
281
|
+
|
|
282
|
+
For advanced customisation, the admin classes are exported from the package:
|
|
283
|
+
|
|
284
|
+
```javascript
|
|
285
|
+
const Server = require('jsgui3-server');
|
|
286
|
+
|
|
287
|
+
// Available on the Server constructor:
|
|
288
|
+
Server.Admin_Module_V1 // Admin adapter (sections, endpoints, auth, SSE)
|
|
289
|
+
Server.Admin_Auth_Service // Session management, cookie handling, role checking
|
|
290
|
+
Server.Admin_User_Store // In-memory user credential store (scrypt)
|
|
291
|
+
|
|
292
|
+
// Also available from the npm module entry:
|
|
293
|
+
const jsgui = require('jsgui3-server');
|
|
294
|
+
jsgui.Admin_Module_V1
|
|
295
|
+
jsgui.Admin_Auth_Service
|
|
296
|
+
jsgui.Admin_User_Store
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
See [Admin Extension Guide](docs/admin-extension-guide.md) for detailed API reference.
|
|
300
|
+
|
|
171
301
|
## Architecture Overview
|
|
172
302
|
|
|
173
303
|
The server operates as a bridge between server-side JavaScript applications and browser clients, offering:
|
package/admin-ui/client.js
CHANGED
|
@@ -2,15 +2,19 @@ const jsgui = require('jsgui3-client');
|
|
|
2
2
|
const { controls, Control, mixins } = jsgui;
|
|
3
3
|
const Active_HTML_Document = require('../controls/Active_HTML_Document');
|
|
4
4
|
|
|
5
|
-
class Admin_Page extends Active_HTML_Document {
|
|
6
|
-
constructor(spec = {}) {
|
|
7
|
-
spec.__type_name = spec.__type_name || 'admin_page';
|
|
8
|
-
super(spec);
|
|
9
|
-
const { context } = this;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
class Admin_Page extends Active_HTML_Document {
|
|
6
|
+
constructor(spec = {}) {
|
|
7
|
+
spec.__type_name = spec.__type_name || 'admin_page';
|
|
8
|
+
super(spec);
|
|
9
|
+
const { context } = this;
|
|
10
|
+
|
|
11
|
+
this._menu_items = [];
|
|
12
|
+
this._section_labels = Object.create(null);
|
|
13
|
+
this._active_section = 'overview';
|
|
14
|
+
|
|
15
|
+
if (typeof this.body.add_class === 'function') {
|
|
16
|
+
this.body.add_class('admin-page');
|
|
17
|
+
}
|
|
14
18
|
|
|
15
19
|
const compose = () => {
|
|
16
20
|
// Sidebar
|
|
@@ -63,16 +67,60 @@ class Admin_Page extends Active_HTML_Document {
|
|
|
63
67
|
}
|
|
64
68
|
}
|
|
65
69
|
|
|
66
|
-
_add_menu_item(label, id, active = false) {
|
|
67
|
-
const item = new controls.div({
|
|
68
|
-
context: this.context,
|
|
69
|
-
class: `menu-item ${active ? 'active' : ''}`
|
|
70
|
-
});
|
|
71
|
-
item.dom.attributes['data-id'] = id;
|
|
72
|
-
item.add(label);
|
|
73
|
-
this.menu.add(item);
|
|
74
|
-
|
|
75
|
-
|
|
70
|
+
_add_menu_item(label, id, active = false) {
|
|
71
|
+
const item = new controls.div({
|
|
72
|
+
context: this.context,
|
|
73
|
+
class: `menu-item ${active ? 'active' : ''}`
|
|
74
|
+
});
|
|
75
|
+
item.dom.attributes['data-id'] = id;
|
|
76
|
+
item.add(label);
|
|
77
|
+
this.menu.add(item);
|
|
78
|
+
|
|
79
|
+
this._section_labels[id] = label;
|
|
80
|
+
this._menu_items.push({
|
|
81
|
+
id,
|
|
82
|
+
label,
|
|
83
|
+
control: item
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
item.on('click', () => {
|
|
87
|
+
this._activate_menu_item(id);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return item;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
_set_control_text(control, text) {
|
|
94
|
+
if (!control) return;
|
|
95
|
+
if (typeof control.clear === 'function') {
|
|
96
|
+
control.clear();
|
|
97
|
+
}
|
|
98
|
+
if (typeof control.add === 'function') {
|
|
99
|
+
control.add(String(text == null ? '' : text));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
_set_active_menu_item(id) {
|
|
104
|
+
this._menu_items.forEach((menu_item) => {
|
|
105
|
+
if (!menu_item.control) return;
|
|
106
|
+
if (menu_item.id === id) {
|
|
107
|
+
menu_item.control.add_class('active');
|
|
108
|
+
} else {
|
|
109
|
+
menu_item.control.remove_class('active');
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
_activate_menu_item(id) {
|
|
115
|
+
this._active_section = id;
|
|
116
|
+
this._set_active_menu_item(id);
|
|
117
|
+
|
|
118
|
+
const label = this._section_labels[id] || id;
|
|
119
|
+
this._set_control_text(this.page_title, label);
|
|
120
|
+
|
|
121
|
+
// Placeholder navigation logic
|
|
122
|
+
console.log('Navigate to:', id);
|
|
123
|
+
}
|
|
76
124
|
|
|
77
125
|
_render_overview() {
|
|
78
126
|
// Clear main content area (content_panel is the admin-content div)
|
|
@@ -86,30 +134,12 @@ class Admin_Page extends Active_HTML_Document {
|
|
|
86
134
|
this.content_panel.add(welcome);
|
|
87
135
|
}
|
|
88
136
|
|
|
89
|
-
activate() {
|
|
90
|
-
if (!this.__active) {
|
|
91
|
-
super.activate();
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
menu_items.forEach(el => {
|
|
96
|
-
el.addEventListener('click', () => {
|
|
97
|
-
// Update Active State
|
|
98
|
-
menu_items.forEach(i => i.classList.remove('active'));
|
|
99
|
-
el.classList.add('active');
|
|
100
|
-
|
|
101
|
-
// Update Title
|
|
102
|
-
const id = el.getAttribute('data-id');
|
|
103
|
-
const label = el.innerText;
|
|
104
|
-
document.querySelector('.page-title').innerText = label;
|
|
105
|
-
|
|
106
|
-
// Placeholder navigation logic
|
|
107
|
-
console.log('Navigate to:', id);
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
137
|
+
activate() {
|
|
138
|
+
if (!this.__active) {
|
|
139
|
+
super.activate();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
113
143
|
|
|
114
144
|
Admin_Page.css = `
|
|
115
145
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
class Admin_Auth_Service {
|
|
6
|
+
constructor(spec = {}) {
|
|
7
|
+
this.user_store = spec.user_store;
|
|
8
|
+
this.session_ttl_ms = Number.isFinite(spec.session_ttl_ms) ? spec.session_ttl_ms : (8 * 60 * 60 * 1000);
|
|
9
|
+
this.cookie_name = spec.cookie_name || 'jsgui_admin_v1_sid';
|
|
10
|
+
this.sessions = new Map();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
_read_json_body(req) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const chunks = [];
|
|
16
|
+
req.on('data', (chunk) => chunks.push(chunk));
|
|
17
|
+
req.on('end', () => {
|
|
18
|
+
if (chunks.length === 0) return resolve({});
|
|
19
|
+
try {
|
|
20
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
21
|
+
resolve(text ? JSON.parse(text) : {});
|
|
22
|
+
} catch (error) {
|
|
23
|
+
reject(error);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
req.on('error', reject);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_parse_cookies(req) {
|
|
31
|
+
const cookies = {};
|
|
32
|
+
const header = req && req.headers ? req.headers.cookie : '';
|
|
33
|
+
if (!header) return cookies;
|
|
34
|
+
|
|
35
|
+
const parts = header.split(';');
|
|
36
|
+
parts.forEach((part) => {
|
|
37
|
+
const idx = part.indexOf('=');
|
|
38
|
+
if (idx === -1) return;
|
|
39
|
+
const key = part.slice(0, idx).trim();
|
|
40
|
+
const value = part.slice(idx + 1).trim();
|
|
41
|
+
cookies[key] = decodeURIComponent(value);
|
|
42
|
+
});
|
|
43
|
+
return cookies;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_set_session_cookie(res, session_id) {
|
|
47
|
+
const secure = process.env.NODE_ENV === 'production' ? '; Secure' : '';
|
|
48
|
+
const cookie_value = this.cookie_name + '=' + encodeURIComponent(session_id) + '; Path=/; HttpOnly; SameSite=Lax; Max-Age=' + Math.floor(this.session_ttl_ms / 1000) + secure;
|
|
49
|
+
res.setHeader('Set-Cookie', cookie_value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_clear_session_cookie(res) {
|
|
53
|
+
const secure = process.env.NODE_ENV === 'production' ? '; Secure' : '';
|
|
54
|
+
res.setHeader('Set-Cookie', this.cookie_name + '=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0' + secure);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
_new_session_id() {
|
|
58
|
+
return crypto.randomBytes(24).toString('hex');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
_cleanup_expired() {
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
for (const [id, session] of this.sessions.entries()) {
|
|
64
|
+
if (!session || session.expires_at <= now) {
|
|
65
|
+
this.sessions.delete(id);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get_session(req) {
|
|
71
|
+
this._cleanup_expired();
|
|
72
|
+
const cookies = this._parse_cookies(req);
|
|
73
|
+
const sid = cookies[this.cookie_name];
|
|
74
|
+
if (!sid) return null;
|
|
75
|
+
|
|
76
|
+
const session = this.sessions.get(sid);
|
|
77
|
+
if (!session) return null;
|
|
78
|
+
if (session.expires_at <= Date.now()) {
|
|
79
|
+
this.sessions.delete(sid);
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
session_id: sid,
|
|
84
|
+
user: session.user,
|
|
85
|
+
expires_at: session.expires_at
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
is_authenticated(req) {
|
|
90
|
+
return !!this.get_session(req);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
has_role(req, role_name) {
|
|
94
|
+
const session = this.get_session(req);
|
|
95
|
+
if (!session || !session.user) return false;
|
|
96
|
+
const roles = Array.isArray(session.user.roles) ? session.user.roles : [];
|
|
97
|
+
return roles.includes(role_name);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
has_any_role(req, role_names) {
|
|
101
|
+
const session = this.get_session(req);
|
|
102
|
+
if (!session || !session.user) return false;
|
|
103
|
+
const roles = Array.isArray(session.user.roles) ? session.user.roles : [];
|
|
104
|
+
if (!Array.isArray(role_names) || role_names.length === 0) return false;
|
|
105
|
+
return role_names.some((role_name) => roles.includes(role_name));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
create_session(user, res) {
|
|
109
|
+
const session_id = this._new_session_id();
|
|
110
|
+
const expires_at = Date.now() + this.session_ttl_ms;
|
|
111
|
+
this.sessions.set(session_id, {
|
|
112
|
+
user,
|
|
113
|
+
created_at: Date.now(),
|
|
114
|
+
expires_at
|
|
115
|
+
});
|
|
116
|
+
this._set_session_cookie(res, session_id);
|
|
117
|
+
return { session_id, expires_at, user };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
destroy_session(req, res) {
|
|
121
|
+
const cookies = this._parse_cookies(req);
|
|
122
|
+
const sid = cookies[this.cookie_name];
|
|
123
|
+
if (sid) this.sessions.delete(sid);
|
|
124
|
+
this._clear_session_cookie(res);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async handle_login(req, res) {
|
|
128
|
+
if (String(req.method || 'GET').toUpperCase() !== 'POST') {
|
|
129
|
+
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
130
|
+
res.end(JSON.stringify({ ok: false, error: 'Method Not Allowed' }));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let body;
|
|
135
|
+
try {
|
|
136
|
+
body = await this._read_json_body(req);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
139
|
+
res.end(JSON.stringify({ ok: false, error: 'Invalid JSON body' }));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const username = body.username;
|
|
144
|
+
const password = body.password;
|
|
145
|
+
const user = this.user_store.verify_credentials(username, password);
|
|
146
|
+
if (!user) {
|
|
147
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
148
|
+
res.end(JSON.stringify({ ok: false, error: 'Invalid credentials' }));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const session = this.create_session(user, res);
|
|
153
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
154
|
+
res.end(JSON.stringify({
|
|
155
|
+
ok: true,
|
|
156
|
+
user: session.user,
|
|
157
|
+
expires_at: session.expires_at
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
handle_logout(req, res) {
|
|
162
|
+
if (String(req.method || 'GET').toUpperCase() !== 'POST') {
|
|
163
|
+
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
164
|
+
res.end(JSON.stringify({ ok: false, error: 'Method Not Allowed' }));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
this.destroy_session(req, res);
|
|
169
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
170
|
+
res.end(JSON.stringify({ ok: true }));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
handle_session(req, res) {
|
|
174
|
+
if (String(req.method || 'GET').toUpperCase() !== 'GET') {
|
|
175
|
+
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
176
|
+
res.end(JSON.stringify({ ok: false, error: 'Method Not Allowed' }));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const session = this.get_session(req);
|
|
181
|
+
if (!session) {
|
|
182
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
183
|
+
res.end(JSON.stringify({ ok: true, authenticated: false }));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
188
|
+
res.end(JSON.stringify({
|
|
189
|
+
ok: true,
|
|
190
|
+
authenticated: true,
|
|
191
|
+
user: session.user,
|
|
192
|
+
expires_at: session.expires_at
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = Admin_Auth_Service;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
class Admin_User_Store {
|
|
6
|
+
constructor(spec = {}) {
|
|
7
|
+
this._users = new Map();
|
|
8
|
+
this._scrypt_cost = spec.scrypt_cost || 16384;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
_hash_password(password, salt) {
|
|
12
|
+
const key = crypto.scryptSync(password, salt, 64, { N: this._scrypt_cost });
|
|
13
|
+
return key.toString('hex');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
add_user(spec = {}) {
|
|
17
|
+
const username = String(spec.username || '').trim();
|
|
18
|
+
const password = String(spec.password || '');
|
|
19
|
+
const roles = Array.isArray(spec.roles) ? spec.roles : ['admin_read'];
|
|
20
|
+
|
|
21
|
+
if (!username) throw new Error('username is required');
|
|
22
|
+
if (!password) throw new Error('password is required');
|
|
23
|
+
|
|
24
|
+
const salt = crypto.randomBytes(16).toString('hex');
|
|
25
|
+
const password_hash = this._hash_password(password, salt);
|
|
26
|
+
|
|
27
|
+
this._users.set(username, {
|
|
28
|
+
username,
|
|
29
|
+
salt,
|
|
30
|
+
password_hash,
|
|
31
|
+
roles,
|
|
32
|
+
created_at: Date.now()
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return { username, roles: roles.slice() };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
has_user(username) {
|
|
39
|
+
return this._users.has(username);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get_user(username) {
|
|
43
|
+
const user = this._users.get(username);
|
|
44
|
+
if (!user) return null;
|
|
45
|
+
return {
|
|
46
|
+
username: user.username,
|
|
47
|
+
roles: user.roles.slice(),
|
|
48
|
+
created_at: user.created_at
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
verify_credentials(username, password) {
|
|
53
|
+
const user = this._users.get(String(username || ''));
|
|
54
|
+
if (!user) return null;
|
|
55
|
+
|
|
56
|
+
const attempted_hash = this._hash_password(String(password || ''), user.salt);
|
|
57
|
+
const expected = Buffer.from(user.password_hash, 'hex');
|
|
58
|
+
const attempted = Buffer.from(attempted_hash, 'hex');
|
|
59
|
+
|
|
60
|
+
if (expected.length !== attempted.length) return null;
|
|
61
|
+
const ok = crypto.timingSafeEqual(expected, attempted);
|
|
62
|
+
if (!ok) return null;
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
username: user.username,
|
|
66
|
+
roles: user.roles.slice()
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = Admin_User_Store;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Admin UI v1 — client entry point.
|
|
5
|
+
*
|
|
6
|
+
* This file is the ESBuild entry point that the HTTP_Webpage_Publisher
|
|
7
|
+
* bundles and serves. It imports the Admin_Shell control (which
|
|
8
|
+
* transitively pulls in Group_Box and Stat_Card) and exports the
|
|
9
|
+
* jsgui module with Admin_Shell registered on controls.
|
|
10
|
+
*/
|
|
11
|
+
const jsgui = require('./controls/admin_shell');
|
|
12
|
+
|
|
13
|
+
// The require above already registers controls.Admin_Shell,
|
|
14
|
+
// controls.Stat_Card, and controls.Group_Box on the jsgui
|
|
15
|
+
// controls namespace.
|
|
16
|
+
|
|
17
|
+
module.exports = jsgui;
|