diff --git a/server.cjs b/server.cjs
index 0d11ff3..96b9e45 100644
--- a/server.cjs
+++ b/server.cjs
@@ -758,8 +758,18 @@ function viewerDashboard() {
limit 12
`).all(daySince)
+ const topPages30d = db.prepare(`
+ select page_path, page_title, count(*) as views
+ from viewer_events
+ where event_type = 'page_view'
+ and occurred_at >= ?
+ group by page_path, page_title
+ order by views desc, page_path asc
+ limit 12
+ `).all(thirtyDaysSince)
+
const clients = db.prepare(`
- select browser, os, device, count(*) as events
+ select browser, os, device, count(*) as events, count(distinct visitor_id) as visitors
from viewer_events
where occurred_at >= ?
group by browser, os, device
@@ -932,6 +942,23 @@ function viewerDashboard() {
location_labels: locationsByDate.get(row.date) || [],
}))
+ const countries24h = db.prepare(`
+ select
+ country,
+ avg(latitude) as latitude,
+ avg(longitude) as longitude,
+ count(*) as events,
+ count(distinct visitor_id) as visitors
+ from viewer_events
+ where occurred_at >= ?
+ and country != ''
+ and latitude is not null
+ and longitude is not null
+ group by country
+ order by visitors desc, events desc
+ limit 80
+ `).all(daySince)
+
const countries = db.prepare(`
select
country,
@@ -949,6 +976,26 @@ function viewerDashboard() {
limit 80
`).all(thirtyDaysSince)
+ const locations24h = db.prepare(`
+ select
+ country,
+ region,
+ city,
+ latitude,
+ longitude,
+ timezone,
+ language,
+ count(*) as events,
+ count(distinct visitor_id) as visitors
+ from viewer_events
+ where occurred_at >= ?
+ and latitude is not null
+ and longitude is not null
+ group by country, region, city, latitude, longitude, timezone, language
+ order by visitors desc, events desc
+ limit 32
+ `).all(daySince)
+
const locations = db.prepare(`
select
country,
@@ -995,11 +1042,14 @@ function viewerDashboard() {
active,
active_pages: activePages,
top_pages: topPages,
+ top_pages_30d: topPages30d,
clients,
clients_30d: clients30d,
activity_24h: activity24hWithLabels,
activity_30d: activityWithLocations,
+ countries_24h: countries24h,
countries,
+ locations_24h: locations24h,
locations,
totals: {
active_now: active.length,
diff --git a/src/App.jsx b/src/App.jsx
index cd8e8bf..f2c74a6 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -2259,19 +2259,54 @@ function AnalyticsPeriodSection({
}
function ViewersPage({ viewers }) {
+ const [analyticsWindow, setAnalyticsWindow] = useState('24h')
const data = viewers.data || {}
const active = data.active || []
const activePages = data.active_pages || []
- 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 || []
const dataTypes = data.data_types || []
const totals = data.totals || {}
const generatedAt = data.generated_at ? dateFormat.format(new Date(data.generated_at)) : 'Waiting for data'
+ const periods = {
+ '24h': {
+ label: '24hr',
+ title: 'Last 24 hours',
+ description: 'Consented analytics grouped by hour',
+ bucketLabel: 'Hourly count',
+ pointFormat: shortTimeFormat,
+ activity: activity24h,
+ topPages: data.top_pages || [],
+ clients: data.clients || [],
+ countries: data.countries_24h || [],
+ locations: data.locations_24h || [],
+ totals: {
+ events: totals.events_24h,
+ visitors: totals.visitors_24h,
+ sessions: totals.sessions_24h,
+ pageViews: totals.page_views_24h,
+ },
+ },
+ '30d': {
+ label: '30d',
+ title: 'Last 30 days',
+ description: 'Retained consented analytics over the current privacy window',
+ bucketLabel: 'Daily count',
+ pointFormat: shortDateFormat,
+ activity: activity30d,
+ topPages: data.top_pages_30d || data.top_pages || [],
+ clients: data.clients_30d || [],
+ countries: data.countries || [],
+ locations: data.locations || [],
+ totals: {
+ events: totals.events_30d,
+ visitors: totals.visitors_30d,
+ sessions: totals.sessions_30d,
+ pageViews: totals.page_views_30d,
+ },
+ },
+ }
+ const periodData = periods[analyticsWindow]
return (
Page views over the last 24 hours
+Page views over {periodData.title.toLowerCase()}
{page.page_title || page.page_path}
@@ -2429,27 +2461,28 @@ function ViewersPage({ viewers }) {{formatNumber(page.views)}
No page views recorded yet
: null} + {!periodData.topPages.length ?No page views recorded yet
: null}Browsers, devices, and operating systems seen in 24 hours
+Browsers, devices, and operating systems seen over {periodData.title.toLowerCase()}
{client.browser} on {client.os}
{client.device}
{formatNumber(client.events)}
+{formatNumber(client.visitors || 0)} visitors
+{formatNumber(client.events)} events
No client data recorded yet
: null} + {!periodData.clients.length ?No client data recorded yet
: null}- Cloudflare edge geolocation from the last 30 days + Cloudflare edge geolocation from {periodData.title.toLowerCase()}
Browsers, devices, and operating systems across retained data
-{client.browser} on {client.os}
-{client.device}
-{formatNumber(client.visitors)} visitors
-{formatNumber(client.events)} events
-No 30-day client data recorded yet
: null} -Analytics are opt-in, retained for {formatNumber(data.privacy?.retention_days || 30)} days, and public output excludes raw IP addresses and precise location.