webspresso 0.0.72 โ 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 +111 -111
- 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
|
@@ -273,23 +273,26 @@ const WidgetRenderers = {
|
|
|
273
273
|
// User stats (for user-management module)
|
|
274
274
|
'user-stats': {
|
|
275
275
|
render: (data) => {
|
|
276
|
-
if (!data) return m('div.text-gray-500', 'No data');
|
|
276
|
+
if (!data) return m('div.text-gray-500.dark:text-slate-400', 'No data');
|
|
277
|
+
if (data.error) {
|
|
278
|
+
return m('div.text-sm.text-red-600.dark:text-red-400', data.error);
|
|
279
|
+
}
|
|
277
280
|
return m('div.grid.grid-cols-2.gap-4', [
|
|
278
|
-
m('div.text-center.p-3.bg-blue-50.rounded', [
|
|
279
|
-
m('p.text-2xl.font-bold.text-blue-600', data.total),
|
|
280
|
-
m('p.text-xs.text-gray-500', 'Total Users'),
|
|
281
|
+
m('div.text-center.p-3.bg-blue-50.dark:bg-blue-950/40.rounded-lg', [
|
|
282
|
+
m('p.text-2xl.font-bold.text-blue-600.dark:text-blue-400', data.total),
|
|
283
|
+
m('p.text-xs.text-gray-500.dark:text-slate-400', 'Total Users'),
|
|
281
284
|
]),
|
|
282
|
-
m('div.text-center.p-3.bg-green-50.rounded', [
|
|
283
|
-
m('p.text-2xl.font-bold.text-green-600', data.active),
|
|
284
|
-
m('p.text-xs.text-gray-500', 'Active'),
|
|
285
|
+
m('div.text-center.p-3.bg-green-50.dark:bg-green-950/40.rounded-lg', [
|
|
286
|
+
m('p.text-2xl.font-bold.text-green-600.dark:text-green-400', data.active),
|
|
287
|
+
m('p.text-xs.text-gray-500.dark:text-slate-400', 'Active'),
|
|
285
288
|
]),
|
|
286
|
-
m('div.text-center.p-3.bg-yellow-50.rounded', [
|
|
287
|
-
m('p.text-2xl.font-bold.text-yellow-600', data.admins),
|
|
288
|
-
m('p.text-xs.text-gray-500', 'Admins'),
|
|
289
|
+
m('div.text-center.p-3.bg-yellow-50.dark:bg-amber-950/40.rounded-lg', [
|
|
290
|
+
m('p.text-2xl.font-bold.text-yellow-600.dark:text-amber-400', data.admins),
|
|
291
|
+
m('p.text-xs.text-gray-500.dark:text-slate-400', 'Admins'),
|
|
289
292
|
]),
|
|
290
|
-
m('div.text-center.p-3.bg-purple-50.rounded', [
|
|
291
|
-
m('p.text-2xl.font-bold.text-purple-600', data.recentUsers),
|
|
292
|
-
m('p.text-xs.text-gray-500', 'This Week'),
|
|
293
|
+
m('div.text-center.p-3.bg-purple-50.dark:bg-purple-950/40.rounded-lg', [
|
|
294
|
+
m('p.text-2xl.font-bold.text-purple-600.dark:text-purple-400', data.recentUsers),
|
|
295
|
+
m('p.text-xs.text-gray-500.dark:text-slate-400', 'This Week'),
|
|
293
296
|
]),
|
|
294
297
|
]);
|
|
295
298
|
},
|
|
@@ -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
|
})(),
|