jsgui3-server 0.0.147 → 0.0.148

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.
@@ -0,0 +1,213 @@
1
+ const jsgui = require('jsgui3-client');
2
+ const { controls, Control, mixins } = jsgui;
3
+ const Active_HTML_Document = require('../controls/Active_HTML_Document');
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
+ if (typeof this.body.add_class === 'function') {
12
+ this.body.add_class('admin-page');
13
+ }
14
+
15
+ const compose = () => {
16
+ // Sidebar
17
+ const sidebar = new controls.div({ context, class: 'admin-sidebar' });
18
+ this.body.add(sidebar);
19
+ this.sidebar = sidebar;
20
+
21
+ // Sidebar Header
22
+ const brand = new controls.div({ context, class: 'admin-brand' });
23
+ brand.add(new controls.span({ context, class: 'brand-icon' }).add('⚙️'));
24
+ brand.add(new controls.span({ context, class: 'brand-text' }).add('jsgui3 Admin'));
25
+ sidebar.add(brand);
26
+
27
+ // Menu Container (Placeholder for Resource_List and Observables_List)
28
+ const menu = new controls.div({ context, class: 'admin-menu' });
29
+ sidebar.add(menu);
30
+ this.menu = menu;
31
+
32
+ this._add_menu_item('Overview', 'overview', true);
33
+ this._add_menu_item('Resources', 'resources');
34
+ this._add_menu_item('Observables', 'observables');
35
+ this._add_menu_item('Settings', 'settings');
36
+
37
+
38
+ // Main Content Area
39
+ const main = new controls.div({ context, class: 'admin-main' });
40
+ this.body.add(main);
41
+ this.main = main;
42
+
43
+ // Top Bar
44
+ const top_bar = new controls.div({ context, class: 'admin-top-bar' });
45
+ main.add(top_bar);
46
+
47
+ // Breadcrumbs / Title
48
+ this.page_title = new controls.h2({ context, class: 'page-title' });
49
+ this.page_title.add('Overview');
50
+ top_bar.add(this.page_title);
51
+
52
+ // Content Panel
53
+ const content = new controls.div({ context, class: 'admin-content' });
54
+ main.add(content);
55
+ this.content = content;
56
+
57
+ // Default content (Overview)
58
+ this._render_overview();
59
+ };
60
+
61
+ if (!spec.el) {
62
+ compose();
63
+ }
64
+ }
65
+
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
+ return item;
75
+ }
76
+
77
+ _render_overview() {
78
+ // Clear main content area (this.content is the admin-content div)
79
+ if (this.content && this.content.content && typeof this.content.content.clear === 'function') {
80
+ this.content.content.clear();
81
+ }
82
+
83
+ const welcome = new controls.div({ context: this.context, class: 'welcome-card' });
84
+ welcome.add(new controls.h3({ context: this.context }).add('Welcome directly to jsgui3-server Admin'));
85
+ welcome.add(new controls.p({ context: this.context }).add('Select a resource from the sidebar to inspect.'));
86
+ this.content.add(welcome);
87
+ }
88
+
89
+ activate() {
90
+ if (!this.__active) {
91
+ super.activate();
92
+
93
+ // Handle menu clicks
94
+ const menu_items = document.querySelectorAll('.menu-item');
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
+ }
113
+
114
+ Admin_Page.css = `
115
+ * { box-sizing: border-box; margin: 0; padding: 0; }
116
+ body {
117
+ background: #1a1a2e;
118
+ color: #e0e0e0;
119
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
120
+ height: 100vh;
121
+ overflow: hidden;
122
+ }
123
+ .admin-page {
124
+ display: grid;
125
+ grid-template-columns: 260px 1fr;
126
+ height: 100%;
127
+ }
128
+
129
+ /* Sidebar */
130
+ .admin-sidebar {
131
+ background: #16213e;
132
+ border-right: 1px solid #2a2a4a;
133
+ display: flex;
134
+ flex-direction: column;
135
+ }
136
+ .admin-brand {
137
+ padding: 20px;
138
+ font-size: 1.2rem;
139
+ font-weight: bold;
140
+ color: #fff;
141
+ border-bottom: 1px solid #2a2a4a;
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 10px;
145
+ }
146
+ .brand-icon { filter: drop-shadow(0 0 5px rgba(255,255,255,0.3)); }
147
+
148
+ .admin-menu {
149
+ flex: 1;
150
+ padding: 20px 0;
151
+ overflow-y: auto;
152
+ }
153
+ .menu-item {
154
+ padding: 12px 24px;
155
+ cursor: pointer;
156
+ border-left: 3px solid transparent;
157
+ transition: all 0.2s;
158
+ color: #a0a0b0;
159
+ }
160
+ .menu-item:hover {
161
+ background: rgba(255,255,255,0.05);
162
+ color: #fff;
163
+ }
164
+ .menu-item.active {
165
+ background: rgba(79, 172, 254, 0.1);
166
+ color: #4facfe;
167
+ border-left-color: #4facfe;
168
+ }
169
+
170
+ /* Main Area */
171
+ .admin-main {
172
+ display: flex;
173
+ flex-direction: column;
174
+ background: #1a1a2e;
175
+ }
176
+ .admin-top-bar {
177
+ height: 64px;
178
+ border-bottom: 1px solid #2a2a4a;
179
+ display: flex;
180
+ align-items: center;
181
+ padding: 0 30px;
182
+ background: #16213e;
183
+ }
184
+ .page-title {
185
+ font-size: 1.2rem;
186
+ font-weight: 500;
187
+ color: #fff;
188
+ }
189
+
190
+ .admin-content {
191
+ flex: 1;
192
+ overflow-y: auto;
193
+ padding: 30px;
194
+ position: relative;
195
+ }
196
+
197
+ /* Cards */
198
+ .welcome-card {
199
+ background: #2a2a4a;
200
+ padding: 40px;
201
+ border-radius: 12px;
202
+ text-align: center;
203
+ max-width: 600px;
204
+ margin: 40px auto;
205
+ border: 1px solid #3a3a5a;
206
+ box-shadow: 0 4px 20px rgba(0,0,0,0.2);
207
+ }
208
+ .welcome-card h3 { color: #fff; margin-bottom: 15px; }
209
+ .welcome-card p { color: #a0a0b0; }
210
+ `;
211
+
212
+ controls.Admin_Page = Admin_Page;
213
+ module.exports = jsgui;
@@ -0,0 +1,104 @@
1
+ const jsgui = require('./client');
2
+ const { Admin_Page } = jsgui.controls;
3
+ const { each } = jsgui;
4
+
5
+ class Admin_Module {
6
+ constructor(server) {
7
+ this.server = server;
8
+ this.setup_routes();
9
+ }
10
+
11
+ setup_routes() {
12
+ const { server } = this;
13
+
14
+ // 1. Main Admin Page Route
15
+ // Using a custom Website_Resource for the admin app
16
+ // We register Admin_Page as the content control
17
+
18
+ // Manual route setup to render the page
19
+ // (Similar to how examples work, but integrated)
20
+
21
+ // We'll expose a 'setup' method that the main server calls
22
+ // Or we can attach directly if we have access to the router
23
+ }
24
+
25
+ // Called by the main server to attach admin functionality
26
+ attach_to_router(router) {
27
+ console.log('[Admin_Module] Attaching /admin routes...');
28
+
29
+ // API: List Resources
30
+ // GET /api/admin/resources
31
+ router.set_route('/api/admin/resources', (req, res) => {
32
+ const resources_data = this.get_resources_tree();
33
+ res.writeHead(200, { 'Content-Type': 'application/json' });
34
+ res.end(JSON.stringify(resources_data));
35
+ });
36
+
37
+ // API: List Observables
38
+ // GET /api/admin/observables
39
+ router.set_route('/api/admin/observables', (req, res) => {
40
+ const observables = this.get_observables_list();
41
+ res.writeHead(200, { 'Content-Type': 'application/json' });
42
+ res.end(JSON.stringify(observables));
43
+ });
44
+ }
45
+
46
+ get_resources_tree() {
47
+ const pool = this.server.resource_pool;
48
+ const tree = {
49
+ name: 'Root',
50
+ type: 'pool',
51
+ children: []
52
+ };
53
+
54
+ if (pool && pool.resources) {
55
+ // Need to iterate pool resources safely
56
+ // resource_pool.resources is likely a Collection or array
57
+ const resources = pool.resources._arr || []; // Assuming Data_Structures.Collection
58
+
59
+ resources.forEach(res => {
60
+ tree.children.push({
61
+ name: res.name || 'Unnamed Resource',
62
+ type: res.constructor.name
63
+ });
64
+ });
65
+ }
66
+ return tree;
67
+ }
68
+
69
+ get_observables_list() {
70
+ // We need a way to track all published observables
71
+ // Ideally, HTTP_Observable_Publisher instances are stored in the resource pool or a specific list
72
+
73
+ // For now, we'll scan the router for observable publishers
74
+ // This relies on the server exposing its router/routes map
75
+ const observables = [];
76
+
77
+ // Implementation detail: server.router doesn't expose a simple list of routes easily in standard Router
78
+ // We might need to track them when publish_observable is called.
79
+ // But we can look at server.resource_pool for publishers
80
+
81
+ const pool = this.server.resource_pool;
82
+ if (pool && pool.resources) {
83
+ const resources = pool.resources._arr || [];
84
+ resources.forEach(res => {
85
+ // Check if it's an observable publisher
86
+ // Robust check: does it have 'obs' property and handle_http?
87
+ // Or check constructor name if available
88
+ if (res.constructor.name === 'Observable_Publisher' || (res.obs && res.handle_http)) {
89
+ observables.push({
90
+ name: res.name || 'Observable',
91
+ route: '?', // Route might not be stored on the resource itself, but on the router
92
+ schema: res.schema,
93
+ status: res.is_paused ? 'paused' : 'active',
94
+ connections: res.active_sse_connections ? res.active_sse_connections.size : 0
95
+ });
96
+ }
97
+ });
98
+ }
99
+
100
+ return observables;
101
+ }
102
+ }
103
+
104
+ module.exports = Admin_Module;
@@ -0,0 +1,207 @@
1
+ const jsgui = require('jsgui3-client');
2
+ const { Control, controls } = jsgui;
3
+
4
+ class Auto_Observable_UI extends Control {
5
+ constructor(spec = {}) {
6
+ spec.__type_name = spec.__type_name || 'auto_observable_ui';
7
+ super(spec);
8
+ this.add_class('auto-observable-ui');
9
+
10
+ this.url = spec.url;
11
+ this.obs = spec.obs;
12
+
13
+ // Container for the dynamic content
14
+ this.content_container = new controls.div({
15
+ context: this.context,
16
+ class: 'content-container'
17
+ });
18
+ this.add(this.content_container);
19
+
20
+ // Status indicator
21
+ this.status_indicator = new controls.div({
22
+ context: this.context,
23
+ class: 'status-indicator status-connecting'
24
+ });
25
+ this.add(this.status_indicator);
26
+ }
27
+
28
+ activate() {
29
+ if (!this.__active) {
30
+ super.activate();
31
+ this._connect();
32
+ }
33
+ }
34
+
35
+ _connect() {
36
+ let obs = this.obs;
37
+ if (!obs && this.url) {
38
+ obs = new jsgui.Remote_Observable({ url: this.url });
39
+ this.obs = obs;
40
+ }
41
+
42
+ if (!obs) return;
43
+
44
+ obs.on('connect', () => {
45
+ this.status_indicator.remove_class('status-connecting');
46
+ this.status_indicator.add_class('status-connected');
47
+ this.status_indicator.dom.innerText = 'Connected';
48
+ });
49
+
50
+ obs.on('schema', (schema) => {
51
+ console.log('Schema received:', schema);
52
+ this._build_ui_from_schema(schema);
53
+ });
54
+
55
+ obs.on('next', (data) => {
56
+ // If we haven't received a schema yet, maybe infer it or just display raw?
57
+ // For now, if we built the UI, we update it.
58
+ if (this._update_handler) {
59
+ this._update_handler(data);
60
+ } else if (!this._ui_built) {
61
+ // Infer simple schema if not provided?
62
+ // Or just show raw JSON
63
+ this._build_default_ui(data);
64
+ this._ui_built = true;
65
+ if (this._update_handler) this._update_handler(data);
66
+ }
67
+ });
68
+
69
+ obs.on('error', (err) => {
70
+ this.status_indicator.add_class('status-error');
71
+ this.status_indicator.dom.innerText = 'Error: ' + err.message;
72
+ });
73
+
74
+ obs.connect();
75
+ }
76
+
77
+ _build_ui_from_schema(schema) {
78
+ if (this._ui_built) return; // Don't rebuild for now
79
+ this._ui_built = true;
80
+
81
+ this.content_container.content.clear();
82
+
83
+ const type = schema.type || (schema.output_type ? schema.output_type : 'unknown');
84
+
85
+ if (type === 'int' || type === 'number') {
86
+ this._build_number_ui(schema);
87
+ } else if (type === 'text' || type === 'log') {
88
+ this._build_log_ui(schema);
89
+ } else if (type === 'percentage' || (type === 'number' && schema.min !== undefined && schema.max !== undefined)) {
90
+ this._build_gauge_ui(schema);
91
+ } else {
92
+ this._build_json_ui(schema);
93
+ }
94
+ }
95
+
96
+ _build_default_ui(first_data) {
97
+ if (typeof first_data === 'number') {
98
+ this._build_number_ui({ type: 'number' });
99
+ } else if (typeof first_data === 'string') {
100
+ this._build_log_ui({ type: 'text' });
101
+ } else {
102
+ this._build_json_ui({ type: 'object' });
103
+ }
104
+ }
105
+
106
+ _build_number_ui(schema) {
107
+ const val_div = new controls.div({
108
+ context: this.context,
109
+ class: 'value-display number-display'
110
+ });
111
+ this.content_container.add(val_div);
112
+
113
+ const label = new controls.h3({ context: this.context });
114
+ label.add(schema.name || 'Value');
115
+ val_div.add(label);
116
+
117
+ const value_text = new controls.div({ context: this.context, class: 'value-text' });
118
+ value_text.add('--');
119
+ val_div.add(value_text);
120
+
121
+ this._update_handler = (data) => {
122
+ let val = data;
123
+ if (typeof data === 'object' && data.value !== undefined) val = data.value;
124
+ value_text.dom.innerText = String(val);
125
+ };
126
+ }
127
+
128
+ _build_log_ui(schema) {
129
+ const log_container = new controls.div({
130
+ context: this.context,
131
+ class: 'value-display log-display'
132
+ });
133
+ this.content_container.add(log_container);
134
+
135
+ const label = new controls.h3({ context: this.context });
136
+ label.add(schema.name || 'Log');
137
+ log_container.add(label);
138
+
139
+ const log_area = new controls.div({ context: this.context, class: 'log-area' });
140
+ log_container.add(log_area);
141
+
142
+ this._update_handler = (data) => {
143
+ const entry = document.createElement('div');
144
+ entry.className = 'log-entry';
145
+ let msg = data;
146
+ if (typeof data === 'object') msg = data.message || JSON.stringify(data);
147
+
148
+ entry.innerText = `[${new Date().toLocaleTimeString()}] ${msg}`;
149
+ log_area.dom.appendChild(entry);
150
+ log_area.dom.scrollTop = log_area.dom.scrollHeight;
151
+ };
152
+ }
153
+
154
+ _build_gauge_ui(schema) {
155
+ // Simple progress bar for now
156
+ const container = new controls.div({
157
+ context: this.context,
158
+ class: 'value-display gauge-display'
159
+ });
160
+ this.content_container.add(container);
161
+
162
+ const label = new controls.h3({ context: this.context });
163
+ label.add(schema.name || 'Gauge');
164
+ container.add(label);
165
+
166
+ const bar_bg = new controls.div({ context: this.context, class: 'progress-bg' });
167
+ const bar_fill = new controls.div({ context: this.context, class: 'progress-fill' });
168
+ bar_bg.add(bar_fill);
169
+ container.add(bar_bg);
170
+
171
+ const value_text = new controls.div({ context: this.context, class: 'value-text-small' });
172
+ container.add(value_text);
173
+
174
+ const min = schema.min || 0;
175
+ const max = schema.max || 100;
176
+
177
+ this._update_handler = (data) => {
178
+ let val = data;
179
+ if (typeof data === 'object' && data.value !== undefined) val = data.value;
180
+
181
+ const pct = Math.max(0, Math.min(100, ((val - min) / (max - min)) * 100));
182
+ bar_fill.dom.style.width = pct + '%';
183
+ value_text.dom.innerText = `${val} / ${max}`;
184
+ }
185
+ }
186
+
187
+ _build_json_ui(schema) {
188
+ const container = new controls.div({
189
+ context: this.context,
190
+ class: 'value-display json-display'
191
+ });
192
+ this.content_container.add(container);
193
+
194
+ const label = new controls.h3({ context: this.context });
195
+ label.add(schema.name || 'Data');
196
+ container.add(label);
197
+
198
+ const pre = new controls.pre({ context: this.context });
199
+ container.add(pre);
200
+
201
+ this._update_handler = (data) => {
202
+ pre.dom.innerText = JSON.stringify(data, null, 2);
203
+ }
204
+ }
205
+ }
206
+
207
+ module.exports = Auto_Observable_UI;
@@ -0,0 +1,32 @@
1
+ # Chapter 1: Introduction
2
+
3
+ ## Vision
4
+
5
+ The Admin UI is the **go-to interface** for developers and operators to administer `jsgui3-server` instances. It should be:
6
+
7
+ - **Instantaneous**: Zero setup required; just navigate to `/admin`
8
+ - **Insightful**: Surface the internal state of the server in real-time
9
+ - **Actionable**: Allow common admin tasks directly from the UI
10
+
11
+ ## Goals
12
+
13
+ 1. **Visibility into Observables**: If a server publishes an observable (e.g., for crawl progress, metrics), the Admin UI should display it automatically with an appropriate control.
14
+ 2. **Resource Browser**: List all registered resources (routes, publishers, static paths) with introspection.
15
+ 3. **Configuration Panel**: View and modify server configuration (where safe).
16
+ 4. **Performance Metrics**: Display throughput, active connections, memory usage.
17
+
18
+ ## Inspiration
19
+
20
+ The design is inspired by:
21
+ - Database admin tools (pgAdmin, phpMyAdmin)
22
+ - Monitoring dashboards (Grafana, Kibana)
23
+ - The jsgui3 Window control aesthetic
24
+
25
+ ## Target Experience
26
+
27
+ A developer starts a jsgui3-server, then navigates to `http://localhost:PORT/admin`. They immediately see:
28
+ - A sidebar listing all resources
29
+ - A main panel showing the selected resource's details
30
+ - Real-time updates for any observables
31
+
32
+ No extra code required—the Admin UI is built into jsgui3-server and activates automatically.
@@ -0,0 +1,92 @@
1
+ # Chapter 2: Architecture
2
+
3
+ ## High-Level Overview
4
+
5
+ ```
6
+ ┌─────────────────────────────────────────────────────────────┐
7
+ │ jsgui3-server │
8
+ │ │
9
+ │ ┌──────────────────┐ ┌────────────────┐ │
10
+ │ │ Admin UI Module │◄──│ Server Router │ │
11
+ │ │ (admin-ui/) │ └───────┬────────┘ │
12
+ │ └────────┬─────────┘ │ │
13
+ │ │ ┌───────┴────────┐ │
14
+ │ ▼ │ Resource Pool │ │
15
+ │ ┌─────────────────┐ │ (resources/) │ │
16
+ │ │ Admin_Page │ └───────┬────────┘ │
17
+ │ │ (client.js) │ │ │
18
+ │ └────────┬────────┘ ┌───────┴────────┐ │
19
+ │ │ │ Publishers │ │
20
+ │ ▼ │ (observables) │ │
21
+ │ ┌─────────────────┐ └────────────────┘ │
22
+ │ │ Controls: │ │
23
+ │ │ - Resource_List │ │
24
+ │ │ - Observable_ │ │
25
+ │ │ Monitor │ │
26
+ │ │ - Config_Panel │ │
27
+ │ └─────────────────┘ │
28
+ └─────────────────────────────────────────────────────────────┘
29
+ ```
30
+
31
+ ## Module Structure
32
+
33
+ ```
34
+ admin-ui/
35
+ ├── client.js # Main client control (Admin_Page)
36
+ ├── server.js # API routes for admin data
37
+ ├── controls/ # UI components
38
+ │ ├── Resource_List.js
39
+ │ ├── Observable_Monitor.js
40
+ │ ├── Config_Panel.js
41
+ │ └── Metrics_Dashboard.js
42
+ └── styles/ # CSS modules
43
+ └── admin.css
44
+ ```
45
+
46
+ ## Key Patterns (from Window examples)
47
+
48
+ 1. **`client.js`** exports a jsgui module with controls registered on `jsgui.controls`
49
+ 2. **`server.js`** creates a `Server` instance, passes `Ctrl` and `src_path_client_js`
50
+ 3. Controls extend `Active_HTML_Document` for full-page apps
51
+ 4. CSS is defined as a static `.css` property on the control class
52
+ 5. `activate()` sets up client-side interactivity
53
+
54
+ ## Data Flow
55
+
56
+ 1. **Server Start** → Admin module registers `/admin` route
57
+ 2. **Client Request** → SSR renders Admin_Page with initial state
58
+ 3. **Activation** → Client connects to `/api/admin/resources` (SSE)
59
+ 4. **Updates** → Resource changes stream to client in real-time
60
+
61
+ ## Integration with `publish_observable`
62
+
63
+ The Admin UI leverages the schema-aware observable system:
64
+ - Server exposes `/api/admin/observables` listing all published observables
65
+ - Each observable has a schema describing its data type
66
+ - `Auto_Observable_UI` renders appropriate controls automatically
67
+
68
+ ## Server Lifecycle Events
69
+
70
+ The server emits two distinct lifecycle events:
71
+
72
+ | Event | Fired When | Use Case |
73
+ |-------|------------|----------|
74
+ | `ready` | Publishers/bundlers complete | Safe to call `server.start()`, publish routes |
75
+ | `listening` | HTTP server bound to ports | Server accepting connections |
76
+
77
+ **Example usage:**
78
+ ```javascript
79
+ server.on('ready', () => {
80
+ // Publish observables and set up routes
81
+ server.publish_observable('/api/data', myObservable);
82
+
83
+ // Now start accepting connections
84
+ server.start(port, (err) => {
85
+ console.log('Server running');
86
+ });
87
+ });
88
+
89
+ server.on('listening', () => {
90
+ console.log('Server accepting connections');
91
+ });
92
+ ```