jsgui3-server 0.0.149 → 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/agents/Mobile Developer.agent.md +89 -0
- package/.github/instructions/copilot.instructions.md +1 -0
- package/AGENTS.md +6 -0
- package/README.md +185 -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/api-reference.md +383 -303
- 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/comprehensive-documentation.md +220 -220
- package/docs/configuration-reference.md +227 -204
- package/docs/designs/server-admin-interface-aero.svg +611 -0
- package/docs/middleware-guide.md +236 -0
- package/docs/system-architecture.md +24 -18
- package/docs/troubleshooting.md +84 -53
- package/middleware/compression.js +217 -0
- package/middleware/index.js +15 -0
- package/module.js +19 -11
- package/package.json +1 -1
- package/serve-factory.js +29 -0
- package/server.js +280 -20
- 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)
|
|
@@ -176,5 +176,6 @@ model.count++; // Triggers change event
|
|
|
176
176
|
| Comprehensive API | `docs/comprehensive-documentation.md` |
|
|
177
177
|
| Control development | `docs/controls-development.md` |
|
|
178
178
|
| Publisher system | `docs/publishers-guide.md` |
|
|
179
|
+
| Middleware & compression | `docs/middleware-guide.md` |
|
|
179
180
|
| Troubleshooting | `docs/troubleshooting.md` |
|
|
180
181
|
| **Broken stuff** | `docs/agent-development-guide.md` |
|
package/AGENTS.md
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
- **[docs/controls-development.md](docs/controls-development.md)** - Guide for developing custom JSGUI3 controls
|
|
20
20
|
- **[docs/publishers-guide.md](docs/publishers-guide.md)** - Guide for publishers and content serving
|
|
21
21
|
- **[docs/resources-guide.md](docs/resources-guide.md)** - Guide for resources and data abstraction
|
|
22
|
+
- **[docs/middleware-guide.md](docs/middleware-guide.md)** - Middleware pipeline and built-in compression middleware
|
|
22
23
|
|
|
23
24
|
### Specialized Documentation
|
|
24
25
|
- **[docs/GUIDE_TO_AGENTIC_WORKFLOWS_BY_GROK.md](docs/GUIDE_TO_AGENTIC_WORKFLOWS_BY_GROK.md)** - Comprehensive guide to agentic workflows and autonomous task execution
|
|
@@ -36,6 +37,9 @@
|
|
|
36
37
|
- **[docs/agent-development-guide.md](docs/agent-development-guide.md)** - Guide for AI agents working on this codebase
|
|
37
38
|
- **[docs/broken-functionality-tracker.md](docs/broken-functionality-tracker.md)** - Tracker for broken/incomplete functionality
|
|
38
39
|
|
|
40
|
+
### Admin UI
|
|
41
|
+
- **[docs/admin-extension-guide.md](docs/admin-extension-guide.md)** - Admin UI extension API: custom sections, endpoints, plugins, exported classes
|
|
42
|
+
|
|
39
43
|
### Review and Maintenance
|
|
40
44
|
- **[docs/documentation-review/CURRENT_REVIEW.md](docs/documentation-review/CURRENT_REVIEW.md)** - Current documentation review status and known issues
|
|
41
45
|
|
|
@@ -58,6 +62,8 @@
|
|
|
58
62
|
- **Custom control development** → `docs/controls-development.md`
|
|
59
63
|
- **Publisher system** → `docs/publishers-guide.md`
|
|
60
64
|
- **Resource management** → `docs/resources-guide.md`
|
|
65
|
+
- **Middleware and compression** → `docs/middleware-guide.md`
|
|
66
|
+
- **Admin UI extensions** → `docs/admin-extension-guide.md`
|
|
61
67
|
|
|
62
68
|
### Agent Development
|
|
63
69
|
- **Agentic workflow patterns** → `docs/GUIDE_TO_AGENTIC_WORKFLOWS_BY_GROK.md`
|
package/README.md
CHANGED
|
@@ -168,6 +168,191 @@ 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
|
+
|
|
301
|
+
## Middleware
|
|
302
|
+
|
|
303
|
+
jsgui3-server includes an Express-style `server.use()` middleware pipeline. Middleware runs before every request reaches the router.
|
|
304
|
+
|
|
305
|
+
### Built-in Compression
|
|
306
|
+
|
|
307
|
+
Enable gzip/deflate/brotli compression for JSON, HTML, CSS, and JS responses:
|
|
308
|
+
|
|
309
|
+
```javascript
|
|
310
|
+
const { compression } = require('jsgui3-server/middleware');
|
|
311
|
+
|
|
312
|
+
const server = new Server({ Ctrl: MyControl, src_path_client_js: __dirname + '/client.js' });
|
|
313
|
+
server.use(compression());
|
|
314
|
+
server.on('ready', () => server.start(8080));
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Or via `Server.serve()`:
|
|
318
|
+
|
|
319
|
+
```javascript
|
|
320
|
+
Server.serve({
|
|
321
|
+
Ctrl: MyControl,
|
|
322
|
+
compression: true, // enable with defaults
|
|
323
|
+
port: 8080
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// With options:
|
|
327
|
+
Server.serve({
|
|
328
|
+
Ctrl: MyControl,
|
|
329
|
+
compression: { threshold: 512 },
|
|
330
|
+
port: 8080
|
|
331
|
+
});
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Custom Middleware
|
|
335
|
+
|
|
336
|
+
```javascript
|
|
337
|
+
// Request logger
|
|
338
|
+
server.use((req, res, next) => {
|
|
339
|
+
console.log(`${req.method} ${req.url}`);
|
|
340
|
+
next();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// Multiple middleware via Server.serve()
|
|
344
|
+
Server.serve({
|
|
345
|
+
Ctrl: MyControl,
|
|
346
|
+
middleware: [
|
|
347
|
+
(req, res, next) => { console.log(req.url); next(); },
|
|
348
|
+
compression({ threshold: 512 })
|
|
349
|
+
],
|
|
350
|
+
port: 8080
|
|
351
|
+
});
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
See [Middleware Guide](docs/middleware-guide.md) for the full API reference and response-wrapping patterns.
|
|
355
|
+
|
|
171
356
|
## Architecture Overview
|
|
172
357
|
|
|
173
358
|
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;
|