Skip to content

Add Stats page: trends over time with URL-based period state - #3

Merged
CalebLovell merged 4 commits into
mainfrom
feat/stats-page
Jul 22, 2026
Merged

Add Stats page: trends over time with URL-based period state#3
CalebLovell merged 4 commits into
mainfrom
feat/stats-page

Conversation

@CalebLovell

Copy link
Copy Markdown
Owner

Summary

Depends on #2 (this branch is stacked on it — the diff below will shrink to just the Stats page once #2 merges).

  • New Stats page: period switcher (Last 24 Hours / Last 7 Days / Last 30 Days / All Time), a detections-over-time area chart, a top-species bar chart, and an activity-by-hour-of-day chart (surfaces the dawn/dusk pattern from the seed data). Summary cards for total detections, unique species, top species, and busiest period.
  • "Last 24 Hours" is a rolling window, not a calendar-day boundary — so it never looks emptied out right after midnight or early in the morning. All periods use the same rolling-window approach for consistency.
  • State lives in the URL: ?period=day etc., via a Zod-validated route search schema (.default().catch() gives a required output type without forcing every <Link to="/stats"> to pass search explicitly), stripped from the URL when it's the default. Back/forward and shareable links work correctly.
  • Charting via Recharts, themed with the site's existing CSS variables (moss/sand/clay).

Test plan

  • tsc --noEmit and biome check clean
  • npm run build succeeds
  • Verified in the browser: all 4 periods show correct data, URL updates on switch, browser back/forward restores state correctly, charts render themed correctly

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com

CalebLovell and others added 4 commits July 22, 2026 17:58
Remove the hill/tree/sun hero graphic and the dotted paper-grain
background texture per feedback -- flat solid background everywhere.
Also reduce the shared --radius token so cards, buttons, inputs, and
badges read with tighter, less rounded corners site-wide, and add
scrollbar-gutter: stable so pages that change height (e.g. filtering
a grid) don't shift horizontally when the scrollbar appears/disappears.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…udio playback

Rebuilds the species listing as a card grid (search box; sort by most
recordings / recent / A-Z with icons and pagination), each card showing
a Wikipedia thumbnail (with graceful fallback), this-hour/all-time
counts, and Play/Wiki/eBird actions.

Audio playback is fully wired, not a placeholder: adds
BIRDNET_EXTRACTED_DIR config and a dynamic server route that streams
clips from BirdNET-Pi's real extraction path (BirdSongs/Extracted/
By_Date/..., a sibling of the repo, matching production layout).
Wikipedia images come from their public summary API (image + canonical
page URL, no key needed); eBird links go through a scoped search since
their species pages require login for anonymous requests.

Worked around three real bugs in this bleeding-edge TanStack Start/
Nitro version along the way: a Node-only import leaking into the
client bundle (fixed by marking db/index.ts server-only and splitting
audio.ts's filesystem code into audio.server.ts), a 3-segment dynamic
route silently failing to register (collapsed to 2 segments), and
native <audio src> requests 404ing while identical fetch() calls
succeeded (worked around by fetching the clip and playing it from a
Blob URL). Verified in both dev and a production build.

Card layout also fixes two bugs found after building it: the image
wrapper's height varied across cards despite aspect-square (a flex/
aspect-ratio interaction quirk) -- replaced with an explicit fixed
height. Reordered content to name/sciname, then image, then stats,
then actions, top to bottom.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
For each species, synthesizes a short distinct tone at its most recent
detection's extraction path (BirdSongs/Extracted/By_Date/...), matching
the same BIRDNET_EXTRACTED_DIR default web-ui uses, so the new
play-button feature has real audio to play during local dev.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a Stats page with a period switcher (Last 24 Hours / Last 7 Days /
Last 30 Days / All Time), backed by Recharts: a detections-over-time
area chart, a top-species bar chart, and an activity-by-hour-of-day
chart (which surfaces the dawn/dusk pattern already built into the seed
data). Summary cards show total detections, unique species, the top
species, and the busiest bucket for the selected period.

"Last 24 Hours" is a rolling window (now minus 24h), not a calendar-day
boundary, so it never looks emptied out right after midnight or early
in the morning -- same rolling-window approach for the other periods.

The selected period lives entirely in the URL (?period=day), validated
via a Zod schema on the route (.default().catch() gives a required
output type without forcing every <Link to="/stats"> to pass search
explicitly) and stripped from the URL when it's the default, so /stats
stays clean until the user picks something non-default. Back/forward
and shareable links work correctly as a result.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 22, 2026 23:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Stats page (charts + summary cards) with URL-driven period state, plus supporting UI components and data/server utilities to power the Stats and updated Species experiences.

Changes:

  • Adds /stats route with period switcher persisted in ?period=... and multiple Recharts visualizations.
  • Reworks /species into a searchable/sortable card grid with image + playback actions, backed by new server data shaping and an audio streaming route.
  • Applies small global UI polish (radius token, stable scrollbar gutter, removes home hero band).

Reviewed changes

Copilot reviewed 21 out of 22 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
web-ui/src/styles.css Updates radius token; adds scrollbar-gutter: stable; removes paper-grain overlay.
web-ui/src/routeTree.gen.ts Registers new /stats and /api/audio/... routes in the generated route tree.
web-ui/src/routes/stats.tsx New Stats page UI with URL-based period state and Recharts visualizations.
web-ui/src/routes/species.tsx Replaces table with searchable/sortable/paginated card grid and audio playback controls.
web-ui/src/routes/index.tsx Removes the HeroBand usage and adjusts spacing/radius on the home page.
web-ui/src/routes/detections.tsx Minor radius tweak for consistent card styling.
web-ui/src/routes/api/audio/$date/$speciesAndFile.ts New server route to read/serve extracted audio clips by date/species/file.
web-ui/src/lib/wikipedia.ts Adds Wikipedia thumbnail lookup + in-memory cache and eBird search URL helper.
web-ui/src/lib/stats.ts Adds server-side stats aggregation for the Stats page (trend/top species/hour activity).
web-ui/src/lib/detections.ts Adds getLifeListCards server fn to power new Species cards (counts, latest audio, wiki info).
web-ui/src/lib/audio.ts Adds base64url segment helpers and audio URL construction/parsing.
web-ui/src/lib/audio.server.ts Adds extracted-dir resolution, traversal-safe path resolution, and MIME mapping.
web-ui/src/db/index.ts Marks DB module as server-only.
web-ui/src/components/ui/toggle.tsx Adds Toggle UI primitive wrapper.
web-ui/src/components/ui/toggle-group.tsx Adds ToggleGroup wrapper used by Stats and Species sort/period controls.
web-ui/src/components/ui/pagination.tsx Adds Pagination UI components used by Species page.
web-ui/src/components/ui/input.tsx Adds Input UI component used by Species search.
web-ui/src/components/HeroBand.tsx Removes unused hero SVG component.
web-ui/src/components/Header.tsx Adds “Stats” navigation link.
web-ui/package.json Adds Recharts and Zod dependencies.
web-ui/package-lock.json Locks added dependencies (Recharts, Zod, and transitive deps).
scripts/seed_test_data.py Adds optional placeholder audio generation under extracted clips directory.
Files not reviewed (1)
  • web-ui/package-lock.json: Generated file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +36 to +40
style={{ "--gap": spacing } as React.CSSProperties}
className={cn(
"group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs",
className,
)}
Comment thread web-ui/src/lib/audio.ts
Comment on lines +20 to +25
function base64UrlEncode(value: string): string {
return btoa(value)
.replaceAll("+", "-")
.replaceAll("/", "_")
.replaceAll("=", "");
}
Comment thread web-ui/src/lib/audio.ts
Comment on lines +27 to +32
function base64UrlDecode(value: string): string {
const padded = value.replaceAll("-", "+").replaceAll("_", "/");
const padding =
padded.length % 4 === 0 ? "" : "=".repeat(4 - (padded.length % 4));
return atob(padded + padding);
}
Comment on lines +10 to +12
const [species, file] = splitSpeciesAndFile(params.speciesAndFile);
const relativePath = `By_Date/${params.date}/${species}/${file}`;
const resolved = resolveExtractedFile(relativePath);
Comment on lines +100 to +104
time: detections.Time,
fileName: detections.File_Name,
})
.from(detections)
.groupBy(detections.Com_Name, detections.Sci_Name)
.orderBy(sql`count(*) desc`);
.orderBy(desc(detections.Date), desc(detections.Time));
Comment on lines +115 to +119
return Promise.all(
totals.map(async (row) => {
const latest = latestByName.get(row.comName);
const { imageUrl, wikipediaUrl } = await getSpeciesInfo(row.comName);
return {
Comment thread web-ui/src/lib/stats.ts
Comment on lines +150 to +152
export const getStatsForPeriod = createServerFn({ method: "GET" })
.validator((period: StatsPeriod) => period)
.handler(async ({ data: period }): Promise<StatsData> => {
onPause={() => setIsPlaying(false)}
onEnded={() => setIsPlaying(false)}
>
<track kind="captions" />
@CalebLovell
CalebLovell merged commit bc86454 into main Jul 22, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants