jsgui3-server 0.0.138 → 0.0.140
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/AGENTS.md +87 -0
- package/README.md +12 -0
- package/docs/GUIDE_TO_AGENTIC_WORKFLOWS_BY_GROK.md +19 -0
- package/docs/advanced-usage-examples.md +1360 -0
- package/docs/agent-development-guide.md +386 -0
- package/docs/api-reference.md +916 -0
- package/docs/broken-functionality-tracker.md +285 -0
- package/docs/bundling-system-deep-dive.md +525 -0
- package/docs/cli-reference.md +393 -0
- package/docs/comprehensive-documentation.md +1403 -0
- package/docs/configuration-reference.md +808 -0
- package/docs/controls-development.md +859 -0
- package/docs/documentation-review/CURRENT_REVIEW.md +95 -0
- package/docs/function-publishers-json-apis.md +847 -0
- package/docs/getting-started-with-json.md +518 -0
- package/docs/minification-compression-sourcemaps-status.md +482 -0
- package/docs/minification-compression-sourcemaps-test-results.md +205 -0
- package/docs/publishers-guide.md +313 -0
- package/docs/resources-guide.md +615 -0
- package/docs/serve-helpers.md +406 -0
- package/docs/simple-server-api-design.md +13 -0
- package/docs/system-architecture.md +275 -0
- package/docs/troubleshooting.md +698 -0
- package/examples/json/README.md +115 -0
- package/examples/json/basic-api/README.md +345 -0
- package/examples/json/basic-api/server.js +199 -0
- package/examples/json/simple-api/README.md +125 -0
- package/examples/json/simple-api/diagnostic-report.json +73 -0
- package/examples/json/simple-api/diagnostic-test.js +433 -0
- package/examples/json/simple-api/server-debug.md +58 -0
- package/examples/json/simple-api/server.js +91 -0
- package/examples/json/simple-api/test.js +215 -0
- package/http/responders/static/Static_Route_HTTP_Responder.js +1 -2
- package/package.json +19 -8
- package/publishers/helpers/assigners/static-compressed-response-buffers/Single_Control_Webpage_Server_Static_Compressed_Response_Buffers_Assigner.js +65 -12
- package/publishers/helpers/preparers/static/bundle/Static_Routes_Responses_Webpage_Bundle_Preparer.js +6 -1
- package/publishers/http-function-publisher.js +59 -38
- package/publishers/http-webpage-publisher.js +48 -1
- package/resources/processors/bundlers/js/esbuild/Advanced_JS_Bundler_Using_ESBuild.js +38 -146
- package/resources/processors/bundlers/js/esbuild/Core_JS_Non_Minifying_Bundler_Using_ESBuild.js +54 -5
- package/resources/processors/bundlers/js/esbuild/Core_JS_Single_File_Minifying_Bundler_Using_ESBuild.js +36 -4
- package/serve-factory.js +36 -9
- package/server.js +10 -4
- package/test-report.json +0 -0
- package/tests/README.md +250 -0
- package/tests/assigners.test.js +316 -0
- package/tests/bundlers.test.js +329 -0
- package/tests/configuration-validation.test.js +530 -0
- package/tests/content-analysis.test.js +641 -0
- package/tests/end-to-end.test.js +496 -0
- package/tests/error-handling.test.js +746 -0
- package/tests/performance.test.js +653 -0
- package/tests/publishers.test.js +395 -0
- package/tests/temp_invalid.js +7 -0
- package/tests/temp_invalid_utf8.js +1 -0
- package/tests/temp_malformed.js +10 -0
- package/tests/test-runner.js +261 -0
|
@@ -0,0 +1,1360 @@
|
|
|
1
|
+
# Advanced Usage Examples
|
|
2
|
+
|
|
3
|
+
## When to Read
|
|
4
|
+
|
|
5
|
+
This document provides practical examples of advanced JSGUI3 Server features. Read this when:
|
|
6
|
+
- You want to implement complex server configurations
|
|
7
|
+
- You're building multi-page applications
|
|
8
|
+
- You need API integration examples
|
|
9
|
+
- You want to understand advanced control patterns
|
|
10
|
+
- You're implementing production-ready features
|
|
11
|
+
|
|
12
|
+
**Note:** For basic usage, see [README.md](../README.md). For API reference, see [docs/api-reference.md](docs/api-reference.md).
|
|
13
|
+
|
|
14
|
+
## Multi-Page Application with API
|
|
15
|
+
|
|
16
|
+
### Complete E-commerce Dashboard
|
|
17
|
+
|
|
18
|
+
```javascript
|
|
19
|
+
// server.js - Full-featured e-commerce server
|
|
20
|
+
const Server = require('jsgui3-server');
|
|
21
|
+
const { controls } = require('./client');
|
|
22
|
+
|
|
23
|
+
// Simulated database (in real app, use actual DB)
|
|
24
|
+
const db = {
|
|
25
|
+
products: [
|
|
26
|
+
{ id: 1, name: 'Widget A', price: 29.99, stock: 50 },
|
|
27
|
+
{ id: 2, name: 'Widget B', price: 39.99, stock: 30 }
|
|
28
|
+
],
|
|
29
|
+
orders: [],
|
|
30
|
+
users: [{ id: 1, name: 'Admin', role: 'admin' }]
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
Server.serve({
|
|
34
|
+
pages: {
|
|
35
|
+
'/': {
|
|
36
|
+
content: controls.Dashboard,
|
|
37
|
+
title: 'E-commerce Dashboard'
|
|
38
|
+
},
|
|
39
|
+
'/products': {
|
|
40
|
+
content: controls.ProductManager,
|
|
41
|
+
title: 'Product Management'
|
|
42
|
+
},
|
|
43
|
+
'/orders': {
|
|
44
|
+
content: controls.OrderManager,
|
|
45
|
+
title: 'Order Management'
|
|
46
|
+
},
|
|
47
|
+
'/analytics': {
|
|
48
|
+
content: controls.Analytics,
|
|
49
|
+
title: 'Sales Analytics'
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
api: {
|
|
54
|
+
// Product management
|
|
55
|
+
'products': () => db.products,
|
|
56
|
+
|
|
57
|
+
'product': ({ id }) => {
|
|
58
|
+
const product = db.products.find(p => p.id === parseInt(id));
|
|
59
|
+
if (!product) throw new Error('Product not found');
|
|
60
|
+
return product;
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
'add-product': ({ name, price, stock }) => {
|
|
64
|
+
const newProduct = {
|
|
65
|
+
id: db.products.length + 1,
|
|
66
|
+
name,
|
|
67
|
+
price: parseFloat(price),
|
|
68
|
+
stock: parseInt(stock)
|
|
69
|
+
};
|
|
70
|
+
db.products.push(newProduct);
|
|
71
|
+
return { success: true, product: newProduct };
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
'update-product': ({ id, ...updates }) => {
|
|
75
|
+
const product = db.products.find(p => p.id === parseInt(id));
|
|
76
|
+
if (!product) throw new Error('Product not found');
|
|
77
|
+
|
|
78
|
+
Object.assign(product, updates);
|
|
79
|
+
return { success: true, product };
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
// Order management
|
|
83
|
+
'orders': () => db.orders,
|
|
84
|
+
|
|
85
|
+
'create-order': ({ productId, quantity, customerEmail }) => {
|
|
86
|
+
const product = db.products.find(p => p.id === parseInt(productId));
|
|
87
|
+
if (!product) throw new Error('Product not found');
|
|
88
|
+
if (product.stock < quantity) throw new Error('Insufficient stock');
|
|
89
|
+
|
|
90
|
+
const order = {
|
|
91
|
+
id: db.orders.length + 1,
|
|
92
|
+
productId: parseInt(productId),
|
|
93
|
+
quantity: parseInt(quantity),
|
|
94
|
+
customerEmail,
|
|
95
|
+
total: product.price * quantity,
|
|
96
|
+
status: 'pending',
|
|
97
|
+
createdAt: new Date()
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
db.orders.push(order);
|
|
101
|
+
product.stock -= quantity;
|
|
102
|
+
|
|
103
|
+
return { success: true, order };
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
// Analytics
|
|
107
|
+
'analytics/summary': () => {
|
|
108
|
+
const totalRevenue = db.orders
|
|
109
|
+
.filter(o => o.status === 'completed')
|
|
110
|
+
.reduce((sum, o) => sum + o.total, 0);
|
|
111
|
+
|
|
112
|
+
const totalOrders = db.orders.length;
|
|
113
|
+
const pendingOrders = db.orders.filter(o => o.status === 'pending').length;
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
totalRevenue,
|
|
117
|
+
totalOrders,
|
|
118
|
+
pendingOrders,
|
|
119
|
+
topProducts: db.products
|
|
120
|
+
.map(p => ({
|
|
121
|
+
...p,
|
|
122
|
+
sold: db.orders
|
|
123
|
+
.filter(o => o.productId === p.id && o.status === 'completed')
|
|
124
|
+
.reduce((sum, o) => sum + o.quantity, 0)
|
|
125
|
+
}))
|
|
126
|
+
.sort((a, b) => b.sold - a.sold)
|
|
127
|
+
.slice(0, 5)
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
port: 3000,
|
|
133
|
+
debug: process.env.NODE_ENV !== 'production'
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```javascript
|
|
138
|
+
// client.js - Client-side controls
|
|
139
|
+
const jsgui = require('jsgui3-client');
|
|
140
|
+
const { controls, Control, Data_Object, field } = jsgui;
|
|
141
|
+
const Active_HTML_Document = require('jsgui3-server/controls/Active_HTML_Document');
|
|
142
|
+
|
|
143
|
+
// Dashboard Control
|
|
144
|
+
class Dashboard extends Active_HTML_Document {
|
|
145
|
+
constructor(spec = {}) {
|
|
146
|
+
spec.__type_name = 'dashboard';
|
|
147
|
+
super(spec);
|
|
148
|
+
const { context } = this;
|
|
149
|
+
|
|
150
|
+
if (typeof this.body.add_class === 'function') {
|
|
151
|
+
this.body.add_class('dashboard');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
this.compose();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
compose() {
|
|
158
|
+
// Navigation
|
|
159
|
+
const nav = new controls.Panel({
|
|
160
|
+
context: this.context,
|
|
161
|
+
class: 'nav'
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const navLinks = [
|
|
165
|
+
{ text: 'Dashboard', path: '/' },
|
|
166
|
+
{ text: 'Products', path: '/products' },
|
|
167
|
+
{ text: 'Orders', path: '/orders' },
|
|
168
|
+
{ text: 'Analytics', path: '/analytics' }
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
navLinks.forEach(link => {
|
|
172
|
+
const button = new controls.Button({
|
|
173
|
+
context: this.context,
|
|
174
|
+
text: link.text
|
|
175
|
+
});
|
|
176
|
+
button.on('click', () => {
|
|
177
|
+
window.location.href = link.path;
|
|
178
|
+
});
|
|
179
|
+
nav.add(button);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Summary cards
|
|
183
|
+
const summaryContainer = new controls.Panel({
|
|
184
|
+
context: this.context,
|
|
185
|
+
class: 'summary-cards'
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Load analytics data
|
|
189
|
+
fetch('/api/analytics/summary')
|
|
190
|
+
.then(res => res.json())
|
|
191
|
+
.then(data => {
|
|
192
|
+
const cards = [
|
|
193
|
+
{ title: 'Total Revenue', value: `$${data.totalRevenue.toFixed(2)}` },
|
|
194
|
+
{ title: 'Total Orders', value: data.totalOrders },
|
|
195
|
+
{ title: 'Pending Orders', value: data.pendingOrders }
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
cards.forEach(card => {
|
|
199
|
+
const cardEl = new controls.Panel({
|
|
200
|
+
context: this.context,
|
|
201
|
+
class: 'summary-card'
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const titleEl = new controls.Text({
|
|
205
|
+
context: this.context,
|
|
206
|
+
text: card.title,
|
|
207
|
+
class: 'card-title'
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const valueEl = new controls.Text({
|
|
211
|
+
context: this.context,
|
|
212
|
+
text: card.value,
|
|
213
|
+
class: 'card-value'
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
cardEl.add(titleEl);
|
|
217
|
+
cardEl.add(valueEl);
|
|
218
|
+
summaryContainer.add(cardEl);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
this.body.add(nav);
|
|
223
|
+
this.body.add(summaryContainer);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
activate() {
|
|
227
|
+
if (!this.__active) {
|
|
228
|
+
super.activate();
|
|
229
|
+
// Additional activation logic
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
Dashboard.css = `
|
|
235
|
+
.dashboard {
|
|
236
|
+
font-family: Arial, sans-serif;
|
|
237
|
+
max-width: 1200px;
|
|
238
|
+
margin: 0 auto;
|
|
239
|
+
padding: 20px;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.nav {
|
|
243
|
+
display: flex;
|
|
244
|
+
gap: 10px;
|
|
245
|
+
margin-bottom: 30px;
|
|
246
|
+
padding: 15px;
|
|
247
|
+
background: #f8f9fa;
|
|
248
|
+
border-radius: 5px;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.summary-cards {
|
|
252
|
+
display: grid;
|
|
253
|
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
254
|
+
gap: 20px;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.summary-card {
|
|
258
|
+
padding: 20px;
|
|
259
|
+
background: white;
|
|
260
|
+
border: 1px solid #ddd;
|
|
261
|
+
border-radius: 8px;
|
|
262
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.card-title {
|
|
266
|
+
font-size: 14px;
|
|
267
|
+
color: #666;
|
|
268
|
+
margin-bottom: 10px;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.card-value {
|
|
272
|
+
font-size: 24px;
|
|
273
|
+
font-weight: bold;
|
|
274
|
+
color: #333;
|
|
275
|
+
}
|
|
276
|
+
`;
|
|
277
|
+
|
|
278
|
+
controls.Dashboard = Dashboard;
|
|
279
|
+
module.exports = jsgui;
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Real-time Data Synchronization
|
|
283
|
+
|
|
284
|
+
### Collaborative Document Editor
|
|
285
|
+
|
|
286
|
+
```javascript
|
|
287
|
+
// server.js - Real-time collaborative editing
|
|
288
|
+
const Server = require('jsgui3-server');
|
|
289
|
+
const WebSocket = require('ws');
|
|
290
|
+
|
|
291
|
+
// In-memory document store (use Redis in production)
|
|
292
|
+
const documents = new Map();
|
|
293
|
+
const clients = new Map(); // docId -> Set of WebSocket clients
|
|
294
|
+
|
|
295
|
+
Server.serve({
|
|
296
|
+
ctrl: require('./client').controls.DocumentEditor,
|
|
297
|
+
|
|
298
|
+
api: {
|
|
299
|
+
'document': ({ id }) => {
|
|
300
|
+
return documents.get(id) || { id, content: '', version: 0 };
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
'save-document': ({ id, content, version }) => {
|
|
304
|
+
const doc = documents.get(id) || { id, version: 0 };
|
|
305
|
+
if (version < doc.version) {
|
|
306
|
+
throw new Error('Version conflict');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
doc.content = content;
|
|
310
|
+
doc.version = version + 1;
|
|
311
|
+
doc.lastModified = new Date();
|
|
312
|
+
documents.set(id, doc);
|
|
313
|
+
|
|
314
|
+
// Broadcast to all clients
|
|
315
|
+
const docClients = clients.get(id);
|
|
316
|
+
if (docClients) {
|
|
317
|
+
const update = { type: 'update', document: doc };
|
|
318
|
+
docClients.forEach(client => {
|
|
319
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
320
|
+
client.send(JSON.stringify(update));
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return { success: true, document: doc };
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
|
|
329
|
+
port: 3000,
|
|
330
|
+
|
|
331
|
+
// Custom WebSocket setup
|
|
332
|
+
setup: (server) => {
|
|
333
|
+
const wss = new WebSocket.Server({ server });
|
|
334
|
+
|
|
335
|
+
wss.on('connection', (ws, req) => {
|
|
336
|
+
const docId = new URL(req.url, 'http://localhost').searchParams.get('doc');
|
|
337
|
+
|
|
338
|
+
if (docId) {
|
|
339
|
+
if (!clients.has(docId)) {
|
|
340
|
+
clients.set(docId, new Set());
|
|
341
|
+
}
|
|
342
|
+
clients.get(docId).add(ws);
|
|
343
|
+
|
|
344
|
+
ws.on('close', () => {
|
|
345
|
+
clients.get(docId)?.delete(ws);
|
|
346
|
+
if (clients.get(docId)?.size === 0) {
|
|
347
|
+
clients.delete(docId);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
```javascript
|
|
357
|
+
// client.js - Real-time document editor
|
|
358
|
+
const jsgui = require('jsgui3-client');
|
|
359
|
+
const { controls, Control, Data_Object, field } = jsgui;
|
|
360
|
+
const Active_HTML_Document = require('jsgui3-server/controls/Active_HTML_Document');
|
|
361
|
+
|
|
362
|
+
class DocumentEditor extends Active_HTML_Document {
|
|
363
|
+
constructor(spec = {}) {
|
|
364
|
+
spec.__type_name = 'document_editor';
|
|
365
|
+
super(spec);
|
|
366
|
+
const { context } = this;
|
|
367
|
+
|
|
368
|
+
// Get document ID from URL
|
|
369
|
+
this.docId = new URL(window.location.href).searchParams.get('doc') || 'default';
|
|
370
|
+
|
|
371
|
+
// Create reactive document model
|
|
372
|
+
this.document = new Data_Object({ context });
|
|
373
|
+
field(this.document, 'content');
|
|
374
|
+
field(this.document, 'version');
|
|
375
|
+
context.register_control(this.document);
|
|
376
|
+
|
|
377
|
+
this.compose();
|
|
378
|
+
this.connectWebSocket();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
compose() {
|
|
382
|
+
const container = new controls.Panel({
|
|
383
|
+
context: this.context,
|
|
384
|
+
class: 'editor-container'
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Document title
|
|
388
|
+
const title = new controls.Text_Input({
|
|
389
|
+
context: this.context,
|
|
390
|
+
placeholder: 'Document Title',
|
|
391
|
+
class: 'doc-title'
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// Content editor
|
|
395
|
+
this.contentEditor = new controls.Text_Area({
|
|
396
|
+
context: this.context,
|
|
397
|
+
placeholder: 'Start writing...',
|
|
398
|
+
class: 'content-editor'
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// Bind to reactive model
|
|
402
|
+
this.contentEditor.data = { model: this.document, field_name: 'content' };
|
|
403
|
+
|
|
404
|
+
// Status indicator
|
|
405
|
+
this.statusIndicator = new controls.Text({
|
|
406
|
+
context: this.context,
|
|
407
|
+
text: 'Connecting...',
|
|
408
|
+
class: 'status'
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Save button
|
|
412
|
+
const saveButton = new controls.Button({
|
|
413
|
+
context: this.context,
|
|
414
|
+
text: 'Save',
|
|
415
|
+
class: 'save-btn'
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
saveButton.on('click', () => this.saveDocument());
|
|
419
|
+
|
|
420
|
+
container.add(title);
|
|
421
|
+
container.add(this.contentEditor);
|
|
422
|
+
container.add(this.statusIndicator);
|
|
423
|
+
container.add(saveButton);
|
|
424
|
+
|
|
425
|
+
this.body.add(container);
|
|
426
|
+
|
|
427
|
+
// Load existing document
|
|
428
|
+
this.loadDocument();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async loadDocument() {
|
|
432
|
+
try {
|
|
433
|
+
const response = await fetch(`/api/document?id=${this.docId}`);
|
|
434
|
+
const doc = await response.json();
|
|
435
|
+
|
|
436
|
+
this.document.content = doc.content;
|
|
437
|
+
this.document.version = doc.version;
|
|
438
|
+
this.updateStatus('Loaded');
|
|
439
|
+
} catch (error) {
|
|
440
|
+
this.updateStatus('Error loading document');
|
|
441
|
+
console.error('Load error:', error);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async saveDocument() {
|
|
446
|
+
try {
|
|
447
|
+
this.updateStatus('Saving...');
|
|
448
|
+
|
|
449
|
+
const response = await fetch('/api/save-document', {
|
|
450
|
+
method: 'POST',
|
|
451
|
+
headers: { 'Content-Type': 'application/json' },
|
|
452
|
+
body: JSON.stringify({
|
|
453
|
+
id: this.docId,
|
|
454
|
+
content: this.document.content,
|
|
455
|
+
version: this.document.version
|
|
456
|
+
})
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
const result = await response.json();
|
|
460
|
+
if (result.success) {
|
|
461
|
+
this.document.version = result.document.version;
|
|
462
|
+
this.updateStatus('Saved');
|
|
463
|
+
} else {
|
|
464
|
+
this.updateStatus('Save failed');
|
|
465
|
+
}
|
|
466
|
+
} catch (error) {
|
|
467
|
+
this.updateStatus('Save error');
|
|
468
|
+
console.error('Save error:', error);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
connectWebSocket() {
|
|
473
|
+
const ws = new WebSocket(`ws://localhost:3000?doc=${this.docId}`);
|
|
474
|
+
|
|
475
|
+
ws.onopen = () => {
|
|
476
|
+
this.updateStatus('Connected');
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
ws.onmessage = (event) => {
|
|
480
|
+
const data = JSON.parse(event.data);
|
|
481
|
+
if (data.type === 'update' && data.document.version > this.document.version) {
|
|
482
|
+
this.document.content = data.document.content;
|
|
483
|
+
this.document.version = data.document.version;
|
|
484
|
+
this.updateStatus('Updated from server');
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
ws.onclose = () => {
|
|
489
|
+
this.updateStatus('Disconnected');
|
|
490
|
+
// Attempt reconnection after delay
|
|
491
|
+
setTimeout(() => this.connectWebSocket(), 5000);
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
ws.onerror = (error) => {
|
|
495
|
+
console.error('WebSocket error:', error);
|
|
496
|
+
this.updateStatus('Connection error');
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
updateStatus(message) {
|
|
501
|
+
if (this.statusIndicator) {
|
|
502
|
+
this.statusIndicator.text = message;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
activate() {
|
|
507
|
+
if (!this.__active) {
|
|
508
|
+
super.activate();
|
|
509
|
+
const { context } = this;
|
|
510
|
+
|
|
511
|
+
// Auto-save on content changes (debounced)
|
|
512
|
+
let saveTimeout;
|
|
513
|
+
this.document.on('change', (e) => {
|
|
514
|
+
if (e.field_name === 'content') {
|
|
515
|
+
clearTimeout(saveTimeout);
|
|
516
|
+
saveTimeout = setTimeout(() => {
|
|
517
|
+
if (this.document.content) {
|
|
518
|
+
this.saveDocument();
|
|
519
|
+
}
|
|
520
|
+
}, 2000); // Save after 2 seconds of no typing
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
DocumentEditor.css = `
|
|
528
|
+
.editor-container {
|
|
529
|
+
max-width: 800px;
|
|
530
|
+
margin: 0 auto;
|
|
531
|
+
padding: 20px;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.doc-title {
|
|
535
|
+
width: 100%;
|
|
536
|
+
padding: 10px;
|
|
537
|
+
font-size: 24px;
|
|
538
|
+
border: 1px solid #ddd;
|
|
539
|
+
border-radius: 4px;
|
|
540
|
+
margin-bottom: 20px;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
.content-editor {
|
|
544
|
+
width: 100%;
|
|
545
|
+
height: 400px;
|
|
546
|
+
padding: 15px;
|
|
547
|
+
border: 1px solid #ddd;
|
|
548
|
+
border-radius: 4px;
|
|
549
|
+
font-family: monospace;
|
|
550
|
+
font-size: 14px;
|
|
551
|
+
line-height: 1.4;
|
|
552
|
+
resize: vertical;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
.status {
|
|
556
|
+
margin: 10px 0;
|
|
557
|
+
padding: 5px 10px;
|
|
558
|
+
background: #e9ecef;
|
|
559
|
+
border-radius: 4px;
|
|
560
|
+
font-size: 12px;
|
|
561
|
+
color: #666;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
.save-btn {
|
|
565
|
+
padding: 10px 20px;
|
|
566
|
+
background: #007bff;
|
|
567
|
+
color: white;
|
|
568
|
+
border: none;
|
|
569
|
+
border-radius: 4px;
|
|
570
|
+
cursor: pointer;
|
|
571
|
+
margin-top: 10px;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
.save-btn:hover {
|
|
575
|
+
background: #0056b3;
|
|
576
|
+
}
|
|
577
|
+
`;
|
|
578
|
+
|
|
579
|
+
controls.DocumentEditor = DocumentEditor;
|
|
580
|
+
module.exports = jsgui;
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
## Advanced Control Patterns
|
|
584
|
+
|
|
585
|
+
### Custom Data-Bound Controls
|
|
586
|
+
|
|
587
|
+
```javascript
|
|
588
|
+
// client.js - Advanced data-bound controls
|
|
589
|
+
const jsgui = require('jsgui3-client');
|
|
590
|
+
const { controls, Control, Data_Object, field, mixins } = jsgui;
|
|
591
|
+
const { dragable } = mixins;
|
|
592
|
+
const Active_HTML_Document = require('jsgui3-server/controls/Active_HTML_Document');
|
|
593
|
+
|
|
594
|
+
// Data Table with Sorting and Filtering
|
|
595
|
+
class DataTable extends Control {
|
|
596
|
+
constructor(spec = {}) {
|
|
597
|
+
spec.__type_name = 'data_table';
|
|
598
|
+
super(spec);
|
|
599
|
+
const { context } = this;
|
|
600
|
+
|
|
601
|
+
this.data = spec.data || [];
|
|
602
|
+
this.columns = spec.columns || [];
|
|
603
|
+
this.sortColumn = null;
|
|
604
|
+
this.sortDirection = 'asc';
|
|
605
|
+
this.filterText = '';
|
|
606
|
+
|
|
607
|
+
this.compose();
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
compose() {
|
|
611
|
+
const table = document.createElement('table');
|
|
612
|
+
table.className = 'data-table';
|
|
613
|
+
|
|
614
|
+
// Header
|
|
615
|
+
const thead = document.createElement('thead');
|
|
616
|
+
const headerRow = document.createElement('tr');
|
|
617
|
+
|
|
618
|
+
this.columns.forEach(column => {
|
|
619
|
+
const th = document.createElement('th');
|
|
620
|
+
th.textContent = column.title;
|
|
621
|
+
th.className = 'sortable';
|
|
622
|
+
th.onclick = () => this.sortBy(column.key);
|
|
623
|
+
headerRow.appendChild(th);
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
thead.appendChild(headerRow);
|
|
627
|
+
table.appendChild(thead);
|
|
628
|
+
|
|
629
|
+
// Filter input
|
|
630
|
+
const filterContainer = document.createElement('div');
|
|
631
|
+
filterContainer.className = 'filter-container';
|
|
632
|
+
|
|
633
|
+
const filterInput = document.createElement('input');
|
|
634
|
+
filterInput.type = 'text';
|
|
635
|
+
filterInput.placeholder = 'Filter...';
|
|
636
|
+
filterInput.className = 'filter-input';
|
|
637
|
+
filterInput.oninput = (e) => {
|
|
638
|
+
this.filterText = e.target.value.toLowerCase();
|
|
639
|
+
this.renderBody(table);
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
filterContainer.appendChild(filterInput);
|
|
643
|
+
this.dom.el.appendChild(filterContainer);
|
|
644
|
+
this.dom.el.appendChild(table);
|
|
645
|
+
|
|
646
|
+
this.table = table;
|
|
647
|
+
this.renderBody(table);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
sortBy(columnKey) {
|
|
651
|
+
if (this.sortColumn === columnKey) {
|
|
652
|
+
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
|
|
653
|
+
} else {
|
|
654
|
+
this.sortColumn = columnKey;
|
|
655
|
+
this.sortDirection = 'asc';
|
|
656
|
+
}
|
|
657
|
+
this.renderBody(this.table);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
getFilteredAndSortedData() {
|
|
661
|
+
let filtered = this.data;
|
|
662
|
+
|
|
663
|
+
if (this.filterText) {
|
|
664
|
+
filtered = this.data.filter(item =>
|
|
665
|
+
Object.values(item).some(value =>
|
|
666
|
+
String(value).toLowerCase().includes(this.filterText)
|
|
667
|
+
)
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (this.sortColumn) {
|
|
672
|
+
filtered = [...filtered].sort((a, b) => {
|
|
673
|
+
const aVal = a[this.sortColumn];
|
|
674
|
+
const bVal = b[this.sortColumn];
|
|
675
|
+
|
|
676
|
+
let result = 0;
|
|
677
|
+
if (aVal < bVal) result = -1;
|
|
678
|
+
if (aVal > bVal) result = 1;
|
|
679
|
+
|
|
680
|
+
return this.sortDirection === 'asc' ? result : -result;
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
return filtered;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
renderBody(table) {
|
|
688
|
+
// Remove existing body
|
|
689
|
+
const existingBody = table.querySelector('tbody');
|
|
690
|
+
if (existingBody) {
|
|
691
|
+
existingBody.remove();
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const tbody = document.createElement('tbody');
|
|
695
|
+
const data = this.getFilteredAndSortedData();
|
|
696
|
+
|
|
697
|
+
data.forEach(item => {
|
|
698
|
+
const row = document.createElement('tr');
|
|
699
|
+
|
|
700
|
+
this.columns.forEach(column => {
|
|
701
|
+
const cell = document.createElement('td');
|
|
702
|
+
const value = item[column.key];
|
|
703
|
+
|
|
704
|
+
if (column.renderer) {
|
|
705
|
+
cell.innerHTML = column.renderer(value, item);
|
|
706
|
+
} else {
|
|
707
|
+
cell.textContent = value;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
row.appendChild(cell);
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
tbody.appendChild(row);
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
table.appendChild(tbody);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
setData(data) {
|
|
720
|
+
this.data = data;
|
|
721
|
+
this.renderBody(this.table);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
DataTable.css = `
|
|
726
|
+
.data-table {
|
|
727
|
+
width: 100%;
|
|
728
|
+
border-collapse: collapse;
|
|
729
|
+
margin-top: 10px;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
.data-table th,
|
|
733
|
+
.data-table td {
|
|
734
|
+
padding: 8px 12px;
|
|
735
|
+
text-align: left;
|
|
736
|
+
border-bottom: 1px solid #ddd;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
.data-table th {
|
|
740
|
+
background-color: #f8f9fa;
|
|
741
|
+
font-weight: bold;
|
|
742
|
+
cursor: pointer;
|
|
743
|
+
user-select: none;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
.data-table th:hover {
|
|
747
|
+
background-color: #e9ecef;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
.data-table th.sortable::after {
|
|
751
|
+
content: ' ⇅';
|
|
752
|
+
opacity: 0.5;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
.data-table th.sortable.sorted-asc::after {
|
|
756
|
+
content: ' ↑';
|
|
757
|
+
opacity: 1;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
.data-table th.sortable.sorted-desc::after {
|
|
761
|
+
content: ' ↓';
|
|
762
|
+
opacity: 1;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
.filter-container {
|
|
766
|
+
margin-bottom: 10px;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
.filter-input {
|
|
770
|
+
width: 100%;
|
|
771
|
+
padding: 8px;
|
|
772
|
+
border: 1px solid #ddd;
|
|
773
|
+
border-radius: 4px;
|
|
774
|
+
font-size: 14px;
|
|
775
|
+
}
|
|
776
|
+
`;
|
|
777
|
+
|
|
778
|
+
// Usage example
|
|
779
|
+
class ProductManager extends Active_HTML_Document {
|
|
780
|
+
constructor(spec = {}) {
|
|
781
|
+
spec.__type_name = 'product_manager';
|
|
782
|
+
super(spec);
|
|
783
|
+
const { context } = this;
|
|
784
|
+
|
|
785
|
+
this.compose();
|
|
786
|
+
this.loadProducts();
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
compose() {
|
|
790
|
+
const container = new controls.Panel({
|
|
791
|
+
context: this.context,
|
|
792
|
+
class: 'product-manager'
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
// Add product button
|
|
796
|
+
const addButton = new controls.Button({
|
|
797
|
+
context: this.context,
|
|
798
|
+
text: 'Add Product',
|
|
799
|
+
class: 'add-btn'
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
addButton.on('click', () => this.showAddProductDialog());
|
|
803
|
+
|
|
804
|
+
// Products table
|
|
805
|
+
this.table = new DataTable({
|
|
806
|
+
context: this.context,
|
|
807
|
+
columns: [
|
|
808
|
+
{ key: 'id', title: 'ID' },
|
|
809
|
+
{ key: 'name', title: 'Name' },
|
|
810
|
+
{ key: 'price', title: 'Price', renderer: (value) => `$${value.toFixed(2)}` },
|
|
811
|
+
{ key: 'stock', title: 'Stock' },
|
|
812
|
+
{
|
|
813
|
+
key: 'actions',
|
|
814
|
+
title: 'Actions',
|
|
815
|
+
renderer: (value, item) => `
|
|
816
|
+
<button onclick="editProduct(${item.id})">Edit</button>
|
|
817
|
+
<button onclick="deleteProduct(${item.id})" class="delete">Delete</button>
|
|
818
|
+
`
|
|
819
|
+
}
|
|
820
|
+
]
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
container.add(addButton);
|
|
824
|
+
container.add(this.table);
|
|
825
|
+
this.body.add(container);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
async loadProducts() {
|
|
829
|
+
try {
|
|
830
|
+
const response = await fetch('/api/products');
|
|
831
|
+
const products = await response.json();
|
|
832
|
+
this.table.setData(products);
|
|
833
|
+
} catch (error) {
|
|
834
|
+
console.error('Failed to load products:', error);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
showAddProductDialog() {
|
|
839
|
+
// Implementation for add product dialog
|
|
840
|
+
alert('Add product functionality would be implemented here');
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
ProductManager.css = `
|
|
845
|
+
.product-manager {
|
|
846
|
+
padding: 20px;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
.add-btn {
|
|
850
|
+
padding: 10px 20px;
|
|
851
|
+
background: #28a745;
|
|
852
|
+
color: white;
|
|
853
|
+
border: none;
|
|
854
|
+
border-radius: 4px;
|
|
855
|
+
cursor: pointer;
|
|
856
|
+
margin-bottom: 20px;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
.add-btn:hover {
|
|
860
|
+
background: #218838;
|
|
861
|
+
}
|
|
862
|
+
`;
|
|
863
|
+
|
|
864
|
+
// Make functions global for inline event handlers
|
|
865
|
+
window.editProduct = (id) => {
|
|
866
|
+
alert(`Edit product ${id}`);
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
window.deleteProduct = (id) => {
|
|
870
|
+
if (confirm('Are you sure you want to delete this product?')) {
|
|
871
|
+
alert(`Delete product ${id}`);
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
controls.DataTable = DataTable;
|
|
876
|
+
controls.ProductManager = ProductManager;
|
|
877
|
+
module.exports = jsgui;
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
## Production Configuration Examples
|
|
881
|
+
|
|
882
|
+
### Load Balancing Setup
|
|
883
|
+
|
|
884
|
+
```javascript
|
|
885
|
+
// server.js - Production server with clustering
|
|
886
|
+
const cluster = require('cluster');
|
|
887
|
+
const os = require('os');
|
|
888
|
+
const Server = require('jsgui3-server');
|
|
889
|
+
|
|
890
|
+
if (cluster.isMaster) {
|
|
891
|
+
// Master process
|
|
892
|
+
const numCPUs = os.cpus().length;
|
|
893
|
+
|
|
894
|
+
console.log(`Master ${process.pid} is running`);
|
|
895
|
+
console.log(`Starting ${numCPUs} workers...`);
|
|
896
|
+
|
|
897
|
+
// Fork workers
|
|
898
|
+
for (let i = 0; i < numCPUs; i++) {
|
|
899
|
+
cluster.fork();
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
cluster.on('exit', (worker, code, signal) => {
|
|
903
|
+
console.log(`Worker ${worker.process.pid} died`);
|
|
904
|
+
// Restart worker
|
|
905
|
+
cluster.fork();
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
} else {
|
|
909
|
+
// Worker process
|
|
910
|
+
Server.serve({
|
|
911
|
+
ctrl: require('./client').controls.App,
|
|
912
|
+
port: process.env.PORT || 3000,
|
|
913
|
+
debug: false,
|
|
914
|
+
|
|
915
|
+
// Production optimizations
|
|
916
|
+
cache: {
|
|
917
|
+
static: { maxAge: 86400 }, // 24 hours
|
|
918
|
+
api: { maxAge: 300 } // 5 minutes
|
|
919
|
+
},
|
|
920
|
+
|
|
921
|
+
compression: true,
|
|
922
|
+
etag: true,
|
|
923
|
+
|
|
924
|
+
// Health check endpoint
|
|
925
|
+
api: {
|
|
926
|
+
'health': () => ({
|
|
927
|
+
status: 'ok',
|
|
928
|
+
uptime: process.uptime(),
|
|
929
|
+
memory: process.memoryUsage(),
|
|
930
|
+
pid: process.pid
|
|
931
|
+
})
|
|
932
|
+
}
|
|
933
|
+
}).catch(err => {
|
|
934
|
+
console.error(`Worker ${process.pid} failed:`, err);
|
|
935
|
+
process.exit(1);
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
### Docker Deployment
|
|
941
|
+
|
|
942
|
+
```dockerfile
|
|
943
|
+
# Dockerfile
|
|
944
|
+
FROM node:18-alpine
|
|
945
|
+
|
|
946
|
+
WORKDIR /app
|
|
947
|
+
|
|
948
|
+
# Install dependencies
|
|
949
|
+
COPY package*.json ./
|
|
950
|
+
RUN npm ci --only=production
|
|
951
|
+
|
|
952
|
+
# Copy application
|
|
953
|
+
COPY . .
|
|
954
|
+
|
|
955
|
+
# Create non-root user
|
|
956
|
+
RUN addgroup -g 1001 -S nodejs && \
|
|
957
|
+
adduser -S appuser -u 1001
|
|
958
|
+
|
|
959
|
+
USER appuser
|
|
960
|
+
|
|
961
|
+
EXPOSE 3000
|
|
962
|
+
|
|
963
|
+
# Health check
|
|
964
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
965
|
+
CMD node -e "
|
|
966
|
+
const http = require('http');
|
|
967
|
+
const options = { hostname: 'localhost', port: 3000, path: '/api/health', method: 'GET' };
|
|
968
|
+
const req = http.request(options, (res) => {
|
|
969
|
+
if (res.statusCode === 200) process.exit(0);
|
|
970
|
+
else process.exit(1);
|
|
971
|
+
});
|
|
972
|
+
req.on('error', () => process.exit(1));
|
|
973
|
+
req.end();
|
|
974
|
+
"
|
|
975
|
+
|
|
976
|
+
CMD ["node", "server.js"]
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
```yaml
|
|
980
|
+
# docker-compose.yml
|
|
981
|
+
version: '3.8'
|
|
982
|
+
services:
|
|
983
|
+
jsgui3-app:
|
|
984
|
+
build: .
|
|
985
|
+
ports:
|
|
986
|
+
- "3000:3000"
|
|
987
|
+
environment:
|
|
988
|
+
- NODE_ENV=production
|
|
989
|
+
- PORT=3000
|
|
990
|
+
restart: unless-stopped
|
|
991
|
+
healthcheck:
|
|
992
|
+
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
|
|
993
|
+
interval: 30s
|
|
994
|
+
timeout: 10s
|
|
995
|
+
retries: 3
|
|
996
|
+
start_period: 40s
|
|
997
|
+
networks:
|
|
998
|
+
- app-network
|
|
999
|
+
|
|
1000
|
+
nginx:
|
|
1001
|
+
image: nginx:alpine
|
|
1002
|
+
ports:
|
|
1003
|
+
- "80:80"
|
|
1004
|
+
volumes:
|
|
1005
|
+
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
|
1006
|
+
depends_on:
|
|
1007
|
+
- jsgui3-app
|
|
1008
|
+
networks:
|
|
1009
|
+
- app-network
|
|
1010
|
+
|
|
1011
|
+
networks:
|
|
1012
|
+
app-network:
|
|
1013
|
+
driver: bridge
|
|
1014
|
+
```
|
|
1015
|
+
|
|
1016
|
+
```nginx
|
|
1017
|
+
# nginx.conf
|
|
1018
|
+
events {
|
|
1019
|
+
worker_connections 1024;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
http {
|
|
1023
|
+
upstream app_backend {
|
|
1024
|
+
server jsgui3-app:3000;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
server {
|
|
1028
|
+
listen 80;
|
|
1029
|
+
server_name localhost;
|
|
1030
|
+
|
|
1031
|
+
# Gzip compression
|
|
1032
|
+
gzip on;
|
|
1033
|
+
gzip_types text/plain text/css application/json application/javascript;
|
|
1034
|
+
|
|
1035
|
+
# Static file caching
|
|
1036
|
+
location /css/ {
|
|
1037
|
+
proxy_pass http://app_backend;
|
|
1038
|
+
expires 1y;
|
|
1039
|
+
add_header Cache-Control "public, immutable";
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
location /js/ {
|
|
1043
|
+
proxy_pass http://app_backend;
|
|
1044
|
+
expires 1y;
|
|
1045
|
+
add_header Cache-Control "public, immutable";
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
# API with shorter cache
|
|
1049
|
+
location /api/ {
|
|
1050
|
+
proxy_pass http://app_backend;
|
|
1051
|
+
expires 5m;
|
|
1052
|
+
add_header Cache-Control "public, must-revalidate, proxy-revalidate";
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
# Main app
|
|
1056
|
+
location / {
|
|
1057
|
+
proxy_pass http://app_backend;
|
|
1058
|
+
proxy_set_header Host $host;
|
|
1059
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
1060
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
1061
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
## Authentication and Security
|
|
1068
|
+
|
|
1069
|
+
### JWT-Based Authentication
|
|
1070
|
+
|
|
1071
|
+
```javascript
|
|
1072
|
+
// server.js - Authentication system
|
|
1073
|
+
const jwt = require('jsonwebtoken');
|
|
1074
|
+
const bcrypt = require('bcrypt');
|
|
1075
|
+
const Server = require('jsgui3-server');
|
|
1076
|
+
|
|
1077
|
+
// In-memory user store (use database in production)
|
|
1078
|
+
const users = [
|
|
1079
|
+
{ id: 1, username: 'admin', password: '$2b$10$...' } // hashed password
|
|
1080
|
+
];
|
|
1081
|
+
|
|
1082
|
+
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
|
|
1083
|
+
|
|
1084
|
+
Server.serve({
|
|
1085
|
+
pages: {
|
|
1086
|
+
'/': {
|
|
1087
|
+
content: require('./client').controls.Dashboard,
|
|
1088
|
+
title: 'Dashboard'
|
|
1089
|
+
},
|
|
1090
|
+
'/login': {
|
|
1091
|
+
content: require('./client').controls.Login,
|
|
1092
|
+
title: 'Login'
|
|
1093
|
+
}
|
|
1094
|
+
},
|
|
1095
|
+
|
|
1096
|
+
api: {
|
|
1097
|
+
'login': async ({ username, password }) => {
|
|
1098
|
+
const user = users.find(u => u.username === username);
|
|
1099
|
+
if (!user) {
|
|
1100
|
+
throw new Error('Invalid credentials');
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
const validPassword = await bcrypt.compare(password, user.password);
|
|
1104
|
+
if (!validPassword) {
|
|
1105
|
+
throw new Error('Invalid credentials');
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
const token = jwt.sign(
|
|
1109
|
+
{ userId: user.id, username: user.username },
|
|
1110
|
+
JWT_SECRET,
|
|
1111
|
+
{ expiresIn: '24h' }
|
|
1112
|
+
);
|
|
1113
|
+
|
|
1114
|
+
return { token, user: { id: user.id, username: user.username } };
|
|
1115
|
+
},
|
|
1116
|
+
|
|
1117
|
+
'verify-token': ({ token }) => {
|
|
1118
|
+
try {
|
|
1119
|
+
const decoded = jwt.verify(token, JWT_SECRET);
|
|
1120
|
+
return { valid: true, user: decoded };
|
|
1121
|
+
} catch (error) {
|
|
1122
|
+
return { valid: false, error: error.message };
|
|
1123
|
+
}
|
|
1124
|
+
},
|
|
1125
|
+
|
|
1126
|
+
'protected-data': ({ token }) => {
|
|
1127
|
+
const verification = jwt.verify(token, JWT_SECRET);
|
|
1128
|
+
if (!verification.valid) {
|
|
1129
|
+
throw new Error('Unauthorized');
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// Return protected data
|
|
1133
|
+
return { secret: 'This is protected data' };
|
|
1134
|
+
}
|
|
1135
|
+
},
|
|
1136
|
+
|
|
1137
|
+
// Middleware for authentication
|
|
1138
|
+
middleware: [
|
|
1139
|
+
(req, res, next) => {
|
|
1140
|
+
// Add CORS headers
|
|
1141
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
1142
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
|
|
1143
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
1144
|
+
|
|
1145
|
+
if (req.method === 'OPTIONS') {
|
|
1146
|
+
res.statusCode = 200;
|
|
1147
|
+
res.end();
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
next();
|
|
1152
|
+
}
|
|
1153
|
+
],
|
|
1154
|
+
|
|
1155
|
+
port: 3000
|
|
1156
|
+
});
|
|
1157
|
+
```
|
|
1158
|
+
|
|
1159
|
+
```javascript
|
|
1160
|
+
// client.js - Client-side authentication
|
|
1161
|
+
const jsgui = require('jsgui3-client');
|
|
1162
|
+
const { controls, Control } = jsgui;
|
|
1163
|
+
const Active_HTML_Document = require('jsgui3-server/controls/Active_HTML_Document');
|
|
1164
|
+
|
|
1165
|
+
class Login extends Active_HTML_Document {
|
|
1166
|
+
constructor(spec = {}) {
|
|
1167
|
+
spec.__type_name = 'login';
|
|
1168
|
+
super(spec);
|
|
1169
|
+
const { context } = this;
|
|
1170
|
+
|
|
1171
|
+
this.token = localStorage.getItem('authToken');
|
|
1172
|
+
if (this.token) {
|
|
1173
|
+
// Verify token and redirect if valid
|
|
1174
|
+
this.verifyToken();
|
|
1175
|
+
} else {
|
|
1176
|
+
this.showLoginForm();
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
showLoginForm() {
|
|
1181
|
+
const form = new controls.Panel({
|
|
1182
|
+
context: this.context,
|
|
1183
|
+
class: 'login-form'
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
const title = new controls.Text({
|
|
1187
|
+
context: this.context,
|
|
1188
|
+
text: 'Login',
|
|
1189
|
+
class: 'form-title'
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
this.usernameInput = new controls.Text_Input({
|
|
1193
|
+
context: this.context,
|
|
1194
|
+
placeholder: 'Username',
|
|
1195
|
+
class: 'form-input'
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
this.passwordInput = new controls.Text_Input({
|
|
1199
|
+
context: this.context,
|
|
1200
|
+
placeholder: 'Password',
|
|
1201
|
+
type: 'password',
|
|
1202
|
+
class: 'form-input'
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
this.loginButton = new controls.Button({
|
|
1206
|
+
context: this.context,
|
|
1207
|
+
text: 'Login',
|
|
1208
|
+
class: 'login-btn'
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
this.messageDiv = new controls.Text({
|
|
1212
|
+
context: this.context,
|
|
1213
|
+
text: '',
|
|
1214
|
+
class: 'message'
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
this.loginButton.on('click', () => this.attemptLogin());
|
|
1218
|
+
|
|
1219
|
+
form.add(title);
|
|
1220
|
+
form.add(this.usernameInput);
|
|
1221
|
+
form.add(this.passwordInput);
|
|
1222
|
+
form.add(this.loginButton);
|
|
1223
|
+
form.add(this.messageDiv);
|
|
1224
|
+
|
|
1225
|
+
this.body.add(form);
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
async attemptLogin() {
|
|
1229
|
+
try {
|
|
1230
|
+
this.setMessage('Logging in...');
|
|
1231
|
+
|
|
1232
|
+
const response = await fetch('/api/login', {
|
|
1233
|
+
method: 'POST',
|
|
1234
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1235
|
+
body: JSON.stringify({
|
|
1236
|
+
username: this.usernameInput.dom.el.value,
|
|
1237
|
+
password: this.passwordInput.dom.el.value
|
|
1238
|
+
})
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
const result = await response.json();
|
|
1242
|
+
|
|
1243
|
+
if (result.token) {
|
|
1244
|
+
localStorage.setItem('authToken', result.token);
|
|
1245
|
+
this.setMessage('Login successful! Redirecting...');
|
|
1246
|
+
setTimeout(() => {
|
|
1247
|
+
window.location.href = '/';
|
|
1248
|
+
}, 1000);
|
|
1249
|
+
} else {
|
|
1250
|
+
this.setMessage('Login failed');
|
|
1251
|
+
}
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
this.setMessage('Login error: ' + error.message);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
async verifyToken() {
|
|
1258
|
+
try {
|
|
1259
|
+
const response = await fetch('/api/verify-token', {
|
|
1260
|
+
method: 'POST',
|
|
1261
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1262
|
+
body: JSON.stringify({ token: this.token })
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
const result = await response.json();
|
|
1266
|
+
|
|
1267
|
+
if (result.valid) {
|
|
1268
|
+
// Token is valid, redirect to dashboard
|
|
1269
|
+
window.location.href = '/';
|
|
1270
|
+
} else {
|
|
1271
|
+
// Token invalid, show login form
|
|
1272
|
+
localStorage.removeItem('authToken');
|
|
1273
|
+
this.showLoginForm();
|
|
1274
|
+
}
|
|
1275
|
+
} catch (error) {
|
|
1276
|
+
localStorage.removeItem('authToken');
|
|
1277
|
+
this.showLoginForm();
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
setMessage(text) {
|
|
1282
|
+
if (this.messageDiv) {
|
|
1283
|
+
this.messageDiv.text = text;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
Login.css = `
|
|
1289
|
+
.login-form {
|
|
1290
|
+
max-width: 400px;
|
|
1291
|
+
margin: 50px auto;
|
|
1292
|
+
padding: 30px;
|
|
1293
|
+
border: 1px solid #ddd;
|
|
1294
|
+
border-radius: 8px;
|
|
1295
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
.form-title {
|
|
1299
|
+
font-size: 24px;
|
|
1300
|
+
text-align: center;
|
|
1301
|
+
margin-bottom: 30px;
|
|
1302
|
+
color: #333;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
.form-input {
|
|
1306
|
+
width: 100%;
|
|
1307
|
+
padding: 12px;
|
|
1308
|
+
margin-bottom: 15px;
|
|
1309
|
+
border: 1px solid #ddd;
|
|
1310
|
+
border-radius: 4px;
|
|
1311
|
+
font-size: 16px;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
.login-btn {
|
|
1315
|
+
width: 100%;
|
|
1316
|
+
padding: 12px;
|
|
1317
|
+
background: #007bff;
|
|
1318
|
+
color: white;
|
|
1319
|
+
border: none;
|
|
1320
|
+
border-radius: 4px;
|
|
1321
|
+
font-size: 16px;
|
|
1322
|
+
cursor: pointer;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
.login-btn:hover {
|
|
1326
|
+
background: #0056b3;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
.message {
|
|
1330
|
+
margin-top: 15px;
|
|
1331
|
+
text-align: center;
|
|
1332
|
+
min-height: 20px;
|
|
1333
|
+
}
|
|
1334
|
+
`;
|
|
1335
|
+
|
|
1336
|
+
// Global auth helper
|
|
1337
|
+
window.Auth = {
|
|
1338
|
+
getToken: () => localStorage.getItem('authToken'),
|
|
1339
|
+
|
|
1340
|
+
logout: () => {
|
|
1341
|
+
localStorage.removeItem('authToken');
|
|
1342
|
+
window.location.href = '/login';
|
|
1343
|
+
},
|
|
1344
|
+
|
|
1345
|
+
// Add auth headers to fetch requests
|
|
1346
|
+
authenticatedFetch: (url, options = {}) => {
|
|
1347
|
+
const token = Auth.getToken();
|
|
1348
|
+
if (token) {
|
|
1349
|
+
options.headers = options.headers || {};
|
|
1350
|
+
options.headers['Authorization'] = `Bearer ${token}`;
|
|
1351
|
+
}
|
|
1352
|
+
return fetch(url, options);
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
1355
|
+
|
|
1356
|
+
controls.Login = Login;
|
|
1357
|
+
module.exports = jsgui;
|
|
1358
|
+
```
|
|
1359
|
+
|
|
1360
|
+
These examples demonstrate advanced patterns for building complex applications with JSGUI3 Server, including real-time collaboration, custom controls, production deployment, and authentication systems.
|