update viewers page

This commit is contained in:
Heidi
2026-05-16 08:12:02 +01:00
parent 97d7e02285
commit 752b5f9eb8
2 changed files with 130 additions and 68 deletions
+51 -1
View File
@@ -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,
+79 -67
View File
@@ -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 (
<section className="space-y-6 pt-10 sm:pt-14">
@@ -2286,45 +2321,42 @@ function ViewersPage({ viewers }) {
Live consented browser sessions and page activity. Last refreshed {generatedAt}.
</p>
</div>
<span className="w-fit rounded-md bg-surface px-3 py-2 text-sm font-semibold text-fury-cyan">
{viewers.status === 'loading' ? 'Loading' : `${formatNumber(totals.active_now)} active now`}
</span>
<div className="flex flex-wrap items-center gap-3">
<div className="flex rounded-md border border-border bg-surface p-1 text-sm font-semibold" aria-label="Analytics time window">
{Object.entries(periods).map(([key, period]) => (
<button
className={`rounded px-3 py-1.5 transition ${
analyticsWindow === key ? 'bg-fury-red text-bg shadow-sm' : 'text-text-soft hover:text-text'
}`}
key={key}
onClick={() => setAnalyticsWindow(key)}
type="button"
>
{period.label}
</button>
))}
</div>
<span className="w-fit rounded-md bg-surface px-3 py-2 text-sm font-semibold text-fury-cyan">
{viewers.status === 'loading' ? 'Loading' : `${formatNumber(totals.active_now)} active now`}
</span>
</div>
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
<Stat label="Active now" value={formatNumber(totals.active_now)} />
<Stat label="Visitors 24h" value={formatNumber(totals.visitors_24h)} />
<Stat label="Page views 24h" value={formatNumber(totals.page_views_24h)} />
<Stat label="Events 24h" value={formatNumber(totals.events_24h)} />
<Stat label={`Visitors ${periodData.label}`} value={formatNumber(periodData.totals.visitors)} />
<Stat label={`Page views ${periodData.label}`} value={formatNumber(periodData.totals.pageViews)} />
<Stat label={`Events ${periodData.label}`} value={formatNumber(periodData.totals.events)} />
</div>
<AnalyticsPeriodSection
activity={activity24h}
bucketLabel="Hourly count"
description="Consented analytics grouped by hour"
pointFormat={shortTimeFormat}
title="Last 24 hours"
totals={{
events: totals.events_24h,
visitors: totals.visitors_24h,
sessions: totals.sessions_24h,
pageViews: totals.page_views_24h,
}}
/>
<AnalyticsPeriodSection
activity={activity30d}
bucketLabel="Daily count"
description="Retained consented analytics over the current privacy window"
pointFormat={shortDateFormat}
title="Last 30 days"
totals={{
events: totals.events_30d,
visitors: totals.visitors_30d,
sessions: totals.sessions_30d,
pageViews: totals.page_views_30d,
}}
activity={periodData.activity}
bucketLabel={periodData.bucketLabel}
description={periodData.description}
pointFormat={periodData.pointFormat}
title={periodData.title}
totals={periodData.totals}
/>
<div className="overflow-hidden rounded-lg border border-border bg-fury-white shadow-sm">
@@ -2418,9 +2450,9 @@ function ViewersPage({ viewers }) {
<div className="rounded-lg border border-border bg-fury-white shadow-sm">
<div className="border-b border-surface px-5 py-4">
<h2 className="text-lg font-semibold">Top pages</h2>
<p className="mt-1 text-sm text-text-soft">Page views over the last 24 hours</p>
<p className="mt-1 text-sm text-text-soft">Page views over {periodData.title.toLowerCase()}</p>
</div>
{topPages.map((page) => (
{periodData.topPages.map((page) => (
<div className="grid grid-cols-[1fr_auto] gap-3 border-b border-surface px-5 py-3 text-sm" key={page.page_path}>
<div className="min-w-0">
<p className="truncate font-semibold">{page.page_title || page.page_path}</p>
@@ -2429,27 +2461,28 @@ function ViewersPage({ viewers }) {
<p className="font-semibold text-fury-cyan">{formatNumber(page.views)}</p>
</div>
))}
{!topPages.length ? <p className="px-5 py-10 text-sm text-text-soft">No page views recorded yet</p> : null}
{!periodData.topPages.length ? <p className="px-5 py-10 text-sm text-text-soft">No page views recorded yet</p> : null}
</div>
<div className="rounded-lg border border-border bg-fury-white shadow-sm">
<div className="border-b border-surface px-5 py-4">
<h2 className="text-lg font-semibold">Clients</h2>
<p className="mt-1 text-sm text-text-soft">Browsers, devices, and operating systems seen in 24 hours</p>
<p className="mt-1 text-sm text-text-soft">Browsers, devices, and operating systems seen over {periodData.title.toLowerCase()}</p>
</div>
{clients.map((client) => (
{periodData.clients.map((client) => (
<div
className="grid grid-cols-[1fr_auto] gap-3 border-b border-surface px-5 py-3 text-sm"
className="grid grid-cols-[1fr_auto_auto] gap-3 border-b border-surface px-5 py-3 text-sm"
key={`${client.browser}-${client.os}-${client.device}`}
>
<div className="min-w-0">
<p className="truncate font-semibold">{client.browser} on {client.os}</p>
<p className="truncate text-xs text-text-soft">{client.device}</p>
</div>
<p className="font-semibold text-fury-cyan">{formatNumber(client.events)}</p>
<p className="text-text-soft">{formatNumber(client.visitors || 0)} visitors</p>
<p className="font-semibold text-fury-cyan">{formatNumber(client.events)} events</p>
</div>
))}
{!clients.length ? <p className="px-5 py-10 text-sm text-text-soft">No client data recorded yet</p> : null}
{!periodData.clients.length ? <p className="px-5 py-10 text-sm text-text-soft">No client data recorded yet</p> : null}
</div>
</div>
@@ -2474,34 +2507,13 @@ function ViewersPage({ viewers }) {
<div className="border-b border-surface px-5 py-4">
<h2 className="text-lg font-semibold">Location signals</h2>
<p className="mt-1 text-sm text-text-soft">
Cloudflare edge geolocation from the last 30 days
Cloudflare edge geolocation from {periodData.title.toLowerCase()}
</p>
</div>
<LocationSignalMap countries={countries} locations={locations} />
<LocationSignalMap countries={periodData.countries} locations={periodData.locations} />
</div>
</div>
<div className="rounded-lg border border-border bg-fury-white shadow-sm">
<div className="border-b border-surface px-5 py-4">
<h2 className="text-lg font-semibold">Clients over 30 days</h2>
<p className="mt-1 text-sm text-text-soft">Browsers, devices, and operating systems across retained data</p>
</div>
{clients30d.map((client) => (
<div
className="grid grid-cols-[1fr_auto_auto] gap-3 border-b border-surface px-5 py-3 text-sm"
key={`${client.browser}-${client.os}-${client.device}-30d`}
>
<div className="min-w-0">
<p className="truncate font-semibold">{client.browser} on {client.os}</p>
<p className="truncate text-xs text-text-soft">{client.device}</p>
</div>
<p className="text-text-soft">{formatNumber(client.visitors)} visitors</p>
<p className="font-semibold text-fury-cyan">{formatNumber(client.events)} events</p>
</div>
))}
{!clients30d.length ? <p className="px-5 py-10 text-sm text-text-soft">No 30-day client data recorded yet</p> : null}
</div>
<p className="text-xs text-text-soft">
Analytics are opt-in, retained for {formatNumber(data.privacy?.retention_days || 30)} days,
and public output excludes raw IP addresses and precise location.