Files
bookhoard/templates/stats.templ
T
John O'Keefe 5a2c1585ae refactor(stats): 'Web / unsynced' bucket label → 'Web'; analytics tests isolated from the live reader
The bucket name read as confusing — 'unsynced' implied data loss when
the push is always captured; it only lacks a device credential. The
bucket is just 'Web' now (service fallback, stats.ts fallback, panel
copy, tests).

Also: analytics_test exact-zero assertions (EmptyHistory,
NoReadingHistory) ran against testuser, which is a LIVE reader — real
sessions accrued on it mid-suite and both flapped. They now run under
the fresh regular user, like the stats tests; the 'testuser is a live
reader' rule is recorded in AGENTS.md.
2026-10-14 13:24:30 -04:00

101 lines
5.5 KiB
Templ
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package templates
templ Stats(user User) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Reading Stats - Bookhoard</title>
<script src="/static/htmx.min.js"></script>
<link href="/static/style.css" rel="stylesheet"/>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
</head>
<body x-data="stats" x-init="initStatsPage()" class="theme-{ user.Theme }">
@Header(user, "/stats")
<div class="mx-auto container px-4 sm:px-6 lg:px-8 py-8">
<div class="mb-8">
<div class="flex items-center gap-3 mb-1">
<span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
@Icon("chart", "h-5 w-5")
</span>
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Reading Stats</h1>
</div>
<p class="text-sm" style="color: var(--text-secondary)">Days are bucketed in your timezone ({ user.Timezone }) · totals derive from your devices' sync pushes, not a stopwatch.</p>
</div>
<div class="card p-4 mb-6 flex flex-wrap items-center justify-between gap-3">
<div class="flex gap-2" id="stats-range-selector">
<button data-range="7" @click="selectRange('7')" class="stats-range-btn btn btn-secondary text-sm">Last 7 days</button>
<button data-range="30" @click="selectRange('30')" class="stats-range-btn btn btn-secondary text-sm">30 days</button>
<button data-range="90" @click="selectRange('90')" class="stats-range-btn btn btn-secondary text-sm">90 days</button>
<button data-range="year" @click="selectRange('year')" class="stats-range-btn btn btn-secondary text-sm">This year</button>
</div>
<p class="text-xs" style="color: var(--text-secondary)" id="stats-window-label"></p>
</div>
<div class="grid grid-cols-2 lg:grid-cols-5 gap-4 mb-8">
<div class="stat-card">
<h3 class="text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Reading Time</h3>
<p id="stats-total-minutes" class="text-3xl font-bold" style="color: var(--accent)">–</p>
</div>
<div class="stat-card">
<h3 class="text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Pages</h3>
<p id="stats-total-pages" class="text-3xl font-bold" style="color: var(--accent)">–</p>
</div>
<div class="stat-card">
<h3 class="text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Streaks</h3>
<p id="stats-streaks" class="text-lg font-bold leading-tight mt-1" style="color: var(--text-primary)">–</p>
</div>
<div class="stat-card">
<h3 class="text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Books Finished</h3>
<p id="stats-books-finished" class="text-3xl font-bold" style="color: var(--accent)">–</p>
</div>
<div class="stat-card">
<h3 class="text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Most Active</h3>
<p id="stats-most-active" class="text-sm font-bold leading-snug mt-2" style="color: var(--text-primary)">–</p>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<div class="card p-6">
<h3 class="text-sm font-semibold uppercase tracking-wide mb-4" style="color: var(--text-secondary)">Daily Reading Minutes</h3>
<div class="h-64">
<canvas id="stats-daily-minutes-chart"></canvas>
</div>
</div>
<div class="card p-6">
<h3 class="text-sm font-semibold uppercase tracking-wide mb-4" style="color: var(--text-secondary)">Pages per Day</h3>
<div class="h-64">
<canvas id="stats-daily-pages-chart"></canvas>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
<div class="card p-6">
<h3 class="text-sm font-semibold uppercase tracking-wide mb-4" style="color: var(--text-secondary)">By Library</h3>
<div id="stats-library-breakdown" class="space-y-3">
<div class="text-center py-6 text-sm" style="color: var(--text-secondary)">Loading…</div>
</div>
</div>
<div class="card p-6">
<h3 class="text-sm font-semibold uppercase tracking-wide mb-4" style="color: var(--text-secondary)">By Device</h3>
<p class="text-xs mb-3" style="color: var(--text-secondary)">Where the sync pushes came from — registered device names, plus a Web bucket for pushes that carry no device credential (the web reader).</p>
<div id="stats-device-breakdown" class="space-y-3">
<div class="text-center py-6 text-sm" style="color: var(--text-secondary)">Loading…</div>
</div>
</div>
<div class="card p-6">
<h3 class="text-sm font-semibold uppercase tracking-wide mb-4" style="color: var(--text-secondary)">Most Read</h3>
<div id="stats-top-books" class="space-y-3">
<div class="text-center py-6 text-sm" style="color: var(--text-secondary)">Loading…</div>
</div>
</div>
</div>
<div id="stats-empty" class="card p-8 text-center hidden">
<p class="text-sm mb-2" style="color: var(--text-primary)">No reading sessions recorded in this range yet.</p>
<p class="text-xs" style="color: var(--text-secondary)">Sessions are captured every time a device syncs reading progress (KOReader, the app, the web reader).</p>
</div>
</div>
@ErrorToast("")
</body>
</html>
}