webspresso 0.0.41 → 0.0.43

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webspresso",
3
- "version": "0.0.41",
3
+ "version": "0.0.43",
4
4
  "description": "Minimal, production-ready SSR framework for Node.js with file-based routing, Nunjucks templating, built-in i18n, and CLI tooling",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -50,7 +50,7 @@ m.route(document.getElementById('app'), '/', {
50
50
  if (!isAuth) {
51
51
  return LoginForm;
52
52
  }
53
- return ModelList;
53
+ return Dashboard;
54
54
  }
55
55
  },
56
56
  '/login': {
@@ -386,7 +386,7 @@ function createExtensionApiHandlers(options) {
386
386
  }
387
387
 
388
388
  /**
389
- * Dashboard stats
389
+ * Dashboard stats - includes count, last updated, column count for each model
390
390
  */
391
391
  async function dashboardStatsHandler(req, res) {
392
392
  try {
@@ -402,14 +402,63 @@ function createExtensionApiHandlers(options) {
402
402
  try {
403
403
  const repo = db.getRepository(model.name);
404
404
  const count = await repo.count();
405
+
406
+ // Get column count
407
+ const columnCount = model.columns ? model.columns.size : 0;
408
+
409
+ // Get last created/updated record
410
+ let lastCreated = null;
411
+ let lastUpdated = null;
412
+
413
+ // Try to get the most recently created record
414
+ const createdAtCol = model.columns?.has('created_at') ? 'created_at' :
415
+ model.columns?.has('createdAt') ? 'createdAt' : null;
416
+ const updatedAtCol = model.columns?.has('updated_at') ? 'updated_at' :
417
+ model.columns?.has('updatedAt') ? 'updatedAt' : null;
418
+
419
+ if (createdAtCol) {
420
+ try {
421
+ const lastRecord = await repo.query()
422
+ .orderBy(createdAtCol, 'desc')
423
+ .first();
424
+ if (lastRecord && lastRecord[createdAtCol]) {
425
+ lastCreated = lastRecord[createdAtCol];
426
+ }
427
+ } catch (e) {
428
+ // Column might not exist or be queryable
429
+ }
430
+ }
431
+
432
+ if (updatedAtCol) {
433
+ try {
434
+ const lastRecord = await repo.query()
435
+ .orderBy(updatedAtCol, 'desc')
436
+ .first();
437
+ if (lastRecord && lastRecord[updatedAtCol]) {
438
+ lastUpdated = lastRecord[updatedAtCol];
439
+ }
440
+ } catch (e) {
441
+ // Column might not exist or be queryable
442
+ }
443
+ }
444
+
405
445
  stats[model.name] = {
406
446
  name: model.name,
407
447
  label: model.admin.label || model.name,
408
448
  icon: model.admin.icon,
409
449
  count,
450
+ columnCount,
451
+ lastCreated,
452
+ lastUpdated,
453
+ table: model.table,
410
454
  };
411
455
  } catch (e) {
412
- stats[model.name] = { name: model.name, count: 0, error: e.message };
456
+ stats[model.name] = {
457
+ name: model.name,
458
+ label: model.admin?.label || model.name,
459
+ count: 0,
460
+ error: e.message
461
+ };
413
462
  }
414
463
  }
415
464
  }
@@ -420,6 +469,38 @@ function createExtensionApiHandlers(options) {
420
469
  }
421
470
  }
422
471
 
472
+ /**
473
+ * Get admin settings
474
+ */
475
+ function settingsGetHandler(req, res) {
476
+ try {
477
+ const settings = registry.settings || {};
478
+ res.json({ settings });
479
+ } catch (error) {
480
+ res.status(500).json({ error: error.message });
481
+ }
482
+ }
483
+
484
+ /**
485
+ * Update admin settings
486
+ */
487
+ function settingsUpdateHandler(req, res) {
488
+ try {
489
+ const updates = req.body || {};
490
+
491
+ // Merge with existing settings
492
+ const currentSettings = registry.settings || {};
493
+ const newSettings = { ...currentSettings, ...updates };
494
+
495
+ // Update registry settings
496
+ registry.configure({ settings: newSettings });
497
+
498
+ res.json({ success: true, settings: registry.settings });
499
+ } catch (error) {
500
+ res.status(500).json({ error: error.message });
501
+ }
502
+ }
503
+
423
504
  /**
424
505
  * Export records (CSV/JSON)
425
506
  * Supports both GET (with ids in query) and POST (with ids in body)
@@ -546,6 +627,8 @@ function createExtensionApiHandlers(options) {
546
627
  dashboardStatsHandler,
547
628
  exportHandler,
548
629
  activityLogHandler,
630
+ settingsGetHandler,
631
+ settingsUpdateHandler,
549
632
  };
550
633
  }
551
634
 
@@ -190,6 +190,10 @@ function adminPanelPlugin(options = {}) {
190
190
  ctx.addRoute('post', `${adminPath}/api/extensions/export/:model`, requireAuth, extensionHandlers.exportHandler);
191
191
  ctx.addRoute('post', `${adminPath}/api/extensions/export`, requireAuth, extensionHandlers.exportHandler);
192
192
  ctx.addRoute('get', `${adminPath}/api/extensions/activity`, requireAuth, extensionHandlers.activityLogHandler);
193
+
194
+ // Settings API routes
195
+ ctx.addRoute('get', `${adminPath}/api/extensions/settings`, requireAuth, extensionHandlers.settingsGetHandler);
196
+ ctx.addRoute('post', `${adminPath}/api/extensions/settings`, requireAuth, extensionHandlers.settingsUpdateHandler);
193
197
 
194
198
  // Custom pages API routes
195
199
  ctx.addRoute('get', `${adminPath}/api/extensions/pages/:pageId/data`, requireAuth, pageHandlers.getPageData);
@@ -13,7 +13,7 @@
13
13
  function registerDashboardWidgets(options) {
14
14
  const { registry, db } = options;
15
15
 
16
- // Model stats widget (shows all admin-enabled model counts)
16
+ // Model stats widget (shows all admin-enabled model counts with extended info)
17
17
  registry.registerWidget('model-stats', {
18
18
  title: 'Overview',
19
19
  size: 'full',
@@ -30,17 +30,52 @@ function registerDashboardWidgets(options) {
30
30
  try {
31
31
  const repo = db.getRepository(model.name);
32
32
  const count = await repo.count();
33
+
34
+ // Get column count
35
+ const columnCount = model.columns ? model.columns.size : 0;
36
+
37
+ // Get last updated/created timestamp
38
+ let lastCreated = null;
39
+ let lastUpdated = null;
40
+
41
+ const createdAtCol = model.columns?.has('created_at') ? 'created_at' :
42
+ model.columns?.has('createdAt') ? 'createdAt' : null;
43
+ const updatedAtCol = model.columns?.has('updated_at') ? 'updated_at' :
44
+ model.columns?.has('updatedAt') ? 'updatedAt' : null;
45
+
46
+ if (createdAtCol) {
47
+ try {
48
+ const lastRecord = await repo.query().orderBy(createdAtCol, 'desc').first();
49
+ if (lastRecord && lastRecord[createdAtCol]) {
50
+ lastCreated = lastRecord[createdAtCol];
51
+ }
52
+ } catch (e) { /* ignore */ }
53
+ }
54
+
55
+ if (updatedAtCol) {
56
+ try {
57
+ const lastRecord = await repo.query().orderBy(updatedAtCol, 'desc').first();
58
+ if (lastRecord && lastRecord[updatedAtCol]) {
59
+ lastUpdated = lastRecord[updatedAtCol];
60
+ }
61
+ } catch (e) { /* ignore */ }
62
+ }
63
+
33
64
  stats.push({
34
65
  name: model.name,
35
66
  label: model.admin.label || model.name,
36
- icon: model.admin.icon || 'database',
67
+ icon: model.admin.icon,
37
68
  count,
69
+ columnCount,
70
+ table: model.table,
71
+ lastCreated,
72
+ lastUpdated,
38
73
  });
39
74
  } catch (e) {
40
75
  stats.push({
41
76
  name: model.name,
42
77
  label: model.admin.label || model.name,
43
- icon: model.admin.icon || 'database',
78
+ icon: model.admin.icon,
44
79
  count: 0,
45
80
  error: e.message,
46
81
  });
@@ -107,27 +142,73 @@ function registerDashboardWidgets(options) {
107
142
  */
108
143
  function generateDashboardComponent() {
109
144
  return `
145
+ // Format relative time
146
+ function formatRelativeTime(date) {
147
+ if (!date) return null;
148
+ const now = new Date();
149
+ const d = new Date(date);
150
+ const diff = now - d;
151
+ const seconds = Math.floor(diff / 1000);
152
+ const minutes = Math.floor(seconds / 60);
153
+ const hours = Math.floor(minutes / 60);
154
+ const days = Math.floor(hours / 24);
155
+
156
+ if (days > 30) {
157
+ return d.toLocaleDateString();
158
+ } else if (days > 0) {
159
+ return days + ' day' + (days > 1 ? 's' : '') + ' ago';
160
+ } else if (hours > 0) {
161
+ return hours + ' hour' + (hours > 1 ? 's' : '') + ' ago';
162
+ } else if (minutes > 0) {
163
+ return minutes + ' min' + (minutes > 1 ? 's' : '') + ' ago';
164
+ } else {
165
+ return 'Just now';
166
+ }
167
+ }
168
+
110
169
  // Dashboard Widget Renderers
111
170
  const WidgetRenderers = {
112
- // Model stats (cards grid)
171
+ // Model stats (cards grid with extended info)
113
172
  'model-stats': {
114
173
  render: (data) => {
115
174
  if (!data || !Array.isArray(data)) return m('div.text-gray-500', 'No data');
116
- return m('div.grid.grid-cols-2.md:grid-cols-3.lg:grid-cols-4.gap-4', data.map(stat =>
117
- m('a.block.bg-white.rounded-lg.shadow.p-4.hover:shadow-md.transition-shadow', {
175
+ return m('div.grid.grid-cols-1.md:grid-cols-2.lg:grid-cols-3.gap-4', data.map(stat =>
176
+ m('a.block.bg-white.rounded-lg.shadow.hover:shadow-md.transition-shadow.overflow-hidden', {
118
177
  href: '/models/' + stat.name,
119
178
  onclick: (e) => {
120
179
  e.preventDefault();
121
180
  m.route.set('/models/' + stat.name);
122
181
  }
123
182
  }, [
124
- m('div.flex.items-center.justify-between', [
183
+ // Header with icon and count
184
+ m('div.p-4.flex.items-center.justify-between.border-b.border-gray-100', [
125
185
  m('div', [
126
- m('p.text-sm.text-gray-500', stat.label),
127
- m('p.text-2xl.font-bold.text-gray-900', stat.count.toLocaleString()),
186
+ m('p.text-sm.font-medium.text-gray-500', stat.label),
187
+ m('p.text-3xl.font-bold.text-gray-900', (stat.count || 0).toLocaleString()),
188
+ ]),
189
+ m('div.w-14.h-14.bg-blue-100.rounded-xl.flex.items-center.justify-center', [
190
+ stat.icon
191
+ ? m('span.text-2xl', stat.icon)
192
+ : m(Icon, { name: 'database', class: 'w-7 h-7 text-blue-600' }),
128
193
  ]),
129
- m('div.w-12.h-12.bg-blue-100.rounded-full.flex.items-center.justify-center', [
130
- m(Icon, { name: stat.icon || 'database', class: 'w-6 h-6 text-blue-600' }),
194
+ ]),
195
+ // Stats row
196
+ m('div.px-4.py-3.bg-gray-50.grid.grid-cols-3.gap-2.text-center', [
197
+ m('div', [
198
+ m('p.text-xs.text-gray-400.uppercase', 'Columns'),
199
+ m('p.text-sm.font-semibold.text-gray-700', stat.columnCount || '-'),
200
+ ]),
201
+ m('div', [
202
+ m('p.text-xs.text-gray-400.uppercase', 'Table'),
203
+ m('p.text-sm.font-semibold.text-gray-700.truncate', { title: stat.table }, stat.table || '-'),
204
+ ]),
205
+ m('div', [
206
+ m('p.text-xs.text-gray-400.uppercase', 'Last Update'),
207
+ m('p.text-sm.font-semibold.text-gray-700',
208
+ stat.lastUpdated || stat.lastCreated
209
+ ? formatRelativeTime(stat.lastUpdated || stat.lastCreated)
210
+ : '-'
211
+ ),
131
212
  ]),
132
213
  ]),
133
214
  ])