update viewers page
This commit is contained in:
+51
-1
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user