webspresso 0.0.73 → 0.0.74
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 +4 -2
- package/bin/commands/upgrade.js +146 -0
- package/bin/webspresso.js +2 -0
- package/package.json +1 -1
- package/plugins/admin-panel/app.js +109 -0
- package/plugins/admin-panel/components.js +110 -110
- package/plugins/admin-panel/modules/dashboard.js +16 -13
- package/plugins/admin-panel/modules/user-management.js +32 -11
- package/plugins/data-exchange/export-xlsx.js +3 -0
- package/plugins/data-exchange/record-selection.js +21 -5
- package/plugins/site-analytics/admin-component.js +88 -78
- package/src/file-router.js +80 -4
- package/src/server.js +2 -0
- package/templates/skills/webspresso-usage/SKILL.md +5 -2
|
@@ -6,6 +6,18 @@
|
|
|
6
6
|
|
|
7
7
|
const { hash, verify } = require('../../../core/auth/hash');
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* DBs like SQLite store booleans as 0/1; `repo.count({ active: true })` can return 0 while rows are "active".
|
|
11
|
+
* Postgres/MySQL use real booleans — `true` is correct.
|
|
12
|
+
*/
|
|
13
|
+
function truthyBooleanForDb(db) {
|
|
14
|
+
try {
|
|
15
|
+
const client = db?.knex?.client?.config?.client;
|
|
16
|
+
if (client === 'sqlite3' || client === 'better-sqlite3') return 1;
|
|
17
|
+
} catch (_) {}
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
|
|
9
21
|
/**
|
|
10
22
|
* Register user management in admin panel
|
|
11
23
|
* @param {Object} options - Options
|
|
@@ -49,11 +61,15 @@ function registerUserManagement(options) {
|
|
|
49
61
|
order: 100,
|
|
50
62
|
});
|
|
51
63
|
|
|
64
|
+
// Sidebar targets ORM CRUD routes directly (avoids Mithril onmatch redirect races on /users/new).
|
|
65
|
+
const userModelListPath = '/models/' + encodeURIComponent(modelName);
|
|
66
|
+
const userModelNewPath = userModelListPath + '/new';
|
|
67
|
+
|
|
52
68
|
// Register menu items
|
|
53
69
|
registry.registerMenuItem({
|
|
54
70
|
id: 'user-list',
|
|
55
71
|
label: 'All Users',
|
|
56
|
-
path:
|
|
72
|
+
path: userModelListPath,
|
|
57
73
|
icon: 'users',
|
|
58
74
|
group: 'users',
|
|
59
75
|
order: 1,
|
|
@@ -62,7 +78,7 @@ function registerUserManagement(options) {
|
|
|
62
78
|
registry.registerMenuItem({
|
|
63
79
|
id: 'user-create',
|
|
64
80
|
label: 'Add User',
|
|
65
|
-
path:
|
|
81
|
+
path: userModelNewPath,
|
|
66
82
|
icon: 'user-plus',
|
|
67
83
|
group: 'users',
|
|
68
84
|
order: 2,
|
|
@@ -87,21 +103,26 @@ function registerUserManagement(options) {
|
|
|
87
103
|
dataLoader: async ({ db }) => {
|
|
88
104
|
try {
|
|
89
105
|
const repo = db.getRepository(modelName);
|
|
106
|
+
const model = repo.model;
|
|
90
107
|
const total = await repo.count();
|
|
91
|
-
const
|
|
108
|
+
const activeEq = truthyBooleanForDb(db);
|
|
109
|
+
const active = await repo.query().where(fieldMap.active, activeEq).count();
|
|
92
110
|
const admins = await repo.count({ [fieldMap.role]: 'admin' });
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
.
|
|
99
|
-
.
|
|
111
|
+
|
|
112
|
+
let recentUsers = 0;
|
|
113
|
+
const createdCol = fieldMap.createdAt;
|
|
114
|
+
if (model.columns && model.columns.has(createdCol)) {
|
|
115
|
+
const weekAgo = new Date();
|
|
116
|
+
weekAgo.setDate(weekAgo.getDate() - 7);
|
|
117
|
+
recentUsers = await repo.query()
|
|
118
|
+
.where(createdCol, '>=', weekAgo.toISOString())
|
|
119
|
+
.count();
|
|
120
|
+
}
|
|
100
121
|
|
|
101
122
|
return {
|
|
102
123
|
total,
|
|
103
124
|
active,
|
|
104
|
-
inactive: total - active,
|
|
125
|
+
inactive: Math.max(0, total - active),
|
|
105
126
|
admins,
|
|
106
127
|
recentUsers,
|
|
107
128
|
};
|
|
@@ -30,6 +30,9 @@ async function buildXlsxBuffer(model, records) {
|
|
|
30
30
|
const rowValues = columns.map((c) => {
|
|
31
31
|
let v = rec[c];
|
|
32
32
|
if (v === null || v === undefined) return '';
|
|
33
|
+
if (typeof v === 'bigint') return v.toString();
|
|
34
|
+
if (v instanceof Date) return v.toISOString();
|
|
35
|
+
if (Buffer.isBuffer(v)) return v.toString('base64');
|
|
33
36
|
if (typeof v === 'object') v = JSON.stringify(v);
|
|
34
37
|
if (typeof v === 'string' && /^[=+\-@]/.test(v)) return ` ${v}`;
|
|
35
38
|
return v;
|
|
@@ -24,6 +24,10 @@ async function resolveExportRecords(db, modelName, req) {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
const trashedOnly =
|
|
28
|
+
body.trashed === 'only' ||
|
|
29
|
+
req.query.trashed === 'only';
|
|
30
|
+
|
|
27
31
|
let idList = null;
|
|
28
32
|
if (req.body?.ids && Array.isArray(req.body.ids)) {
|
|
29
33
|
idList = req.body.ids;
|
|
@@ -45,14 +49,26 @@ async function resolveExportRecords(db, modelName, req) {
|
|
|
45
49
|
const repo = db.getRepository(model.name);
|
|
46
50
|
let records;
|
|
47
51
|
|
|
52
|
+
const filterOpts =
|
|
53
|
+
trashedOnly && model.scopes?.softDelete ? { onlyTrashed: true } : {};
|
|
54
|
+
|
|
48
55
|
if (selectAll) {
|
|
49
|
-
const query = buildFilteredQuery(repo, filters);
|
|
56
|
+
const query = buildFilteredQuery(repo, filters, filterOpts);
|
|
50
57
|
records = await query.list();
|
|
51
58
|
} else if (idList && idList.length > 0) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
if (trashedOnly && model.scopes?.softDelete) {
|
|
60
|
+
const pk = model.primaryKey || 'id';
|
|
61
|
+
records = await repo
|
|
62
|
+
.query()
|
|
63
|
+
.onlyTrashed()
|
|
64
|
+
.whereIn(pk, idList)
|
|
65
|
+
.list();
|
|
66
|
+
} else {
|
|
67
|
+
records = [];
|
|
68
|
+
for (const id of idList) {
|
|
69
|
+
const record = await repo.findById(id);
|
|
70
|
+
if (record) records.push(record);
|
|
71
|
+
}
|
|
56
72
|
}
|
|
57
73
|
} else {
|
|
58
74
|
records = await repo.findAll();
|
|
@@ -67,13 +67,13 @@ function StatCard() {
|
|
|
67
67
|
return {
|
|
68
68
|
view: function(vnode) {
|
|
69
69
|
var a = vnode.attrs;
|
|
70
|
-
return m('div.bg-white.rounded-lg.shadow.p-5.flex.items-center.gap-4', [
|
|
70
|
+
return m('div.bg-white.dark:bg-slate-800/90.rounded-lg.shadow.dark:shadow-slate-900/50.p-5.flex.items-center.gap-4.border.border-transparent.dark:border-slate-700/80', [
|
|
71
71
|
m('div.w-12.h-12.rounded-xl.flex.items-center.justify-center.text-xl', {
|
|
72
|
-
class: a.bgClass || 'bg-blue-100',
|
|
72
|
+
class: a.bgClass || 'bg-blue-100 dark:bg-blue-900/50',
|
|
73
73
|
}, a.icon),
|
|
74
74
|
m('div', [
|
|
75
|
-
m('p.text-2xl.font-bold.text-gray-900', formatNumber(a.value || 0)),
|
|
76
|
-
m('p.text-xs.text-gray-500.uppercase.tracking-wide', a.label),
|
|
75
|
+
m('p.text-2xl.font-bold.text-gray-900.dark:text-slate-100', formatNumber(a.value || 0)),
|
|
76
|
+
m('p.text-xs.text-gray-500.dark:text-slate-400.uppercase.tracking-wide', a.label),
|
|
77
77
|
]),
|
|
78
78
|
]);
|
|
79
79
|
}
|
|
@@ -86,6 +86,10 @@ function ViewsChart() {
|
|
|
86
86
|
oncreate: function(vnode) {
|
|
87
87
|
var ctx = vnode.dom.querySelector('canvas').getContext('2d');
|
|
88
88
|
var data = vnode.attrs.data || [];
|
|
89
|
+
var isDark = typeof document !== 'undefined' && document.documentElement && document.documentElement.classList.contains('dark');
|
|
90
|
+
var tickColor = isDark ? '#94a3b8' : '#64748b';
|
|
91
|
+
var gridColor = isDark ? 'rgba(148,163,184,0.2)' : 'rgba(0,0,0,0.06)';
|
|
92
|
+
var legendColor = isDark ? '#e2e8f0' : '#334155';
|
|
89
93
|
chartInstance = new Chart(ctx, {
|
|
90
94
|
type: 'line',
|
|
91
95
|
data: {
|
|
@@ -95,7 +99,7 @@ function ViewsChart() {
|
|
|
95
99
|
label: 'Views',
|
|
96
100
|
data: data.map(function(d) { return d.views; }),
|
|
97
101
|
borderColor: '#3B82F6',
|
|
98
|
-
backgroundColor: 'rgba(59,130,246,0.08)',
|
|
102
|
+
backgroundColor: isDark ? 'rgba(59,130,246,0.2)' : 'rgba(59,130,246,0.08)',
|
|
99
103
|
fill: true,
|
|
100
104
|
tension: 0.3,
|
|
101
105
|
pointRadius: data.length > 60 ? 0 : 2,
|
|
@@ -104,7 +108,7 @@ function ViewsChart() {
|
|
|
104
108
|
label: 'Visitors',
|
|
105
109
|
data: data.map(function(d) { return d.visitors; }),
|
|
106
110
|
borderColor: '#10B981',
|
|
107
|
-
backgroundColor: 'rgba(16,185,129,0.08)',
|
|
111
|
+
backgroundColor: isDark ? 'rgba(16,185,129,0.2)' : 'rgba(16,185,129,0.08)',
|
|
108
112
|
fill: true,
|
|
109
113
|
tension: 0.3,
|
|
110
114
|
pointRadius: data.length > 60 ? 0 : 2,
|
|
@@ -113,7 +117,7 @@ function ViewsChart() {
|
|
|
113
117
|
label: 'Sessions',
|
|
114
118
|
data: data.map(function(d) { return d.sessions; }),
|
|
115
119
|
borderColor: '#F59E0B',
|
|
116
|
-
backgroundColor: 'rgba(245,158,11,0.05)',
|
|
120
|
+
backgroundColor: isDark ? 'rgba(245,158,11,0.1)' : 'rgba(245,158,11,0.05)',
|
|
117
121
|
fill: false,
|
|
118
122
|
tension: 0.3,
|
|
119
123
|
borderDash: [4, 4],
|
|
@@ -128,13 +132,19 @@ function ViewsChart() {
|
|
|
128
132
|
plugins: {
|
|
129
133
|
legend: {
|
|
130
134
|
position: 'top',
|
|
131
|
-
labels: {
|
|
135
|
+
labels: {
|
|
136
|
+
usePointStyle: true,
|
|
137
|
+
boxWidth: 6,
|
|
138
|
+
padding: 16,
|
|
139
|
+
color: legendColor,
|
|
140
|
+
},
|
|
132
141
|
},
|
|
133
142
|
},
|
|
134
143
|
scales: {
|
|
135
144
|
x: {
|
|
136
145
|
grid: { display: false },
|
|
137
146
|
ticks: {
|
|
147
|
+
color: tickColor,
|
|
138
148
|
maxTicksLimit: 12,
|
|
139
149
|
callback: function(val, i) {
|
|
140
150
|
var label = this.getLabelForValue(val);
|
|
@@ -144,8 +154,8 @@ function ViewsChart() {
|
|
|
144
154
|
},
|
|
145
155
|
y: {
|
|
146
156
|
beginAtZero: true,
|
|
147
|
-
grid: { color:
|
|
148
|
-
ticks: { precision: 0 },
|
|
157
|
+
grid: { color: gridColor },
|
|
158
|
+
ticks: { color: tickColor, precision: 0 },
|
|
149
159
|
},
|
|
150
160
|
},
|
|
151
161
|
},
|
|
@@ -168,7 +178,7 @@ function HBar() {
|
|
|
168
178
|
view: function(vnode) {
|
|
169
179
|
var pct = vnode.attrs.pct || 0;
|
|
170
180
|
var color = vnode.attrs.color || 'bg-blue-500';
|
|
171
|
-
return m('div.flex-1.h-2.bg-gray-100.rounded-full.overflow-hidden', [
|
|
181
|
+
return m('div.flex-1.h-2.bg-gray-100.dark:bg-slate-600.rounded-full.overflow-hidden', [
|
|
172
182
|
m('div.h-full.rounded-full.transition-all', {
|
|
173
183
|
class: color,
|
|
174
184
|
style: 'width:' + Math.min(pct, 100) + '%',
|
|
@@ -273,11 +283,11 @@ var AnalyticsPage = {
|
|
|
273
283
|
// Header row
|
|
274
284
|
m('div.flex.items-center.justify-between.mb-6', [
|
|
275
285
|
m('div', [
|
|
276
|
-
m('h1.text-2xl.font-bold.text-gray-900.flex.items-center.gap-2', [
|
|
286
|
+
m('h1.text-2xl.font-bold.text-gray-900.dark:text-slate-100.flex.items-center.gap-2', [
|
|
277
287
|
m(Icon, { name: 'chart', class: 'w-6 h-6' }),
|
|
278
288
|
'Analytics',
|
|
279
289
|
]),
|
|
280
|
-
m('p.text-gray-500.text-sm.mt-1', 'Page view statistics and visitor analytics'),
|
|
290
|
+
m('p.text-gray-500.dark:text-slate-400.text-sm.mt-1', 'Page view statistics and visitor analytics'),
|
|
281
291
|
]),
|
|
282
292
|
m('div.flex.items-center.gap-2.flex-wrap.justify-end', [
|
|
283
293
|
typeof RefreshIconButton !== 'undefined'
|
|
@@ -288,11 +298,11 @@ var AnalyticsPage = {
|
|
|
288
298
|
})
|
|
289
299
|
: null,
|
|
290
300
|
// Day filter
|
|
291
|
-
m('div.flex.gap-1.bg-gray-100.rounded-lg.p-1', [7, 30, 90].map(function(d) {
|
|
301
|
+
m('div.flex.gap-1.bg-gray-100.dark:bg-slate-800.rounded-lg.p-1', [7, 30, 90].map(function(d) {
|
|
292
302
|
return m('button.px-3.py-1.5.text-sm.font-medium.rounded-md.transition-colors', {
|
|
293
303
|
class: s.days === d
|
|
294
|
-
? 'bg-white text-gray-900 shadow-sm'
|
|
295
|
-
: 'text-gray-500 hover:text-gray-700',
|
|
304
|
+
? 'bg-white text-gray-900 shadow-sm dark:bg-slate-700 dark:text-slate-100'
|
|
305
|
+
: 'text-gray-500 hover:text-gray-700 dark:text-slate-400 dark:hover:text-slate-200',
|
|
296
306
|
onclick: function() { self.setDays(vnode, d); },
|
|
297
307
|
}, 'Last ' + d + ' days');
|
|
298
308
|
})),
|
|
@@ -304,46 +314,46 @@ var AnalyticsPage = {
|
|
|
304
314
|
: [
|
|
305
315
|
// Stat cards
|
|
306
316
|
m('div.grid.grid-cols-2.sm:grid-cols-3.lg:grid-cols-3.xl:grid-cols-6.gap-4.mb-6', [
|
|
307
|
-
m(StatCard, { icon: '👁', label: 'Views', value: s.stats?.views, bgClass: 'bg-blue-100' }),
|
|
308
|
-
m(StatCard, { icon: '👤', label: 'Visitors', value: s.stats?.visitors, bgClass: 'bg-green-100' }),
|
|
309
|
-
m(StatCard, { icon: '📄', label: 'Unique Pages', value: s.stats?.uniquePages, bgClass: 'bg-yellow-100' }),
|
|
310
|
-
m(StatCard, { icon: '🔗', label: 'Sessions', value: s.stats?.sessions, bgClass: 'bg-purple-100' }),
|
|
311
|
-
m(StatCard, { icon: '🏠', label: 'Direct traffic', value: s.stats?.directViews, bgClass: 'bg-slate-100' }),
|
|
312
|
-
m(StatCard, { icon: '🌐', label: 'With referrer', value: s.stats?.referredViews, bgClass: 'bg-cyan-100' }),
|
|
317
|
+
m(StatCard, { icon: '👁', label: 'Views', value: s.stats?.views, bgClass: 'bg-blue-100 dark:bg-blue-900/50' }),
|
|
318
|
+
m(StatCard, { icon: '👤', label: 'Visitors', value: s.stats?.visitors, bgClass: 'bg-green-100 dark:bg-green-900/40' }),
|
|
319
|
+
m(StatCard, { icon: '📄', label: 'Unique Pages', value: s.stats?.uniquePages, bgClass: 'bg-yellow-100 dark:bg-yellow-900/35' }),
|
|
320
|
+
m(StatCard, { icon: '🔗', label: 'Sessions', value: s.stats?.sessions, bgClass: 'bg-purple-100 dark:bg-purple-900/40' }),
|
|
321
|
+
m(StatCard, { icon: '🏠', label: 'Direct traffic', value: s.stats?.directViews, bgClass: 'bg-slate-100 dark:bg-slate-700' }),
|
|
322
|
+
m(StatCard, { icon: '🌐', label: 'With referrer', value: s.stats?.referredViews, bgClass: 'bg-cyan-100 dark:bg-cyan-900/40' }),
|
|
313
323
|
]),
|
|
314
324
|
|
|
315
325
|
// Chart + Bot Activity row
|
|
316
326
|
m('div.grid.grid-cols-1.lg:grid-cols-3.gap-4.mb-6', [
|
|
317
327
|
// Views over time chart
|
|
318
|
-
m('div.lg:col-span-2.bg-white.rounded-lg.shadow', [
|
|
319
|
-
m('div.px-5.py-4.border-b.border-gray-100.flex.items-center.justify-between', [
|
|
320
|
-
m('h3.text-sm.font-semibold.text-gray-900.flex.items-center.gap-2', [
|
|
321
|
-
m(Icon, { name: 'chart', class: 'w-4 h-4 text-gray-400' }),
|
|
328
|
+
m('div.lg:col-span-2.bg-white.dark:bg-slate-800/90.rounded-lg.shadow.dark:shadow-slate-900/50.border.border-transparent.dark:border-slate-700/80', [
|
|
329
|
+
m('div.px-5.py-4.border-b.border-gray-100.dark:border-slate-700.flex.items-center.justify-between', [
|
|
330
|
+
m('h3.text-sm.font-semibold.text-gray-900.dark:text-slate-100.flex.items-center.gap-2', [
|
|
331
|
+
m(Icon, { name: 'chart', class: 'w-4 h-4 text-gray-400 dark:text-slate-500' }),
|
|
322
332
|
'Views Over Time',
|
|
323
333
|
]),
|
|
324
334
|
]),
|
|
325
335
|
m('div.p-5', [
|
|
326
336
|
s.chartLoaded && s.viewsOverTime.length > 0
|
|
327
337
|
? m(ViewsChart, { key: 'chart-' + s.days + '-' + (s.chartDataVersion || 0), data: s.viewsOverTime })
|
|
328
|
-
: m('div.flex.justify-center.py-16.text-gray-400.text-sm', 'Loading chart...'),
|
|
338
|
+
: m('div.flex.justify-center.py-16.text-gray-400.dark:text-slate-500.text-sm', 'Loading chart...'),
|
|
329
339
|
]),
|
|
330
340
|
]),
|
|
331
341
|
|
|
332
342
|
// Bot Activity
|
|
333
|
-
m('div.bg-white.rounded-lg.shadow', [
|
|
334
|
-
m('div.px-5.py-4.border-b.border-gray-100', [
|
|
335
|
-
m('h3.text-sm.font-semibold.text-gray-900', 'Bot Activity'),
|
|
343
|
+
m('div.bg-white.dark:bg-slate-800/90.rounded-lg.shadow.dark:shadow-slate-900/50.border.border-transparent.dark:border-slate-700/80', [
|
|
344
|
+
m('div.px-5.py-4.border-b.border-gray-100.dark:border-slate-700', [
|
|
345
|
+
m('h3.text-sm.font-semibold.text-gray-900.dark:text-slate-100', 'Bot Activity'),
|
|
336
346
|
]),
|
|
337
347
|
m('div.p-4', [
|
|
338
348
|
s.botActivity.length === 0
|
|
339
|
-
? m('p.text-gray-400.text-sm.text-center.py-4', 'No bot activity')
|
|
349
|
+
? m('p.text-gray-400.dark:text-slate-500.text-sm.text-center.py-4', 'No bot activity')
|
|
340
350
|
: (function() {
|
|
341
351
|
var maxReqs = Math.max.apply(null, s.botActivity.map(function(b) { return b.requests; }));
|
|
342
352
|
return m('div.space-y-2.5', s.botActivity.slice(0, 12).map(function(bot) {
|
|
343
353
|
return m('div.flex.items-center.gap-3', [
|
|
344
|
-
m('span.text-xs.font-medium.text-gray-700.w-24.truncate', { title: bot.name }, bot.name),
|
|
354
|
+
m('span.text-xs.font-medium.text-gray-700.dark:text-slate-300.w-24.truncate', { title: bot.name }, bot.name),
|
|
345
355
|
m(HBar, { pct: (bot.requests / maxReqs) * 100, color: 'bg-indigo-500' }),
|
|
346
|
-
m('span.text-xs.text-gray-500.w-12.text-right.tabular-nums', formatNumber(bot.requests)),
|
|
356
|
+
m('span.text-xs.text-gray-500.dark:text-slate-400.w-12.text-right.tabular-nums', formatNumber(bot.requests)),
|
|
347
357
|
]);
|
|
348
358
|
}));
|
|
349
359
|
})(),
|
|
@@ -354,28 +364,28 @@ var AnalyticsPage = {
|
|
|
354
364
|
// Top Pages + Recent Activity row
|
|
355
365
|
m('div.grid.grid-cols-1.lg:grid-cols-2.gap-4.mb-6', [
|
|
356
366
|
// Top Pages
|
|
357
|
-
m('div.bg-white.rounded-lg.shadow.flex.flex-col', { style: 'max-height:480px' }, [
|
|
358
|
-
m('div.px-5.py-4.border-b.border-gray-100.shrink-0', [
|
|
359
|
-
m('h3.text-sm.font-semibold.text-gray-900', 'Top Pages'),
|
|
367
|
+
m('div.bg-white.dark:bg-slate-800/90.rounded-lg.shadow.dark:shadow-slate-900/50.border.border-transparent.dark:border-slate-700/80.flex.flex-col', { style: 'max-height:480px' }, [
|
|
368
|
+
m('div.px-5.py-4.border-b.border-gray-100.dark:border-slate-700.shrink-0', [
|
|
369
|
+
m('h3.text-sm.font-semibold.text-gray-900.dark:text-slate-100', 'Top Pages'),
|
|
360
370
|
]),
|
|
361
|
-
m('div.divide-y.divide-gray-50.overflow-y-auto.flex-1', [
|
|
371
|
+
m('div.divide-y.divide-gray-50.dark:divide-slate-700/80.overflow-y-auto.flex-1', [
|
|
362
372
|
s.topPages.length === 0
|
|
363
|
-
? m('p.text-gray-400.text-sm.text-center.py-6', 'No page views yet')
|
|
373
|
+
? m('p.text-gray-400.dark:text-slate-500.text-sm.text-center.py-6', 'No page views yet')
|
|
364
374
|
: (function() {
|
|
365
375
|
var maxViews = s.topPages[0]?.views || 1;
|
|
366
376
|
return s.topPages.slice(0, 15).map(function(page, i) {
|
|
367
|
-
return m('div.flex.items-center.gap-3.px-5.py-2.5.hover:bg-gray-50', [
|
|
368
|
-
m('span.text-xs.text-gray-400.w-5.text-right', i + 1),
|
|
377
|
+
return m('div.flex.items-center.gap-3.px-5.py-2.5.hover:bg-gray-50.dark:hover:bg-slate-700/50', [
|
|
378
|
+
m('span.text-xs.text-gray-400.dark:text-slate-500.w-5.text-right', i + 1),
|
|
369
379
|
m('div.flex-1.min-w-0', [
|
|
370
|
-
m('p.text-sm.text-gray-800.truncate', { title: page.path }, page.path),
|
|
380
|
+
m('p.text-sm.text-gray-800.dark:text-slate-200.truncate', { title: page.path }, page.path),
|
|
371
381
|
m('div.mt-1', m(HBar, {
|
|
372
382
|
pct: (page.views / maxViews) * 100,
|
|
373
383
|
color: 'bg-blue-400',
|
|
374
384
|
})),
|
|
375
385
|
]),
|
|
376
386
|
m('div.text-right.shrink-0', [
|
|
377
|
-
m('span.text-sm.font-semibold.text-gray-900', formatNumber(page.views)),
|
|
378
|
-
m('span.text-xs.text-gray-400.ml-1', 'views'),
|
|
387
|
+
m('span.text-sm.font-semibold.text-gray-900.dark:text-slate-100', formatNumber(page.views)),
|
|
388
|
+
m('span.text-xs.text-gray-400.dark:text-slate-500.ml-1', 'views'),
|
|
379
389
|
]),
|
|
380
390
|
]);
|
|
381
391
|
});
|
|
@@ -384,24 +394,24 @@ var AnalyticsPage = {
|
|
|
384
394
|
]),
|
|
385
395
|
|
|
386
396
|
// Recent Activity
|
|
387
|
-
m('div.bg-white.rounded-lg.shadow.flex.flex-col', { style: 'max-height:480px' }, [
|
|
388
|
-
m('div.px-5.py-4.border-b.border-gray-100.flex.items-center.justify-between.shrink-0', [
|
|
389
|
-
m('h3.text-sm.font-semibold.text-gray-900', 'Recent Activity'),
|
|
390
|
-
m('span.text-xs.text-gray-400', 'Live'),
|
|
397
|
+
m('div.bg-white.dark:bg-slate-800/90.rounded-lg.shadow.dark:shadow-slate-900/50.border.border-transparent.dark:border-slate-700/80.flex.flex-col', { style: 'max-height:480px' }, [
|
|
398
|
+
m('div.px-5.py-4.border-b.border-gray-100.dark:border-slate-700.flex.items-center.justify-between.shrink-0', [
|
|
399
|
+
m('h3.text-sm.font-semibold.text-gray-900.dark:text-slate-100', 'Recent Activity'),
|
|
400
|
+
m('span.text-xs.text-gray-400.dark:text-slate-500', 'Live'),
|
|
391
401
|
]),
|
|
392
|
-
m('div.divide-y.divide-gray-50.overflow-y-auto.flex-1', [
|
|
402
|
+
m('div.divide-y.divide-gray-50.dark:divide-slate-700/80.overflow-y-auto.flex-1', [
|
|
393
403
|
s.recent.length === 0
|
|
394
|
-
? m('p.text-gray-400.text-sm.text-center.py-6', 'No recent activity')
|
|
404
|
+
? m('p.text-gray-400.dark:text-slate-500.text-sm.text-center.py-6', 'No recent activity')
|
|
395
405
|
: s.recent.slice(0, 10).map(function(item) {
|
|
396
|
-
return m('div.flex.items-center.gap-3.px-5.py-2.5.hover:bg-gray-50', [
|
|
397
|
-
m('div.w-7.h-7.rounded-full.bg-blue-50.flex.items-center.justify-center.shrink-0', [
|
|
406
|
+
return m('div.flex.items-center.gap-3.px-5.py-2.5.hover:bg-gray-50.dark:hover:bg-slate-700/50', [
|
|
407
|
+
m('div.w-7.h-7.rounded-full.bg-blue-50.dark:bg-slate-700.flex.items-center.justify-center.shrink-0', [
|
|
398
408
|
m('span.text-xs', item.country ? (COUNTRY_FLAGS[item.country] || '🌐') : '🌐'),
|
|
399
409
|
]),
|
|
400
410
|
m('div.flex-1.min-w-0', [
|
|
401
|
-
m('p.text-sm.text-gray-800.truncate', { title: item.path }, item.path),
|
|
402
|
-
m('p.text-xs.text-gray-400', relativeTime(item.created_at)),
|
|
411
|
+
m('p.text-sm.text-gray-800.dark:text-slate-200.truncate', { title: item.path }, item.path),
|
|
412
|
+
m('p.text-xs.text-gray-400.dark:text-slate-500', relativeTime(item.created_at)),
|
|
403
413
|
]),
|
|
404
|
-
item.referrer && m('span.text-xs.text-gray-400.truncate.max-w-[120px]', {
|
|
414
|
+
item.referrer && m('span.text-xs.text-gray-400.dark:text-slate-500.truncate.max-w-[120px]', {
|
|
405
415
|
title: item.referrer,
|
|
406
416
|
}, (function() {
|
|
407
417
|
try { return new URL(item.referrer).hostname; } catch(e) { return ''; }
|
|
@@ -413,51 +423,51 @@ var AnalyticsPage = {
|
|
|
413
423
|
]),
|
|
414
424
|
|
|
415
425
|
// Client Errors
|
|
416
|
-
m('div.bg-white.rounded-lg.shadow.mb-6', [
|
|
417
|
-
m('div.px-5.py-4.border-b.border-gray-100.flex.items-center.justify-between', [
|
|
418
|
-
m('h3.text-sm.font-semibold.text-gray-900.flex.items-center.gap-2', [
|
|
426
|
+
m('div.bg-white.dark:bg-slate-800/90.rounded-lg.shadow.dark:shadow-slate-900/50.border.border-transparent.dark:border-slate-700/80.mb-6', [
|
|
427
|
+
m('div.px-5.py-4.border-b.border-gray-100.dark:border-slate-700.flex.items-center.justify-between', [
|
|
428
|
+
m('h3.text-sm.font-semibold.text-gray-900.dark:text-slate-100.flex.items-center.gap-2', [
|
|
419
429
|
m('span', '⚠️'),
|
|
420
430
|
'Client Errors',
|
|
421
431
|
]),
|
|
422
|
-
m('span.text-xs.text-gray-400', 'Last ' + s.days + ' days'),
|
|
432
|
+
m('span.text-xs.text-gray-400.dark:text-slate-500', 'Last ' + s.days + ' days'),
|
|
423
433
|
]),
|
|
424
434
|
m('div.p-4.max-h-64.overflow-y-auto', [
|
|
425
435
|
!s.clientErrors || s.clientErrors.length === 0
|
|
426
|
-
? m('p.text-gray-400.text-sm.text-center.py-6', 'No client errors')
|
|
436
|
+
? m('p.text-gray-400.dark:text-slate-500.text-sm.text-center.py-6', 'No client errors')
|
|
427
437
|
: s.clientErrors.slice(0, 15).map(function(err) {
|
|
428
|
-
return m('div.border-b.border-gray-50.pb-3.mb-3.last:border-0.last:mb-0.last:pb-0', [
|
|
438
|
+
return m('div.border-b.border-gray-50.dark:border-slate-700/60.pb-3.mb-3.last:border-0.last:mb-0.last:pb-0', [
|
|
429
439
|
m('div.flex.items-start.gap-2', [
|
|
430
|
-
m('span.text-xs.px-1.5.py-0.5.rounded.bg-red-100.text-red-700', err.error_type || 'error'),
|
|
431
|
-
m('span.text-xs.text-gray-500', relativeTime(err.created_at)),
|
|
440
|
+
m('span.text-xs.px-1.5.py-0.5.rounded.bg-red-100.text-red-700.dark:bg-red-950/60.dark:text-red-300', err.error_type || 'error'),
|
|
441
|
+
m('span.text-xs.text-gray-500.dark:text-slate-500', relativeTime(err.created_at)),
|
|
432
442
|
]),
|
|
433
|
-
m('p.text-sm.text-gray-800.font-mono.break-all.mt-1', {
|
|
443
|
+
m('p.text-sm.text-gray-800.dark:text-slate-200.font-mono.break-all.mt-1', {
|
|
434
444
|
title: err.stack || err.message,
|
|
435
445
|
}, (err.message || '').slice(0, 120) + (err.message && err.message.length > 120 ? '…' : '')),
|
|
436
|
-
err.path && m('p.text-xs.text-gray-500.mt-0.5', err.path),
|
|
446
|
+
err.path && m('p.text-xs.text-gray-500.dark:text-slate-500.mt-0.5', err.path),
|
|
437
447
|
]);
|
|
438
448
|
}),
|
|
439
449
|
]),
|
|
440
450
|
]),
|
|
441
451
|
|
|
442
452
|
// Referrer sources (hostname-level)
|
|
443
|
-
m('div.bg-white.rounded-lg.shadow.mb-6', [
|
|
444
|
-
m('div.px-5.py-4.border-b.border-gray-100.flex.items-center.justify-between', [
|
|
445
|
-
m('h3.text-sm.font-semibold.text-gray-900', 'Referrer sources'),
|
|
446
|
-
m('span.text-xs.text-gray-400', 'By hostname · last ' + s.days + ' days'),
|
|
453
|
+
m('div.bg-white.dark:bg-slate-800/90.rounded-lg.shadow.dark:shadow-slate-900/50.border.border-transparent.dark:border-slate-700/80.mb-6', [
|
|
454
|
+
m('div.px-5.py-4.border-b.border-gray-100.dark:border-slate-700.flex.items-center.justify-between', [
|
|
455
|
+
m('h3.text-sm.font-semibold.text-gray-900.dark:text-slate-100', 'Referrer sources'),
|
|
456
|
+
m('span.text-xs.text-gray-400.dark:text-slate-500', 'By hostname · last ' + s.days + ' days'),
|
|
447
457
|
]),
|
|
448
458
|
m('div.p-4', [
|
|
449
459
|
!s.referrerSources || s.referrerSources.length === 0
|
|
450
|
-
? m('p.text-gray-400.text-sm.text-center.py-4', 'No external referrer data (direct visits or missing Referer header)')
|
|
460
|
+
? m('p.text-gray-400.dark:text-slate-500.text-sm.text-center.py-4', 'No external referrer data (direct visits or missing Referer header)')
|
|
451
461
|
: (function() {
|
|
452
462
|
var maxV = s.referrerSources[0]?.views || 1;
|
|
453
463
|
return m('div.grid.grid-cols-1.sm:grid-cols-2.gap-x-8.gap-y-2', s.referrerSources.map(function(r) {
|
|
454
464
|
return m('div.flex.items-center.gap-3.py-1', [
|
|
455
|
-
m('span.text-sm.text-gray-700.flex-1.min-w-0.truncate', { title: r.source }, r.source),
|
|
465
|
+
m('span.text-sm.text-gray-700.dark:text-slate-300.flex-1.min-w-0.truncate', { title: r.source }, r.source),
|
|
456
466
|
m('div.w-32.shrink-0', m(HBar, {
|
|
457
467
|
pct: (r.views / maxV) * 100,
|
|
458
468
|
color: 'bg-sky-500',
|
|
459
469
|
})),
|
|
460
|
-
m('span.text-xs.text-gray-500.w-12.text-right.tabular-nums', formatNumber(r.views)),
|
|
470
|
+
m('span.text-xs.text-gray-500.dark:text-slate-400.w-12.text-right.tabular-nums', formatNumber(r.views)),
|
|
461
471
|
]);
|
|
462
472
|
}));
|
|
463
473
|
})(),
|
|
@@ -465,25 +475,25 @@ var AnalyticsPage = {
|
|
|
465
475
|
]),
|
|
466
476
|
|
|
467
477
|
// Country Stats
|
|
468
|
-
m('div.bg-white.rounded-lg.shadow.mb-6', [
|
|
469
|
-
m('div.px-5.py-4.border-b.border-gray-100.flex.items-center.justify-between', [
|
|
470
|
-
m('h3.text-sm.font-semibold.text-gray-900', 'Country Stats'),
|
|
471
|
-
m('span.text-xs.text-gray-400', 'Last ' + s.days + ' days'),
|
|
478
|
+
m('div.bg-white.dark:bg-slate-800/90.rounded-lg.shadow.dark:shadow-slate-900/50.border.border-transparent.dark:border-slate-700/80.mb-6', [
|
|
479
|
+
m('div.px-5.py-4.border-b.border-gray-100.dark:border-slate-700.flex.items-center.justify-between', [
|
|
480
|
+
m('h3.text-sm.font-semibold.text-gray-900.dark:text-slate-100', 'Country Stats'),
|
|
481
|
+
m('span.text-xs.text-gray-400.dark:text-slate-500', 'Last ' + s.days + ' days'),
|
|
472
482
|
]),
|
|
473
483
|
m('div.p-4', [
|
|
474
484
|
s.countries.length === 0
|
|
475
|
-
? m('p.text-gray-400.text-sm.text-center.py-4', 'No country data')
|
|
485
|
+
? m('p.text-gray-400.dark:text-slate-500.text-sm.text-center.py-4', 'No country data')
|
|
476
486
|
: (function() {
|
|
477
487
|
var maxViews = s.countries[0]?.views || 1;
|
|
478
488
|
return m('div.grid.grid-cols-1.sm:grid-cols-2.gap-x-8.gap-y-2', s.countries.map(function(c) {
|
|
479
489
|
return m('div.flex.items-center.gap-3.py-1', [
|
|
480
490
|
m('span.text-base.w-6.text-center', COUNTRY_FLAGS[c.country] || '🏳️'),
|
|
481
|
-
m('span.text-sm.text-gray-700.w-6.font-medium', c.country),
|
|
491
|
+
m('span.text-sm.text-gray-700.dark:text-slate-300.w-6.font-medium', c.country),
|
|
482
492
|
m('div.flex-1', m(HBar, {
|
|
483
493
|
pct: (c.views / maxViews) * 100,
|
|
484
494
|
color: 'bg-emerald-500',
|
|
485
495
|
})),
|
|
486
|
-
m('span.text-xs.text-gray-500.w-12.text-right.tabular-nums', formatNumber(c.views)),
|
|
496
|
+
m('span.text-xs.text-gray-500.dark:text-slate-400.w-12.text-right.tabular-nums', formatNumber(c.views)),
|
|
487
497
|
]);
|
|
488
498
|
}));
|
|
489
499
|
})(),
|
package/src/file-router.js
CHANGED
|
@@ -140,6 +140,69 @@ function extractMethodFromFilename(filename) {
|
|
|
140
140
|
return result;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Whether `load()` return values for `stylesheets` and `scripts` are promoted to
|
|
145
|
+
* `pageHead` in Nunjucks (see `createApp({ pageAssets })`).
|
|
146
|
+
* @param {boolean|{enabled?: boolean, stylesheets?: boolean, scripts?: boolean}|null|undefined} raw
|
|
147
|
+
* @returns {{ enabled: boolean, stylesheets: boolean, scripts: boolean }}
|
|
148
|
+
*/
|
|
149
|
+
function resolvePageAssets(raw) {
|
|
150
|
+
if (raw === true) {
|
|
151
|
+
return { enabled: true, stylesheets: true, scripts: true };
|
|
152
|
+
}
|
|
153
|
+
if (raw == null || raw === false) {
|
|
154
|
+
return { enabled: false, stylesheets: false, scripts: false };
|
|
155
|
+
}
|
|
156
|
+
if (typeof raw === 'object') {
|
|
157
|
+
const on = raw.enabled !== false;
|
|
158
|
+
if (!on) {
|
|
159
|
+
return { enabled: false, stylesheets: false, scripts: false };
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
enabled: true,
|
|
163
|
+
stylesheets: raw.stylesheets !== false,
|
|
164
|
+
scripts: raw.scripts !== false,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return { enabled: false, stylesheets: false, scripts: false };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @param {unknown} v
|
|
172
|
+
* @returns {unknown[]}
|
|
173
|
+
*/
|
|
174
|
+
function toList(v) {
|
|
175
|
+
if (v == null) return [];
|
|
176
|
+
return Array.isArray(v) ? v : [v];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @param {{ enabled: boolean, stylesheets: boolean, scripts: boolean }} cfg
|
|
181
|
+
* @param {Object} data
|
|
182
|
+
* @returns {{ data: Object, pageHead: { stylesheets: unknown[], scripts: unknown[] }|null, pageAssets: boolean }}
|
|
183
|
+
*/
|
|
184
|
+
function applyPageAssetsToTemplateData(cfg, data) {
|
|
185
|
+
if (!cfg || !cfg.enabled) {
|
|
186
|
+
return { data, pageHead: null, pageAssets: false };
|
|
187
|
+
}
|
|
188
|
+
const out = { ...data };
|
|
189
|
+
let styles = [];
|
|
190
|
+
let scriptItems = [];
|
|
191
|
+
if (cfg.stylesheets && Object.prototype.hasOwnProperty.call(out, 'stylesheets')) {
|
|
192
|
+
styles = toList(out.stylesheets);
|
|
193
|
+
delete out.stylesheets;
|
|
194
|
+
}
|
|
195
|
+
if (cfg.scripts && Object.prototype.hasOwnProperty.call(out, 'scripts')) {
|
|
196
|
+
scriptItems = toList(out.scripts);
|
|
197
|
+
delete out.scripts;
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
data: out,
|
|
201
|
+
pageHead: { stylesheets: styles, scripts: scriptItems },
|
|
202
|
+
pageAssets: true,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
143
206
|
/**
|
|
144
207
|
* Recursively scan a directory for files
|
|
145
208
|
* @param {string} dir - Directory to scan
|
|
@@ -439,6 +502,7 @@ function resolveMiddlewares(middlewareConfig, middlewareRegistry = {}) {
|
|
|
439
502
|
* @param {boolean} options.silent - Suppress console output
|
|
440
503
|
* @param {Object} options.db - Database instance (exposed as ctx.db in load/meta)
|
|
441
504
|
* @param {{ alpine?: boolean, swup?: boolean }} [options.clientRuntime] - Passed to Nunjucks as `clientRuntime` (default both false)
|
|
505
|
+
* @param {boolean|{enabled?: boolean, stylesheets?: boolean, scripts?: boolean}} [options.pageAssets] - If set, `load()` may return `stylesheets` / `scripts` promoted to `pageHead` in templates
|
|
442
506
|
* @returns {Array} Route metadata for plugins
|
|
443
507
|
*/
|
|
444
508
|
function mountPages(app, options) {
|
|
@@ -450,7 +514,9 @@ function mountPages(app, options) {
|
|
|
450
514
|
silent = false,
|
|
451
515
|
db = null,
|
|
452
516
|
clientRuntime: clientRuntimeOpt = null,
|
|
517
|
+
pageAssets: pageAssetsOpt = null,
|
|
453
518
|
} = options;
|
|
519
|
+
const pageAssetsResolved = resolvePageAssets(pageAssetsOpt);
|
|
454
520
|
const clientRuntime = clientRuntimeOpt && typeof clientRuntimeOpt === 'object'
|
|
455
521
|
? { alpine: !!clientRuntimeOpt.alpine, swup: !!clientRuntimeOpt.swup }
|
|
456
522
|
: { alpine: false, swup: false };
|
|
@@ -686,9 +752,9 @@ function mountPages(app, options) {
|
|
|
686
752
|
await executeHook(globalHooks, 'beforeRender', ctx);
|
|
687
753
|
await executeHook(routeHooks, 'beforeRender', ctx);
|
|
688
754
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
const
|
|
755
|
+
const pageAssetBundle = applyPageAssetsToTemplateData(pageAssetsResolved, ctx.data);
|
|
756
|
+
ctx.data = pageAssetBundle.data;
|
|
757
|
+
const renderContext = {
|
|
692
758
|
...ctx.data,
|
|
693
759
|
meta: ctx.meta,
|
|
694
760
|
locale: ctx.locale,
|
|
@@ -700,7 +766,15 @@ function mountPages(app, options) {
|
|
|
700
766
|
query: req.query,
|
|
701
767
|
params: req.params
|
|
702
768
|
}
|
|
703
|
-
}
|
|
769
|
+
};
|
|
770
|
+
if (pageAssetBundle.pageAssets) {
|
|
771
|
+
renderContext.pageAssets = true;
|
|
772
|
+
renderContext.pageHead = pageAssetBundle.pageHead;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Render the template
|
|
776
|
+
const templatePath = route.file.split(path.sep).join('/');
|
|
777
|
+
const html = nunjucks.render(templatePath, renderContext);
|
|
704
778
|
|
|
705
779
|
// Execute hooks: afterRender
|
|
706
780
|
ctx.html = html;
|
|
@@ -769,5 +843,7 @@ module.exports = {
|
|
|
769
843
|
resolveMiddlewares,
|
|
770
844
|
routeRegistrationMeta,
|
|
771
845
|
compareRouteRegistrationOrder,
|
|
846
|
+
resolvePageAssets,
|
|
847
|
+
applyPageAssetsToTemplateData,
|
|
772
848
|
};
|
|
773
849
|
|
package/src/server.js
CHANGED
|
@@ -261,6 +261,7 @@ function haltOnTimedout(req, res, next) {
|
|
|
261
261
|
* @param {Object} options.auth - Authentication manager instance (from createAuth)
|
|
262
262
|
* @param {Object} options.db - Database instance (exposed as ctx.db to plugins)
|
|
263
263
|
* @param {Object} [options.clientRuntime] - Optional client assets: `{ alpine?: boolean|object, swup?: boolean|object }`. Overridable by env `WEBSPRESSO_ALPINE` / `WEBSPRESSO_SWUP` (=1 or true). Serves `/__webspresso/client-runtime/*` when either flag is on.
|
|
264
|
+
* @param {boolean|{enabled?: boolean, stylesheets?: boolean, scripts?: boolean}} [options.pageAssets] - If truthy, route `load()` return values for `stylesheets` and `scripts` are reserved: removed from the root template context and passed as `pageHead` to Nunjucks, with `pageAssets: true` (for layout to emit `<link>` / `<script>`). Default off.
|
|
264
265
|
* @param {function(import('express').Express, Object): void} [options.setupRoutes] - Called after file routes and plugins, before 404 handler
|
|
265
266
|
* @returns {Object} { app, nunjucksEnv, pluginManager, authMiddleware }
|
|
266
267
|
*/
|
|
@@ -448,6 +449,7 @@ function createApp(options = {}) {
|
|
|
448
449
|
silent: isTest,
|
|
449
450
|
db: options.db ?? null,
|
|
450
451
|
clientRuntime,
|
|
452
|
+
pageAssets: options.pageAssets,
|
|
451
453
|
});
|
|
452
454
|
|
|
453
455
|
// Set route metadata in plugin manager
|