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 (
@@ -2286,45 +2321,42 @@ function ViewersPage({ viewers }) { Live consented browser sessions and page activity. Last refreshed {generatedAt}.

- - {viewers.status === 'loading' ? 'Loading' : `${formatNumber(totals.active_now)} active now`} - +
+
+ {Object.entries(periods).map(([key, period]) => ( + + ))} +
+ + {viewers.status === 'loading' ? 'Loading' : `${formatNumber(totals.active_now)} active now`} + +
- - - + + +
- -
@@ -2418,9 +2450,9 @@ function ViewersPage({ viewers }) {

Top pages

-

Page views over the last 24 hours

+

Page views over {periodData.title.toLowerCase()}

- {topPages.map((page) => ( + {periodData.topPages.map((page) => (

{page.page_title || page.page_path}

@@ -2429,27 +2461,28 @@ function ViewersPage({ viewers }) {

{formatNumber(page.views)}

))} - {!topPages.length ?

No page views recorded yet

: null} + {!periodData.topPages.length ?

No page views recorded yet

: null}

Clients

-

Browsers, devices, and operating systems seen in 24 hours

+

Browsers, devices, and operating systems seen over {periodData.title.toLowerCase()}

- {clients.map((client) => ( + {periodData.clients.map((client) => (

{client.browser} on {client.os}

{client.device}

-

{formatNumber(client.events)}

+

{formatNumber(client.visitors || 0)} visitors

+

{formatNumber(client.events)} events

))} - {!clients.length ?

No client data recorded yet

: null} + {!periodData.clients.length ?

No client data recorded yet

: null}
@@ -2474,34 +2507,13 @@ function ViewersPage({ viewers }) {

Location signals

- Cloudflare edge geolocation from the last 30 days + Cloudflare edge geolocation from {periodData.title.toLowerCase()}

- +
-
-
-

Clients over 30 days

-

Browsers, devices, and operating systems across retained data

-
- {clients30d.map((client) => ( -
-
-

{client.browser} on {client.os}

-

{client.device}

-
-

{formatNumber(client.visitors)} visitors

-

{formatNumber(client.events)} events

-
- ))} - {!clients30d.length ?

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.