webspresso 0.0.90 → 0.0.93
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/README.md +98 -2509
- package/bin/commands/admin-password.js +8 -2
- package/bin/commands/api.js +7 -1
- package/bin/commands/favicon-generate.js +6 -4
- package/bin/commands/mcp.js +162 -0
- package/bin/commands/new.js +7 -1
- package/bin/commands/page.js +7 -1
- package/bin/commands/seed.js +7 -1
- package/bin/commands/service.js +199 -0
- package/bin/commands/skill.js +7 -1
- package/bin/webspresso.js +4 -0
- package/core/auth/middleware.js +9 -0
- package/core/compileSchema.js +1 -5
- package/core/compression/compressible.js +63 -0
- package/core/compression/compression-stream.js +59 -0
- package/core/compression/index.js +25 -0
- package/core/compression/middleware.js +310 -0
- package/core/compression/negotiate.js +165 -0
- package/core/errors/base.js +31 -0
- package/core/errors/domain.js +193 -0
- package/core/errors/http.js +133 -0
- package/core/errors/index.js +77 -0
- package/core/errors/middleware.js +162 -0
- package/core/errors/normalize.js +171 -0
- package/core/kernel/app.js +2 -1
- package/core/kernel/index.js +5 -1
- package/core/orm/cache/fingerprint.js +23 -5
- package/core/orm/complexity.js +120 -0
- package/core/orm/eager-loader.js +54 -9
- package/core/orm/events.js +64 -14
- package/core/orm/index.js +83 -12
- package/core/orm/model.js +13 -2
- package/core/orm/query-builder.js +30 -4
- package/core/orm/repository.js +57 -25
- package/core/orm/transaction.js +39 -5
- package/core/orm/utils.js +6 -3
- package/core/queue/adapters/database.js +317 -0
- package/core/queue/adapters/memory.js +176 -0
- package/core/queue/adapters/redis.js +357 -0
- package/core/queue/index.js +21 -0
- package/core/queue/job.js +103 -0
- package/core/queue/manager.js +413 -0
- package/core/realtime/adapters/redis.js +431 -0
- package/core/realtime/adapters/socket-io.js +203 -0
- package/core/realtime/adapters/sse.js +210 -0
- package/core/realtime/adapters/websocket.js +337 -0
- package/core/realtime/auth-manager.js +140 -0
- package/core/realtime/core.js +359 -0
- package/core/realtime/emitter.js +98 -0
- package/core/realtime/errors.js +71 -0
- package/core/realtime/index.js +79 -0
- package/core/realtime/reconnect-manager.js +118 -0
- package/core/realtime/registry.js +243 -0
- package/core/shutdown/http-adapter.js +134 -0
- package/core/shutdown/index.js +12 -0
- package/core/shutdown/shutdown-manager.js +312 -0
- package/core/ssr/index.js +13 -0
- package/core/ssr/stream.js +174 -0
- package/core/validation/index.js +70 -0
- package/index.d.ts +924 -53
- package/index.js +110 -1
- package/package.json +30 -2
- package/plugins/admin-panel/api.js +37 -4
- package/plugins/admin-panel/app.js +57 -17
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +10 -2
- package/plugins/admin-panel/client/parts/04-field-renderers.js +102 -58
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +51 -27
- package/plugins/admin-panel/client/parts/07-model-list.js +9 -4
- package/plugins/admin-panel/client/parts/08-record-list.js +37 -26
- package/plugins/admin-panel/client/parts/09-record-form.js +168 -19
- package/plugins/admin-panel/core/admin-module.js +2 -2
- package/plugins/admin-panel/core/api-extensions.js +149 -0
- package/plugins/admin-panel/field-renderers/rich-text.js +3 -2
- package/plugins/admin-panel/index.js +155 -23
- package/plugins/admin-panel/modules/bulk-actions.js +9 -2
- package/plugins/admin-panel/modules/custom-pages.js +144 -45
- package/plugins/admin-panel/modules/dashboard.js +8 -1
- package/plugins/admin-panel/modules/menu.js +85 -0
- package/plugins/admin-panel/modules/user-management.js +5 -2
- package/plugins/admin-panel/vendor/mithril.min.js +1 -0
- package/plugins/admin-panel/vendor/quill.min.js +7 -0
- package/plugins/admin-panel/vendor/quill.snow.min.css +945 -0
- package/plugins/admin-panel/vendor/tailwind.min.js +65 -0
- package/plugins/basic-auth/index.js +293 -0
- package/plugins/data-exchange/export-xlsx.js +28 -2
- package/plugins/data-exchange/import.js +55 -16
- package/plugins/data-exchange/index.js +19 -0
- package/plugins/data-exchange/parse-table.js +14 -5
- package/plugins/data-exchange/record-selection.js +2 -0
- package/plugins/email/index.js +14 -1
- package/plugins/index.js +33 -0
- package/plugins/mcp/auth.js +104 -0
- package/plugins/mcp/discovery.js +88 -0
- package/plugins/mcp/index.js +208 -0
- package/plugins/mcp/prompts.js +149 -0
- package/plugins/mcp/providers/orm.js +325 -0
- package/plugins/mcp/providers/services.js +190 -0
- package/plugins/mcp/providers/system.js +87 -0
- package/plugins/mcp/server.js +472 -0
- package/plugins/mcp/transports/sse.js +139 -0
- package/plugins/mcp/transports/stdio.js +108 -0
- package/plugins/queue/index.js +108 -0
- package/plugins/realtime/index.js +150 -0
- package/plugins/recaptcha/index.js +11 -0
- package/plugins/redirect/index.js +9 -1
- package/plugins/rest-resources/index.js +280 -11
- package/plugins/upload/index.js +16 -0
- package/src/app-context.js +46 -6
- package/src/file-router.js +366 -130
- package/src/helpers.js +20 -2
- package/src/njk-frontmatter.js +24 -14
- package/src/plugin-manager.js +124 -4
- package/src/server.js +332 -54
- package/src/services/builtins/auth.js +273 -0
- package/src/services/builtins/exchange.js +229 -0
- package/src/services/builtins/mail.js +134 -0
- package/src/services/builtins/media.js +196 -0
- package/src/services/builtins/system.js +221 -0
- package/src/services/discovery.js +141 -0
- package/src/services/executor.js +292 -0
- package/src/services/index.js +70 -0
- package/src/services/memoize.js +256 -0
- package/src/services/registry.js +290 -0
- package/src/services/validator.js +230 -0
- package/templates/skills/webspresso-usage/REFERENCE-framework.md +6 -4
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* @module plugins/admin-panel
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
7
9
|
const { createAdminUserModel } = require('./admin-user-model');
|
|
8
10
|
const { generateAdminUsersMigration } = require('./migration-template');
|
|
9
11
|
const { createApiHandlers } = require('./api');
|
|
@@ -49,6 +51,7 @@ function adminPanelPlugin(options = {}) {
|
|
|
49
51
|
configure,
|
|
50
52
|
uploadUrl: uploadUrlOption,
|
|
51
53
|
richTextSanitize = true,
|
|
54
|
+
dataExchange: dataExchangeOption = true,
|
|
52
55
|
} = options;
|
|
53
56
|
|
|
54
57
|
const adminUsersConfig = typeof adminUsersOption === 'object' ? adminUsersOption : { enabled: adminUsersOption !== false };
|
|
@@ -75,22 +78,26 @@ function adminPanelPlugin(options = {}) {
|
|
|
75
78
|
version: '2.0.0',
|
|
76
79
|
description: 'Modular admin panel for Webspresso with extensions support',
|
|
77
80
|
|
|
78
|
-
// CSP requirements for admin panel scripts, styles, and custom page iframe sources
|
|
79
|
-
// Note: cdn.quilljs.com 301-redirects to cdn.jsdelivr.net; CSP is enforced on the final URL.
|
|
81
|
+
// CSP requirements for admin panel scripts, styles, and custom page iframe sources (Offline Zero-CDN)
|
|
80
82
|
csp: {
|
|
81
83
|
styleSrc: [
|
|
84
|
+
"'self'",
|
|
85
|
+
"'unsafe-inline'",
|
|
82
86
|
'https://cdn.quilljs.com',
|
|
83
87
|
'https://cdn.jsdelivr.net',
|
|
84
88
|
'https://unpkg.com',
|
|
85
89
|
'https://cdn.tailwindcss.com',
|
|
86
90
|
],
|
|
87
91
|
scriptSrc: [
|
|
92
|
+
"'self'",
|
|
93
|
+
"'unsafe-inline'",
|
|
94
|
+
"'unsafe-eval'",
|
|
88
95
|
'https://cdn.quilljs.com',
|
|
89
96
|
'https://cdn.jsdelivr.net',
|
|
90
97
|
'https://unpkg.com',
|
|
91
98
|
'https://cdn.tailwindcss.com',
|
|
92
99
|
],
|
|
93
|
-
connectSrc: ['https://unpkg.com', 'https://cdn.tailwindcss.com', 'https://cdn.jsdelivr.net'],
|
|
100
|
+
connectSrc: ["'self'", 'https://unpkg.com', 'https://cdn.tailwindcss.com', 'https://cdn.jsdelivr.net'],
|
|
94
101
|
frameSrc: ["'self'", 'data:', 'blob:', 'https:'],
|
|
95
102
|
},
|
|
96
103
|
enabled,
|
|
@@ -162,6 +169,15 @@ function adminPanelPlugin(options = {}) {
|
|
|
162
169
|
/^https:/i.test(String(process.env.BASE_URL || '').trim())
|
|
163
170
|
? true
|
|
164
171
|
: 'auto';
|
|
172
|
+
if (process.env.NODE_ENV === 'production') {
|
|
173
|
+
const warnMsg = '[webspresso:admin] Warning: Admin panel is using default MemoryStore in production. Please configure a persistent session store to prevent memory leaks.';
|
|
174
|
+
if (typeof process !== 'undefined' && typeof process.emitWarning === 'function') {
|
|
175
|
+
process.emitWarning(warnMsg, 'SecurityWarning');
|
|
176
|
+
} else {
|
|
177
|
+
console.warn(warnMsg);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
165
181
|
ctx.app.use(session({
|
|
166
182
|
secret,
|
|
167
183
|
resave: false,
|
|
@@ -175,6 +191,17 @@ function adminPanelPlugin(options = {}) {
|
|
|
175
191
|
}));
|
|
176
192
|
ctx.app._webspressoSessionInitialized = true;
|
|
177
193
|
}
|
|
194
|
+
|
|
195
|
+
// Auto-register built-in exchange services if enabled and serviceRegistry exists
|
|
196
|
+
if (enabled && dataExchangeOption !== false && ctx.app && ctx.app.serviceRegistry) {
|
|
197
|
+
const { createExchangeServices } = require('../../src/services/builtins/exchange');
|
|
198
|
+
const exchangeServices = createExchangeServices({ db });
|
|
199
|
+
for (const [name, def] of Object.entries(exchangeServices)) {
|
|
200
|
+
if (!ctx.app.serviceRegistry.has(name)) {
|
|
201
|
+
ctx.app.serviceRegistry.register(name, def);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
178
205
|
},
|
|
179
206
|
|
|
180
207
|
/**
|
|
@@ -189,22 +216,24 @@ function adminPanelPlugin(options = {}) {
|
|
|
189
216
|
const { app } = ctx;
|
|
190
217
|
|
|
191
218
|
const uploadUrlResolved =
|
|
192
|
-
uploadUrlOption || app.get('webspresso.uploadPath')
|
|
219
|
+
uploadUrlOption || (app && typeof app.get === 'function' ? app.get('webspresso.uploadPath') : null);
|
|
193
220
|
if (uploadUrlResolved) {
|
|
194
221
|
registry.configure({ uploadUrl: uploadUrlResolved });
|
|
195
222
|
}
|
|
196
223
|
|
|
197
224
|
// Check if admin_users table exists (migration run)
|
|
198
|
-
db
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
225
|
+
if (db?.knex?.schema?.hasTable) {
|
|
226
|
+
db.knex.schema.hasTable('admin_users').then((hasAdminTable) => {
|
|
227
|
+
if (!hasAdminTable) {
|
|
228
|
+
console.warn(
|
|
229
|
+
'\n⚠️ [admin-panel] admin_users table not found. Run: webspresso admin:setup\n' +
|
|
230
|
+
' Or create the table manually via migration. See: AdminUser model / getMigrationTemplate()\n'
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
}).catch((err) => {
|
|
234
|
+
console.warn('[admin-panel] Could not check admin_users table:', err.message);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
208
237
|
|
|
209
238
|
// Create and register AdminUser model
|
|
210
239
|
const { hasModel: hasGlobalModel, getModel: getGlobalModel } = require('../../core/orm/model');
|
|
@@ -251,7 +280,7 @@ function adminPanelPlugin(options = {}) {
|
|
|
251
280
|
path: adminPath,
|
|
252
281
|
db,
|
|
253
282
|
AdminUser,
|
|
254
|
-
hashPassword: (password, rounds) => bcrypt.hash(password, rounds),
|
|
283
|
+
hashPassword: (password, rounds) => bcrypt.hash(password, process.env.NODE_ENV === 'test' ? 4 : (rounds || 10)),
|
|
255
284
|
comparePassword: (password, hash) => bcrypt.compare(password, hash),
|
|
256
285
|
richTextSanitize,
|
|
257
286
|
});
|
|
@@ -270,7 +299,7 @@ function adminPanelPlugin(options = {}) {
|
|
|
270
299
|
adminUsersHandlers = createAdminUsersApiHandlers({
|
|
271
300
|
db,
|
|
272
301
|
AdminUser,
|
|
273
|
-
hashPassword: (password, rounds) => bcrypt.hash(password, rounds),
|
|
302
|
+
hashPassword: (password, rounds) => bcrypt.hash(password, process.env.NODE_ENV === 'test' ? 4 : (rounds || 10)),
|
|
274
303
|
});
|
|
275
304
|
}
|
|
276
305
|
|
|
@@ -313,6 +342,8 @@ function adminPanelPlugin(options = {}) {
|
|
|
313
342
|
// Settings API routes
|
|
314
343
|
ctx.addRoute('get', `${adminPath}/api/extensions/settings`, requireAuth, extensionHandlers.settingsGetHandler);
|
|
315
344
|
ctx.addRoute('post', `${adminPath}/api/extensions/settings`, requireAuth, extensionHandlers.settingsUpdateHandler);
|
|
345
|
+
ctx.addRoute('get', `${adminPath}/api/extensions/system-info`, requireAuth, extensionHandlers.systemInfoHandler);
|
|
346
|
+
ctx.addRoute('get', `${adminPath}/api/system-info`, requireAuth, extensionHandlers.systemInfoHandler);
|
|
316
347
|
|
|
317
348
|
// Custom pages API routes
|
|
318
349
|
ctx.addRoute('get', `${adminPath}/api/extensions/pages/:pageId/data`, requireAuth, pageHandlers.getPageData);
|
|
@@ -360,6 +391,73 @@ function adminPanelPlugin(options = {}) {
|
|
|
360
391
|
// Query API routes (auth required)
|
|
361
392
|
ctx.addRoute('get', `${adminPath}/api/models/:model/queries/:query`, requireAuth, apiHandlers.queryHandler);
|
|
362
393
|
|
|
394
|
+
// Data exchange (Excel export & CSV/XLSX import) routes
|
|
395
|
+
const dataExchangeConfig = typeof dataExchangeOption === 'object' ? dataExchangeOption : { enabled: dataExchangeOption !== false };
|
|
396
|
+
if (dataExchangeConfig.enabled !== false && !ctx.app?._dataExchangeRoutesRegistered) {
|
|
397
|
+
const { createExportXlsxHandler } = require('../data-exchange/export-xlsx');
|
|
398
|
+
const { createImportHandler } = require('../data-exchange/import');
|
|
399
|
+
const exportHandler = createExportXlsxHandler({ db, maxRows: dataExchangeConfig.maxRows || 10000 });
|
|
400
|
+
const importHandler = createImportHandler({
|
|
401
|
+
db,
|
|
402
|
+
maxRows: dataExchangeConfig.maxRows || 10000,
|
|
403
|
+
maxFileBytes: dataExchangeConfig.maxFileBytes || 10 * 1024 * 1024,
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const base = `${adminPath}/api/data-exchange`;
|
|
407
|
+
ctx.addRoute('get', `${base}/export/:model`, requireAuth, exportHandler);
|
|
408
|
+
ctx.addRoute('post', `${base}/export/:model`, requireAuth, exportHandler);
|
|
409
|
+
ctx.addRoute('post', `${base}/import/:model`, requireAuth, importHandler);
|
|
410
|
+
|
|
411
|
+
if (!registry.bulkActions.has('export-xlsx')) {
|
|
412
|
+
registry.registerBulkAction('export-xlsx', {
|
|
413
|
+
label: 'Export as Excel',
|
|
414
|
+
icon: 'download',
|
|
415
|
+
color: 'blue',
|
|
416
|
+
models: '*',
|
|
417
|
+
confirm: false,
|
|
418
|
+
handler: async (records, modelName, { db: d }) => {
|
|
419
|
+
const { getModel } = require('../../core/orm/model');
|
|
420
|
+
const model = d.getModel ? d.getModel(modelName) : getModel(modelName);
|
|
421
|
+
const pk = model?.primaryKey || 'id';
|
|
422
|
+
const ids = records.map((r) => r[pk]).join(',');
|
|
423
|
+
return {
|
|
424
|
+
download: true,
|
|
425
|
+
url: `${adminPath}/api/data-exchange/export/${modelName}?ids=${encodeURIComponent(ids)}`,
|
|
426
|
+
filename: `${modelName}_export.xlsx`,
|
|
427
|
+
};
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (ctx.app) {
|
|
433
|
+
ctx.app._dataExchangeRoutesRegistered = true;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ==========================================
|
|
438
|
+
// Static Vendor Assets (Offline Zero-CDN)
|
|
439
|
+
// ==========================================
|
|
440
|
+
const vendorDir = path.join(__dirname, 'vendor');
|
|
441
|
+
ctx.addRoute('get', `${adminPath}/vendor/:file`, (req, res) => {
|
|
442
|
+
const fileName = path.basename(req.params.file);
|
|
443
|
+
const filePath = path.join(vendorDir, fileName);
|
|
444
|
+
|
|
445
|
+
if (!fs.existsSync(filePath)) {
|
|
446
|
+
return res.status(404).send('Vendor asset not found');
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (fileName.endsWith('.js')) {
|
|
450
|
+
res.type('application/javascript');
|
|
451
|
+
} else if (fileName.endsWith('.css')) {
|
|
452
|
+
res.type('text/css');
|
|
453
|
+
} else {
|
|
454
|
+
res.type('application/octet-stream');
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
res.setHeader('Cache-Control', 'public, max-age=86400');
|
|
458
|
+
res.sendFile(filePath);
|
|
459
|
+
});
|
|
460
|
+
|
|
363
461
|
// ==========================================
|
|
364
462
|
// HTML Endpoints
|
|
365
463
|
// ==========================================
|
|
@@ -433,10 +531,14 @@ function generateAdminPanelHtml(adminPath, registry) {
|
|
|
433
531
|
|
|
434
532
|
const settings = registry.settings;
|
|
435
533
|
const fieldRenderersCode = Array.from(registry.fieldRenderers.entries()).map(([type, r]) => {
|
|
436
|
-
return `
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
534
|
+
return `try {
|
|
535
|
+
window.__customFieldRenderers['${type}'] = {
|
|
536
|
+
display: ${r.display ? serializeJs(r.display) : 'null'},
|
|
537
|
+
edit: ${r.edit ? serializeJs(r.edit) : 'null'}
|
|
538
|
+
};
|
|
539
|
+
} catch (err) {
|
|
540
|
+
console.error('[Admin Panel] Failed to register custom field renderer "${type}":', err);
|
|
541
|
+
}`;
|
|
440
542
|
}).join('\n');
|
|
441
543
|
|
|
442
544
|
return `<!DOCTYPE html>
|
|
@@ -465,9 +567,9 @@ function generateAdminPanelHtml(adminPath, registry) {
|
|
|
465
567
|
});
|
|
466
568
|
} catch (e) {}
|
|
467
569
|
})();
|
|
468
|
-
|
|
469
|
-
<script src="
|
|
470
|
-
<script src="
|
|
570
|
+
<script>window.__ADMIN_PATH__ = ${JSON.stringify(adminPath)};</script>
|
|
571
|
+
<script src="${adminPath}/vendor/mithril.min.js"></script>
|
|
572
|
+
<script src="${adminPath}/vendor/tailwind.min.js"></script>
|
|
471
573
|
<script>tailwind.config = { darkMode: 'class' };</script>
|
|
472
574
|
<style>
|
|
473
575
|
body { margin: 0; font-family: system-ui, -apple-system, sans-serif; }
|
|
@@ -484,6 +586,35 @@ function generateAdminPanelHtml(adminPath, registry) {
|
|
|
484
586
|
.dark .ql-snow .ql-stroke { stroke: #94a3b8; }
|
|
485
587
|
.dark .ql-snow .ql-fill { fill: #94a3b8; }
|
|
486
588
|
.dark .ql-picker { color: #cbd5e1; }
|
|
589
|
+
@media (min-width: 768px) {
|
|
590
|
+
.admin-table-sticky-check {
|
|
591
|
+
position: sticky;
|
|
592
|
+
left: 0;
|
|
593
|
+
z-index: 5;
|
|
594
|
+
box-shadow: 4px 0 8px -4px rgba(0, 0, 0, 0.08);
|
|
595
|
+
}
|
|
596
|
+
th.admin-table-sticky-check {
|
|
597
|
+
z-index: 15;
|
|
598
|
+
}
|
|
599
|
+
.admin-table-sticky-first {
|
|
600
|
+
position: sticky;
|
|
601
|
+
left: 40px;
|
|
602
|
+
z-index: 5;
|
|
603
|
+
box-shadow: 4px 0 8px -4px rgba(0, 0, 0, 0.08);
|
|
604
|
+
}
|
|
605
|
+
th.admin-table-sticky-first {
|
|
606
|
+
z-index: 15;
|
|
607
|
+
}
|
|
608
|
+
.admin-table-sticky-actions {
|
|
609
|
+
position: sticky;
|
|
610
|
+
right: 0;
|
|
611
|
+
z-index: 5;
|
|
612
|
+
box-shadow: -4px 0 8px -4px rgba(0, 0, 0, 0.08);
|
|
613
|
+
}
|
|
614
|
+
th.admin-table-sticky-actions {
|
|
615
|
+
z-index: 15;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
487
618
|
</style>
|
|
488
619
|
${registry.getStylesHtml()}
|
|
489
620
|
${registry.getScriptsHtml('head')}
|
|
@@ -530,5 +661,6 @@ function generateAdminPanelHtml(adminPath, registry) {
|
|
|
530
661
|
|
|
531
662
|
// Export registry and utilities for external use
|
|
532
663
|
module.exports = adminPanelPlugin;
|
|
664
|
+
module.exports.adminPanelPlugin = adminPanelPlugin;
|
|
533
665
|
module.exports.AdminRegistry = AdminRegistry;
|
|
534
666
|
module.exports.defaultRegistry = defaultRegistry;
|
|
@@ -238,11 +238,18 @@ async function executeBulkAction(action, selectedIds, modelName) {
|
|
|
238
238
|
if (result.result?.download) {
|
|
239
239
|
// Trigger download
|
|
240
240
|
const link = document.createElement('a');
|
|
241
|
-
link.
|
|
241
|
+
link.style.display = 'none';
|
|
242
|
+
const rawUrl = result.result.url || '';
|
|
243
|
+
const adminPath = window.__ADMIN_PATH__ || '/_admin';
|
|
244
|
+
link.href = (rawUrl.startsWith('/') && !rawUrl.startsWith(adminPath))
|
|
245
|
+
? adminPath + rawUrl
|
|
246
|
+
: rawUrl;
|
|
242
247
|
link.download = result.result.filename || 'export';
|
|
243
248
|
document.body.appendChild(link);
|
|
244
249
|
link.click();
|
|
245
|
-
|
|
250
|
+
setTimeout(function () {
|
|
251
|
+
if (link.parentNode) link.parentNode.removeChild(link);
|
|
252
|
+
}, 1000);
|
|
246
253
|
return { success: true, message: 'Download started' };
|
|
247
254
|
}
|
|
248
255
|
|
|
@@ -315,45 +315,41 @@ function createCustomPage(pageConfig) {
|
|
|
315
315
|
const SettingsPage = {
|
|
316
316
|
oninit(vnode) {
|
|
317
317
|
vnode.state.config = null;
|
|
318
|
+
vnode.state.systemInfo = null;
|
|
318
319
|
vnode.state.loading = true;
|
|
319
320
|
vnode.state.saving = false;
|
|
321
|
+
vnode.state.checkingUpdates = false;
|
|
320
322
|
vnode.state.formData = {};
|
|
321
323
|
vnode.state.reloadBusy = false;
|
|
324
|
+
vnode.state.error = null;
|
|
322
325
|
|
|
323
|
-
vnode.state.
|
|
326
|
+
vnode.state.loadAll = (forceUpdateCheck = false) => {
|
|
324
327
|
vnode.state.reloadBusy = true;
|
|
325
328
|
vnode.state.error = null;
|
|
329
|
+
if (forceUpdateCheck) vnode.state.checkingUpdates = true;
|
|
326
330
|
m.redraw();
|
|
327
|
-
return
|
|
328
|
-
.
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
.
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
.finally(() => {
|
|
336
|
-
vnode.state.reloadBusy = false;
|
|
337
|
-
m.redraw();
|
|
338
|
-
});
|
|
339
|
-
};
|
|
340
|
-
|
|
341
|
-
api.get('/extensions/config')
|
|
342
|
-
.then(result => {
|
|
343
|
-
vnode.state.config = result;
|
|
344
|
-
vnode.state.formData = { ...result.settings };
|
|
345
|
-
vnode.state.loading = false;
|
|
346
|
-
m.redraw();
|
|
347
|
-
})
|
|
348
|
-
.catch(err => {
|
|
331
|
+
return Promise.all([
|
|
332
|
+
api.get('/extensions/config'),
|
|
333
|
+
api.get('/extensions/system-info' + (forceUpdateCheck ? '?force=true' : '')).catch(() => null)
|
|
334
|
+
]).then(([configRes, sysRes]) => {
|
|
335
|
+
vnode.state.config = configRes;
|
|
336
|
+
vnode.state.formData = { ...configRes.settings };
|
|
337
|
+
if (sysRes) vnode.state.systemInfo = sysRes;
|
|
338
|
+
}).catch(err => {
|
|
349
339
|
vnode.state.error = err.message;
|
|
340
|
+
}).finally(() => {
|
|
350
341
|
vnode.state.loading = false;
|
|
342
|
+
vnode.state.reloadBusy = false;
|
|
343
|
+
vnode.state.checkingUpdates = false;
|
|
351
344
|
m.redraw();
|
|
352
345
|
});
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
vnode.state.loadAll();
|
|
353
349
|
},
|
|
354
350
|
|
|
355
351
|
view(vnode) {
|
|
356
|
-
const { config, loading, saving, formData, error } = vnode.state;
|
|
352
|
+
const { config, systemInfo, loading, saving, checkingUpdates, formData, error, reloadBusy } = vnode.state;
|
|
357
353
|
|
|
358
354
|
if (loading) {
|
|
359
355
|
return m(Layout, m('div.flex.justify-center.py-12', m(Spinner)));
|
|
@@ -365,43 +361,147 @@ const SettingsPage = {
|
|
|
365
361
|
m('div.mb-6.flex.items-start.justify-between.gap-4', [
|
|
366
362
|
m('div', [
|
|
367
363
|
m('h1.text-2xl.font-bold.text-gray-900.dark:text-slate-100', 'Admin Settings'),
|
|
368
|
-
m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', '
|
|
364
|
+
m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', 'System overview and admin panel configuration'),
|
|
369
365
|
]),
|
|
370
366
|
m(RefreshIconButton, {
|
|
371
367
|
title: 'Reload settings from server',
|
|
372
|
-
disabled: saving,
|
|
373
|
-
spinning:
|
|
368
|
+
disabled: saving || reloadBusy,
|
|
369
|
+
spinning: reloadBusy,
|
|
374
370
|
onclick: () => {
|
|
375
|
-
vnode.state.
|
|
371
|
+
vnode.state.loadAll();
|
|
376
372
|
},
|
|
377
373
|
}),
|
|
378
374
|
]),
|
|
379
375
|
|
|
380
|
-
error && m('div.bg-red-50.border.border-red-200.rounded.p-4.text-red-700.mb-
|
|
376
|
+
error && m('div.bg-red-50.dark:bg-red-950/40.border.border-red-200.dark:border-red-800.rounded-lg.p-4.text-red-700.dark:text-red-300.mb-6', error),
|
|
377
|
+
|
|
378
|
+
// System Information Card
|
|
379
|
+
systemInfo ? m('div.bg-white.dark:bg-slate-800.rounded-xl.shadow-sm.border.border-gray-200.dark:border-slate-700.mb-6.overflow-hidden', [
|
|
380
|
+
m('div.px-6.py-4.border-b.border-gray-100.dark:border-slate-700.flex.items-center.justify-between.bg-gray-50/50.dark:bg-slate-800/80', [
|
|
381
|
+
m('div.flex.items-center.gap-2.5', [
|
|
382
|
+
m('div.w-8.h-8.rounded-lg.bg-indigo-50.dark:bg-indigo-950/60.text-indigo-600.dark:text-indigo-400.flex.items-center.justify-center', [
|
|
383
|
+
m('svg.w-4.h-4', { fill: 'none', stroke: 'currentColor', viewBox: '0 0 24 24' },
|
|
384
|
+
m('path', { 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': '2', d: 'M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z' })
|
|
385
|
+
)
|
|
386
|
+
]),
|
|
387
|
+
m('div', [
|
|
388
|
+
m('h2.text-base.font-semibold.text-gray-900.dark:text-slate-100', 'System Information'),
|
|
389
|
+
m('p.text-xs.text-gray-500.dark:text-slate-400', 'Runtime environment and version status')
|
|
390
|
+
])
|
|
391
|
+
]),
|
|
392
|
+
m('button.inline-flex.items-center.gap-1.5.px-3.py-1.5.text-xs.font-medium.text-indigo-600.dark:text-indigo-400.bg-indigo-50.dark:bg-indigo-950/50.hover:bg-indigo-100.dark:hover:bg-indigo-900/50.rounded-lg.transition-colors.disabled:opacity-50', {
|
|
393
|
+
disabled: checkingUpdates,
|
|
394
|
+
onclick: () => {
|
|
395
|
+
vnode.state.loadAll(true);
|
|
396
|
+
}
|
|
397
|
+
}, [
|
|
398
|
+
checkingUpdates ? m(Spinner) : m('svg.w-3.5.h-3.5', { fill: 'none', stroke: 'currentColor', viewBox: '0 0 24 24' },
|
|
399
|
+
m('path', { 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': '2', d: 'M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15' })
|
|
400
|
+
),
|
|
401
|
+
checkingUpdates ? 'Checking...' : 'Check for Updates'
|
|
402
|
+
])
|
|
403
|
+
]),
|
|
404
|
+
m('div.p-6.grid.grid-cols-1.md:grid-cols-2.lg:grid-cols-3.gap-5', [
|
|
405
|
+
// Webspresso Version
|
|
406
|
+
m('div.p-3.5.rounded-lg.bg-gray-50.dark:bg-slate-900/50.border.border-gray-100.dark:border-slate-800', [
|
|
407
|
+
m('div.text-xs.font-medium.text-gray-500.dark:text-slate-400', 'Webspresso Framework'),
|
|
408
|
+
m('div.mt-1.5.flex.items-center.gap-2.flex-wrap', [
|
|
409
|
+
m('span.text-sm.font-semibold.text-gray-900.dark:text-slate-100', 'v' + (systemInfo.webspressoVersion || '0.0.1')),
|
|
410
|
+
systemInfo.hasUpdate
|
|
411
|
+
? m('span.inline-flex.items-center.px-2.py-0.5.rounded-full.text-[11px].font-semibold.bg-amber-100.text-amber-800.dark:bg-amber-950/60.dark:text-amber-300.border.border-amber-200.dark:border-amber-800', '⚡ Update: v' + systemInfo.latestVersion)
|
|
412
|
+
: m('span.inline-flex.items-center.px-2.py-0.5.rounded-full.text-[11px].font-semibold.bg-emerald-100.text-emerald-800.dark:bg-emerald-950/60.dark:text-emerald-300.border.border-emerald-200.dark:border-emerald-800', '✓ Up to date')
|
|
413
|
+
]),
|
|
414
|
+
systemInfo.hasUpdate && m('div.mt-2', [
|
|
415
|
+
m('code.text-[11px].bg-white.dark:bg-slate-800.text-indigo-600.dark:text-indigo-400.px-2.py-0.5.rounded.border.border-gray-200.dark:border-slate-700.select-all', 'npm i webspresso@latest')
|
|
416
|
+
])
|
|
417
|
+
]),
|
|
418
|
+
|
|
419
|
+
// Node.js Version
|
|
420
|
+
m('div.p-3.5.rounded-lg.bg-gray-50.dark:bg-slate-900/50.border.border-gray-100.dark:border-slate-800', [
|
|
421
|
+
m('div.text-xs.font-medium.text-gray-500.dark:text-slate-400', 'Node.js Runtime'),
|
|
422
|
+
m('div.mt-1.5.text-sm.font-semibold.text-gray-900.dark:text-slate-100', systemInfo.nodeVersion || process.version)
|
|
423
|
+
]),
|
|
381
424
|
|
|
382
|
-
|
|
383
|
-
|
|
425
|
+
// Environment
|
|
426
|
+
m('div.p-3.5.rounded-lg.bg-gray-50.dark:bg-slate-900/50.border.border-gray-100.dark:border-slate-800', [
|
|
427
|
+
m('div.text-xs.font-medium.text-gray-500.dark:text-slate-400', 'Environment'),
|
|
428
|
+
m('div.mt-1.5', [
|
|
429
|
+
m('span.inline-flex.items-center.px-2.5.py-0.5.rounded-full.text-xs.font-semibold', {
|
|
430
|
+
class: systemInfo.environment === 'production'
|
|
431
|
+
? 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950/60 dark:text-emerald-300'
|
|
432
|
+
: 'bg-blue-100 text-blue-800 dark:bg-blue-950/60 dark:text-blue-300'
|
|
433
|
+
}, systemInfo.environment || 'development')
|
|
434
|
+
])
|
|
435
|
+
]),
|
|
436
|
+
|
|
437
|
+
// Operating System
|
|
438
|
+
m('div.p-3.5.rounded-lg.bg-gray-50.dark:bg-slate-900/50.border.border-gray-100.dark:border-slate-800', [
|
|
439
|
+
m('div.text-xs.font-medium.text-gray-500.dark:text-slate-400', 'OS & Architecture'),
|
|
440
|
+
m('div.mt-1.5.text-sm.font-semibold.text-gray-900.dark:text-slate-100.truncate', { title: systemInfo.platform }, systemInfo.platform || 'Server')
|
|
441
|
+
]),
|
|
442
|
+
|
|
443
|
+
// Server Uptime
|
|
444
|
+
m('div.p-3.5.rounded-lg.bg-gray-50.dark:bg-slate-900/50.border.border-gray-100.dark:border-slate-800', [
|
|
445
|
+
m('div.text-xs.font-medium.text-gray-500.dark:text-slate-400', 'Server Uptime'),
|
|
446
|
+
m('div.mt-1.5.text-sm.font-semibold.text-gray-900.dark:text-slate-100', systemInfo.uptimeFormatted || '0s')
|
|
447
|
+
]),
|
|
448
|
+
|
|
449
|
+
// Memory Usage
|
|
450
|
+
m('div.p-3.5.rounded-lg.bg-gray-50.dark:bg-slate-900/50.border.border-gray-100.dark:border-slate-800', [
|
|
451
|
+
m('div.text-xs.font-medium.text-gray-500.dark:text-slate-400', 'Memory (Heap / RSS)'),
|
|
452
|
+
m('div.mt-1.5.text-sm.font-semibold.text-gray-900.dark:text-slate-100',
|
|
453
|
+
(systemInfo.memory?.heapUsed || '0 MB') + ' / ' + (systemInfo.memory?.heapTotal || '0 MB') + ' (' + (systemInfo.memory?.rss || '0 MB') + ')'
|
|
454
|
+
)
|
|
455
|
+
]),
|
|
456
|
+
|
|
457
|
+
// Database Engine
|
|
458
|
+
m('div.p-3.5.rounded-lg.bg-gray-50.dark:bg-slate-900/50.border.border-gray-100.dark:border-slate-800', [
|
|
459
|
+
m('div.text-xs.font-medium.text-gray-500.dark:text-slate-400', 'Database Client'),
|
|
460
|
+
m('div.mt-1.5.text-sm.font-semibold.text-gray-900.dark:text-slate-100', systemInfo.database?.client || 'knex')
|
|
461
|
+
])
|
|
462
|
+
])
|
|
463
|
+
]) : null,
|
|
464
|
+
|
|
465
|
+
// Admin Preferences Card
|
|
466
|
+
m('div.bg-white.dark:bg-slate-800.rounded-xl.shadow-sm.border.border-gray-200.dark:border-slate-700.overflow-hidden', [
|
|
467
|
+
m('div.px-6.py-4.border-b.border-gray-100.dark:border-slate-700.flex.items-center.gap-2.5.bg-gray-50/50.dark:bg-slate-800/80', [
|
|
468
|
+
m('div.w-8.h-8.rounded-lg.bg-blue-50.dark:bg-blue-950/60.text-blue-600.dark:text-blue-400.flex.items-center.justify-center', [
|
|
469
|
+
m('svg.w-4.h-4', { fill: 'none', stroke: 'currentColor', viewBox: '0 0 24 24' },
|
|
470
|
+
m('path', { 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': '2', d: 'M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4' })
|
|
471
|
+
)
|
|
472
|
+
]),
|
|
384
473
|
m('div', [
|
|
385
|
-
m('
|
|
386
|
-
m('
|
|
474
|
+
m('h2.text-base.font-semibold.text-gray-900.dark:text-slate-100', 'Panel Preferences'),
|
|
475
|
+
m('p.text-xs.text-gray-500.dark:text-slate-400', 'Customize admin title, theme colors, and table pagination')
|
|
476
|
+
])
|
|
477
|
+
]),
|
|
478
|
+
|
|
479
|
+
m('div.p-6.space-y-5', [
|
|
480
|
+
m('div', [
|
|
481
|
+
m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300', 'Panel Title'),
|
|
482
|
+
m('input.mt-1.block.w-full.rounded-lg.border-gray-300.dark:border-slate-600.dark:bg-slate-900.dark:text-slate-100.shadow-sm.focus:border-blue-500.focus:ring-blue-500.text-sm.px-3.py-2', {
|
|
387
483
|
type: 'text',
|
|
388
484
|
value: formData.title || '',
|
|
485
|
+
placeholder: 'Admin Panel',
|
|
389
486
|
oninput: (e) => { formData.title = e.target.value; },
|
|
390
487
|
}),
|
|
391
488
|
]),
|
|
392
489
|
|
|
393
490
|
m('div', [
|
|
394
|
-
m('label.block.text-sm.font-medium.text-gray-700', 'Primary Color'),
|
|
395
|
-
m('
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
491
|
+
m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300', 'Primary Accent Color'),
|
|
492
|
+
m('div.mt-1.5.flex.items-center.gap-3', [
|
|
493
|
+
m('input.w-10.h-10.rounded-lg.border.border-gray-300.dark:border-slate-600.cursor-pointer.p-0.5.bg-transparent', {
|
|
494
|
+
type: 'color',
|
|
495
|
+
value: formData.primaryColor || '#3B82F6',
|
|
496
|
+
oninput: (e) => { formData.primaryColor = e.target.value; },
|
|
497
|
+
}),
|
|
498
|
+
m('span.text-xs.font-mono.text-gray-600.dark:text-slate-400', formData.primaryColor || '#3B82F6'),
|
|
499
|
+
]),
|
|
400
500
|
]),
|
|
401
501
|
|
|
402
502
|
m('div', [
|
|
403
|
-
m('label.block.text-sm.font-medium.text-gray-700', 'Records Per Page'),
|
|
404
|
-
m('input.mt-1.block.w-32.rounded.border-gray-300
|
|
503
|
+
m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300', 'Records Per Page'),
|
|
504
|
+
m('input.mt-1.block.w-32.rounded-lg.border-gray-300.dark:border-slate-600.dark:bg-slate-900.dark:text-slate-100.shadow-sm.focus:border-blue-500.focus:ring-blue-500.text-sm.px-3.py-2', {
|
|
405
505
|
type: 'number',
|
|
406
506
|
min: 5,
|
|
407
507
|
max: 100,
|
|
@@ -411,15 +511,14 @@ const SettingsPage = {
|
|
|
411
511
|
]),
|
|
412
512
|
]),
|
|
413
513
|
|
|
414
|
-
m('div.bg-gray-50
|
|
415
|
-
m('button.px-4.py-2.text-sm.font-medium.text-white.bg-blue-600.
|
|
514
|
+
m('div.bg-gray-50.dark:bg-slate-900/70.px-6.py-4.flex.justify-end.gap-3.border-t.border-gray-100.dark:border-slate-700', [
|
|
515
|
+
m('button.inline-flex.items-center.gap-2.px-4.py-2.text-sm.font-medium.text-white.bg-blue-600.hover:bg-blue-700.rounded-lg.shadow-sm.transition-colors.disabled:opacity-50', {
|
|
416
516
|
disabled: saving,
|
|
417
517
|
onclick: async () => {
|
|
418
518
|
vnode.state.saving = true;
|
|
419
519
|
m.redraw();
|
|
420
520
|
try {
|
|
421
521
|
await api.post('/extensions/settings', formData);
|
|
422
|
-
// Reload to apply changes
|
|
423
522
|
window.location.reload();
|
|
424
523
|
} catch (err) {
|
|
425
524
|
vnode.state.error = err.message;
|
|
@@ -427,7 +526,7 @@ const SettingsPage = {
|
|
|
427
526
|
m.redraw();
|
|
428
527
|
}
|
|
429
528
|
},
|
|
430
|
-
}, saving ? 'Saving...' : 'Save Settings'),
|
|
529
|
+
}, saving ? [m(Spinner), 'Saving...'] : 'Save Settings'),
|
|
431
530
|
]),
|
|
432
531
|
]),
|
|
433
532
|
]);
|
|
@@ -384,7 +384,14 @@ const Widget = {
|
|
|
384
384
|
? m('div.flex.justify-center.py-8', m(Spinner))
|
|
385
385
|
: error
|
|
386
386
|
? m('div.text-red-500.text-sm', error)
|
|
387
|
-
: (
|
|
387
|
+
: (() => {
|
|
388
|
+
try {
|
|
389
|
+
return (WidgetRenderers[widget.id] || WidgetRenderers.default).render(data);
|
|
390
|
+
} catch (err) {
|
|
391
|
+
console.error('[Admin Panel] Error rendering widget "' + widget.id + '":', err);
|
|
392
|
+
return m('div.text-red-500.text-xs', 'Widget render error: ' + (err.message || String(err)));
|
|
393
|
+
}
|
|
394
|
+
})(),
|
|
388
395
|
]),
|
|
389
396
|
]);
|
|
390
397
|
},
|