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.
Files changed (125) hide show
  1. package/README.md +98 -2509
  2. package/bin/commands/admin-password.js +8 -2
  3. package/bin/commands/api.js +7 -1
  4. package/bin/commands/favicon-generate.js +6 -4
  5. package/bin/commands/mcp.js +162 -0
  6. package/bin/commands/new.js +7 -1
  7. package/bin/commands/page.js +7 -1
  8. package/bin/commands/seed.js +7 -1
  9. package/bin/commands/service.js +199 -0
  10. package/bin/commands/skill.js +7 -1
  11. package/bin/webspresso.js +4 -0
  12. package/core/auth/middleware.js +9 -0
  13. package/core/compileSchema.js +1 -5
  14. package/core/compression/compressible.js +63 -0
  15. package/core/compression/compression-stream.js +59 -0
  16. package/core/compression/index.js +25 -0
  17. package/core/compression/middleware.js +310 -0
  18. package/core/compression/negotiate.js +165 -0
  19. package/core/errors/base.js +31 -0
  20. package/core/errors/domain.js +193 -0
  21. package/core/errors/http.js +133 -0
  22. package/core/errors/index.js +77 -0
  23. package/core/errors/middleware.js +162 -0
  24. package/core/errors/normalize.js +171 -0
  25. package/core/kernel/app.js +2 -1
  26. package/core/kernel/index.js +5 -1
  27. package/core/orm/cache/fingerprint.js +23 -5
  28. package/core/orm/complexity.js +120 -0
  29. package/core/orm/eager-loader.js +54 -9
  30. package/core/orm/events.js +64 -14
  31. package/core/orm/index.js +83 -12
  32. package/core/orm/model.js +13 -2
  33. package/core/orm/query-builder.js +30 -4
  34. package/core/orm/repository.js +57 -25
  35. package/core/orm/transaction.js +39 -5
  36. package/core/orm/utils.js +6 -3
  37. package/core/queue/adapters/database.js +317 -0
  38. package/core/queue/adapters/memory.js +176 -0
  39. package/core/queue/adapters/redis.js +357 -0
  40. package/core/queue/index.js +21 -0
  41. package/core/queue/job.js +103 -0
  42. package/core/queue/manager.js +413 -0
  43. package/core/realtime/adapters/redis.js +431 -0
  44. package/core/realtime/adapters/socket-io.js +203 -0
  45. package/core/realtime/adapters/sse.js +210 -0
  46. package/core/realtime/adapters/websocket.js +337 -0
  47. package/core/realtime/auth-manager.js +140 -0
  48. package/core/realtime/core.js +359 -0
  49. package/core/realtime/emitter.js +98 -0
  50. package/core/realtime/errors.js +71 -0
  51. package/core/realtime/index.js +79 -0
  52. package/core/realtime/reconnect-manager.js +118 -0
  53. package/core/realtime/registry.js +243 -0
  54. package/core/shutdown/http-adapter.js +134 -0
  55. package/core/shutdown/index.js +12 -0
  56. package/core/shutdown/shutdown-manager.js +312 -0
  57. package/core/ssr/index.js +13 -0
  58. package/core/ssr/stream.js +174 -0
  59. package/core/validation/index.js +70 -0
  60. package/index.d.ts +924 -53
  61. package/index.js +110 -1
  62. package/package.json +30 -2
  63. package/plugins/admin-panel/api.js +37 -4
  64. package/plugins/admin-panel/app.js +57 -17
  65. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +10 -2
  66. package/plugins/admin-panel/client/parts/04-field-renderers.js +102 -58
  67. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +51 -27
  68. package/plugins/admin-panel/client/parts/07-model-list.js +9 -4
  69. package/plugins/admin-panel/client/parts/08-record-list.js +37 -26
  70. package/plugins/admin-panel/client/parts/09-record-form.js +168 -19
  71. package/plugins/admin-panel/core/admin-module.js +2 -2
  72. package/plugins/admin-panel/core/api-extensions.js +149 -0
  73. package/plugins/admin-panel/field-renderers/rich-text.js +3 -2
  74. package/plugins/admin-panel/index.js +155 -23
  75. package/plugins/admin-panel/modules/bulk-actions.js +9 -2
  76. package/plugins/admin-panel/modules/custom-pages.js +144 -45
  77. package/plugins/admin-panel/modules/dashboard.js +8 -1
  78. package/plugins/admin-panel/modules/menu.js +85 -0
  79. package/plugins/admin-panel/modules/user-management.js +5 -2
  80. package/plugins/admin-panel/vendor/mithril.min.js +1 -0
  81. package/plugins/admin-panel/vendor/quill.min.js +7 -0
  82. package/plugins/admin-panel/vendor/quill.snow.min.css +945 -0
  83. package/plugins/admin-panel/vendor/tailwind.min.js +65 -0
  84. package/plugins/basic-auth/index.js +293 -0
  85. package/plugins/data-exchange/export-xlsx.js +28 -2
  86. package/plugins/data-exchange/import.js +55 -16
  87. package/plugins/data-exchange/index.js +19 -0
  88. package/plugins/data-exchange/parse-table.js +14 -5
  89. package/plugins/data-exchange/record-selection.js +2 -0
  90. package/plugins/email/index.js +14 -1
  91. package/plugins/index.js +33 -0
  92. package/plugins/mcp/auth.js +104 -0
  93. package/plugins/mcp/discovery.js +88 -0
  94. package/plugins/mcp/index.js +208 -0
  95. package/plugins/mcp/prompts.js +149 -0
  96. package/plugins/mcp/providers/orm.js +325 -0
  97. package/plugins/mcp/providers/services.js +190 -0
  98. package/plugins/mcp/providers/system.js +87 -0
  99. package/plugins/mcp/server.js +472 -0
  100. package/plugins/mcp/transports/sse.js +139 -0
  101. package/plugins/mcp/transports/stdio.js +108 -0
  102. package/plugins/queue/index.js +108 -0
  103. package/plugins/realtime/index.js +150 -0
  104. package/plugins/recaptcha/index.js +11 -0
  105. package/plugins/redirect/index.js +9 -1
  106. package/plugins/rest-resources/index.js +280 -11
  107. package/plugins/upload/index.js +16 -0
  108. package/src/app-context.js +46 -6
  109. package/src/file-router.js +366 -130
  110. package/src/helpers.js +20 -2
  111. package/src/njk-frontmatter.js +24 -14
  112. package/src/plugin-manager.js +124 -4
  113. package/src/server.js +332 -54
  114. package/src/services/builtins/auth.js +273 -0
  115. package/src/services/builtins/exchange.js +229 -0
  116. package/src/services/builtins/mail.js +134 -0
  117. package/src/services/builtins/media.js +196 -0
  118. package/src/services/builtins/system.js +221 -0
  119. package/src/services/discovery.js +141 -0
  120. package/src/services/executor.js +292 -0
  121. package/src/services/index.js +70 -0
  122. package/src/services/memoize.js +256 -0
  123. package/src/services/registry.js +290 -0
  124. package/src/services/validator.js +230 -0
  125. 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') || null;
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.knex.schema.hasTable('admin_users').then((hasAdminTable) => {
199
- if (!hasAdminTable) {
200
- console.warn(
201
- '\n⚠️ [admin-panel] admin_users table not found. Run: webspresso admin:setup\n' +
202
- ' Or create the table manually via migration. See: AdminUser model / getMigrationTemplate()\n'
203
- );
204
- }
205
- }).catch((err) => {
206
- console.warn('[admin-panel] Could not check admin_users table:', err.message);
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 `window.__customFieldRenderers['${type}'] = {
437
- display: ${r.display ? serializeJs(r.display) : 'null'},
438
- edit: ${r.edit ? serializeJs(r.edit) : 'null'}
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
- </script>
469
- <script src="https://unpkg.com/mithril/mithril.js"></script>
470
- <script src="https://cdn.tailwindcss.com"></script>
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.href = window.__ADMIN_PATH__ + result.result.url;
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
- document.body.removeChild(link);
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.reloadFromServer = () => {
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 api.get('/extensions/config')
328
- .then(result => {
329
- vnode.state.config = result;
330
- vnode.state.formData = { ...result.settings };
331
- })
332
- .catch(err => {
333
- vnode.state.error = err.message;
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', 'Reload discards unsaved changes'),
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: vnode.state.reloadBusy,
368
+ disabled: saving || reloadBusy,
369
+ spinning: reloadBusy,
374
370
  onclick: () => {
375
- vnode.state.reloadFromServer();
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-4', error),
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
- m('div.bg-white dark:bg-slate-800.rounded-lg.shadow', [
383
- m('div.p-6.space-y-4', [
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('label.block.text-sm.font-medium.text-gray-700', 'Panel Title'),
386
- m('input.mt-1.block.w-full.rounded.border-gray-300 dark:border-slate-600.shadow-sm.focus:border-blue-500.focus:ring-blue-500', {
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('input.mt-1.block.w-24.h-10.rounded.border-gray-300 dark:border-slate-600.shadow-sm', {
396
- type: 'color',
397
- value: formData.primaryColor || '#3B82F6',
398
- oninput: (e) => { formData.primaryColor = e.target.value; },
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 dark:border-slate-600.shadow-sm.focus:border-blue-500.focus:ring-blue-500', {
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 dark:bg-slate-900.px-6.py-4.flex.justify-end.gap-3.rounded-b-lg', [
415
- m('button.px-4.py-2.text-sm.font-medium.text-white.bg-blue-600.rounded.hover:bg-blue-700.disabled:opacity-50', {
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
- : (WidgetRenderers[widget.id] || WidgetRenderers.default).render(data),
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
  },