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
|
@@ -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] = {
|
|
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
|
|
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
|
|
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-
|
|
117
|
-
m('a.block.bg-white.rounded-lg.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
|
-
|
|
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-
|
|
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
|
-
|
|
130
|
-
|
|
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
|
])
|