diff --git a/server.cjs b/server.cjs index 8485739..0d11ff3 100644 --- a/server.cjs +++ b/server.cjs @@ -776,6 +776,24 @@ function viewerDashboard() { limit 12 `).all(thirtyDaysSince) + const activity24h = db.prepare(` + select + substr(occurred_at, 1, 13) || ':00:00.000Z' as date, + count(*) as events, + count(distinct visitor_id) as visitors, + sum(case when event_type = 'page_view' then 1 else 0 end) as page_views, + count(distinct browser || '|' || os || '|' || device) as clients, + count(distinct case + when country != '' then country + when timezone != '' and timezone != 'Not shared' then timezone + else null + end) as locations + from viewer_events + where occurred_at >= ? + group by substr(occurred_at, 1, 13) + order by date asc + `).all(daySince) + const activity30d = db.prepare(` select date(occurred_at) as date, @@ -794,6 +812,26 @@ function viewerDashboard() { order by date asc `).all(thirtyDaysSince) + const activityLocationRows24h = db.prepare(` + select + substr(occurred_at, 1, 13) || ':00:00.000Z' as date, + coalesce(nullif(country, ''), '') as country, + coalesce(nullif(city, ''), '') as city, + coalesce(nullif(region, ''), '') as region, + coalesce(nullif(timezone, ''), '') as timezone, + count(distinct visitor_id) as visitors + from viewer_events + where occurred_at >= ? + and ( + country != '' + or city != '' + or region != '' + or (timezone != '' and timezone != 'Not shared') + ) + group by substr(occurred_at, 1, 13), coalesce(nullif(country, ''), ''), coalesce(nullif(city, ''), ''), coalesce(nullif(region, ''), ''), coalesce(nullif(timezone, ''), '') + order by date asc, visitors desc + `).all(daySince) + const activityLocationRows = db.prepare(` select date(occurred_at) as date, @@ -815,6 +853,18 @@ function viewerDashboard() { `).all(thirtyDaysSince) const locationsByDate = new Map() + const locations24hByDate = new Map() + + for (const row of activityLocationRows24h) { + const label = [row.city, row.region, row.country || row.timezone] + .filter(Boolean) + .join(', ') + if (!label) continue + const current = locations24hByDate.get(row.date) || [] + current.push({ label, visitors: row.visitors || 0 }) + locations24hByDate.set(row.date, current) + } + for (const row of activityLocationRows) { const label = [row.city, row.region, row.country || row.timezone] .filter(Boolean) @@ -825,6 +875,20 @@ function viewerDashboard() { locationsByDate.set(row.date, current) } + const activityClientRows24h = db.prepare(` + select + substr(occurred_at, 1, 13) || ':00:00.000Z' as date, + browser, + os, + device, + count(distinct visitor_id) as visitors, + count(*) as events + from viewer_events + where occurred_at >= ? + group by substr(occurred_at, 1, 13), browser, os, device + order by date asc, visitors desc, events desc + `).all(daySince) + const activityClientRows = db.prepare(` select date(occurred_at) as date, @@ -840,6 +904,15 @@ function viewerDashboard() { `).all(thirtyDaysSince) const clientsByDate = new Map() + const clients24hByDate = new Map() + + for (const row of activityClientRows24h) { + const label = `${row.browser} on ${row.os}${row.device ? ` (${row.device})` : ''}` + const current = clients24hByDate.get(row.date) || [] + if (current.length < 4) current.push({ label, visitors: row.visitors || 0, events: row.events || 0 }) + clients24hByDate.set(row.date, current) + } + for (const row of activityClientRows) { const label = `${row.browser} on ${row.os}${row.device ? ` (${row.device})` : ''}` const current = clientsByDate.get(row.date) || [] @@ -847,6 +920,12 @@ function viewerDashboard() { clientsByDate.set(row.date, current) } + const activity24hWithLabels = activity24h.map((row) => ({ + ...row, + client_labels: clients24hByDate.get(row.date) || [], + location_labels: locations24hByDate.get(row.date) || [], + })) + const activityWithLocations = activity30d.map((row) => ({ ...row, client_labels: clientsByDate.get(row.date) || [], @@ -894,6 +973,7 @@ function viewerDashboard() { select count(*) as events_24h, count(distinct visitor_id) as visitors_24h, + count(distinct session_id) as sessions_24h, sum(case when event_type = 'page_view' then 1 else 0 end) as page_views_24h from viewer_events where occurred_at >= ? @@ -917,6 +997,7 @@ function viewerDashboard() { top_pages: topPages, clients, clients_30d: clients30d, + activity_24h: activity24hWithLabels, activity_30d: activityWithLocations, countries, locations, @@ -924,6 +1005,7 @@ function viewerDashboard() { active_now: active.length, events_24h: totals?.events_24h || 0, visitors_24h: totals?.visitors_24h || 0, + sessions_24h: totals?.sessions_24h || 0, page_views_24h: totals?.page_views_24h || 0, events_30d: totals30d?.events_30d || 0, visitors_30d: totals30d?.visitors_30d || 0, diff --git a/src/App.jsx b/src/App.jsx index e1be0fa..b2b41a3 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -2011,6 +2011,11 @@ const shortDateFormat = new Intl.DateTimeFormat('en-GB', { month: 'short', }) +const shortTimeFormat = new Intl.DateTimeFormat('en-GB', { + hour: '2-digit', + minute: '2-digit', +}) + const countryNames = { AR: 'Argentina', AU: 'Australia', @@ -2054,7 +2059,35 @@ function filledLast30Days(rows) { }) } -function MiniLineChart({ accent = 'text-fury-cyan', data, label, metric, stroke = '#e82517' }) { +function filledLast24Hours(rows) { + const byDate = new Map(rows.map((row) => [row.date, row])) + return Array.from({ length: 24 }, (_, index) => { + const date = new Date() + date.setMinutes(0, 0, 0) + date.setHours(date.getHours() - (23 - index)) + const key = date.toISOString().slice(0, 13) + ':00:00.000Z' + return byDate.get(key) || { + date: key, + events: 0, + visitors: 0, + page_views: 0, + clients: 0, + locations: 0, + client_labels: [], + location_labels: [], + } + }) +} + +function MiniLineChart({ + accent = 'text-fury-cyan', + bucketLabel = 'Daily count', + data, + label, + metric, + pointFormat = shortDateFormat, + stroke = '#e82517', +}) { const [hoveredPoint, setHoveredPoint] = useState(null) const width = 320 const height = 112 @@ -2076,7 +2109,7 @@ function MiniLineChart({ accent = 'text-fury-cyan', data, label, metric, stroke

{label}

-

Daily count

+

{bucketLabel}

{formatNumber(latest)}

@@ -2118,7 +2151,7 @@ function MiniLineChart({ accent = 'text-fury-cyan', data, label, metric, stroke transform: 'translate(-50%, -115%)', }} > - {shortDateFormat.format(new Date(hoveredPoint.date))}: {formatNumber(hoveredPoint.value)} {label.toLowerCase()} + {pointFormat.format(new Date(hoveredPoint.date))}: {formatNumber(hoveredPoint.value)} {label.toLowerCase()} {formatNumber(hoveredPoint.visitors || 0)} visitors @@ -2144,6 +2177,87 @@ function MiniLineChart({ accent = 'text-fury-cyan', data, label, metric, stroke ) } +function AnalyticsPeriodSection({ + activity, + bucketLabel, + description, + pointFormat, + title, + totals, +}) { + return ( +
+
+
+

{title}

+

{description}

+
+
+ + {formatNumber(totals.visitors)} visitors + + + {formatNumber(totals.sessions)} sessions + + + {formatNumber(totals.pageViews)} page views + + + {formatNumber(totals.events)} events + +
+
+ +
+ + + + + +
+
+ ) +} + function ViewersPage({ viewers }) { const data = viewers.data || {} const active = data.active || [] @@ -2151,6 +2265,7 @@ function ViewersPage({ viewers }) { const topPages = data.top_pages || [] const clients = data.clients || [] const clients30d = data.clients_30d || [] + const activity24h = filledLast24Hours(data.activity_24h || []) const activity30d = filledLast30Days(data.activity_30d || []) const countries = data.countries || [] const locations = data.locations || [] @@ -2184,67 +2299,33 @@ function ViewersPage({ viewers }) { -
-
-
-

Last 30 days

-

- Retained consented analytics over the current privacy window -

-
-
- - {formatNumber(totals.visitors_30d)} visitors - - - {formatNumber(totals.sessions_30d)} sessions - - - {formatNumber(totals.page_views_30d)} page views - - - {formatNumber(totals.events_30d)} events - -
-
+ -
- - - - - -
-
+