Files
puppy-tracker/src/index.html
T
Alexander Heldt dbcac0653e Prompt to reload when a new version is available
The service worker used to skipWaiting() on install and claim clients on
activate, so a new build's assets swapped in silently and a long-open tab
kept running stale JS. Switch to the standard update flow: the worker now
waits until the page sends it a SKIP_WAITING message, and the page shows a
"A new version is available — Reload / Later" banner when a new worker
reaches "installed" while one is already controlling the tab (that
controller check suppresses the first-install prompt).

Reload posts SKIP_WAITING and reloads on controllerchange (guarded against
a reload loop and against the initial clients.claim on a fresh install);
Later dismisses until the next update. Since browsers only auto-check on
navigation, also poll registration.update() hourly and on visibilitychange.
Bump the cache to v9 so the old cache is cleaned up on activate.
2026-07-10 12:30:30 +00:00

309 lines
13 KiB
HTML
Raw 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.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="theme-color" content="#7c5cff" />
<title>Puppy Tracker</title>
<link rel="manifest" href="manifest.json" />
<link rel="icon" href="icon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="icon.svg" />
<link rel="stylesheet" href="style.css" />
<script>
// Apply a saved theme before first paint so there's no light/dark flash.
// No saved choice → leave it to the prefers-color-scheme media query.
(function () {
try {
var t = localStorage.getItem("puppy-tracker:theme");
if (t === "light" || t === "dark") document.documentElement.dataset.theme = t;
} catch (e) {}
})();
</script>
</head>
<body>
<!-- Shown when a newer build's service worker is waiting. "Reload" activates
it and refreshes onto the new assets; "Later" dismisses until next time.
Suppressed on the very first install (see app.js). -->
<div id="update-banner" class="update-banner" hidden role="status" aria-live="polite">
<span class="update-banner-msg">A new version is available</span>
<span class="update-banner-actions">
<button type="button" id="update-reload" class="update-banner-btn">Reload</button>
<button type="button" id="update-later" class="update-banner-btn ghostish">Later</button>
</span>
</div>
<!-- Login / register gate. Shown until the session check succeeds; the app
(#app) stays hidden behind it so no puppy data paints while logged out. -->
<div id="auth-screen" class="auth-screen" hidden>
<div class="auth-card">
<h1>🐶 Puppy Tracker</h1>
<p class="auth-sub" id="auth-sub">Sign in to continue</p>
<form id="auth-form">
<label>Email
<input type="email" id="auth-email" autocomplete="username" required />
</label>
<label>Password
<input type="password" id="auth-password" autocomplete="current-password" required minlength="8" />
</label>
<label id="auth-invite-field" hidden>Invite code
<input type="text" id="auth-invite" autocomplete="off" placeholder="Ask the owner for this" />
</label>
<p id="auth-error" class="auth-error" hidden></p>
<button type="submit" id="auth-submit">Sign in</button>
</form>
<p class="auth-toggle">
<span id="auth-toggle-text">No account yet?</span>
<button type="button" id="auth-toggle-btn" class="linklike">Create one</button>
</p>
</div>
</div>
<div id="app" hidden>
<header>
<div class="title">
<h1 id="app-title">🐶 Puppy Tracker</h1>
<div id="puppy-age" class="puppy-age" hidden></div>
</div>
<div class="header-actions">
<button type="button" id="settings-btn" class="ghost icon-btn" aria-label="Settings" title="Settings">⚙️</button>
<button type="button" id="logout-btn" class="ghost icon-btn" aria-label="Log out" title="Log out">🚪</button>
<div id="online-status" class="status-pill"></div>
</div>
</header>
<main>
<section id="big-clock" class="big-clock" hidden>
<div class="bc-label" id="bc-label"></div>
<div class="bc-time" id="bc-time">0:00</div>
<div class="bc-since" id="bc-since"></div>
</section>
<section class="quick-actions">
<h2>Log event</h2>
<div class="grid">
<button class="action sleep" data-type="sleep-start">😴 Sleep start</button>
<button class="action sleep" data-type="sleep-end">⏰ Sleep end</button>
<button class="action eat" data-type="eat">🍽️ Ate</button>
<button class="action pee" data-type="pee">💧 Pee</button>
<button class="action poo" data-type="poo">💩 Poo</button>
<button class="action weight" data-type="weight">⚖️ Weigh-in</button>
</div>
</section>
<section class="day-bar">
<button type="button" id="day-prev" class="ghost" aria-label="Previous day"></button>
<input type="date" id="day-picker" />
<button type="button" id="day-today" class="ghost">Today</button>
<button type="button" id="day-next" class="ghost" aria-label="Next day"></button>
</section>
<section class="overview">
<h2 id="overview-title">Today's overview</h2>
<div class="stats">
<div class="stat">
<div class="stat-label">Sleep</div>
<div class="stat-value" id="stat-sleep">0h 0m</div>
</div>
<div class="stat">
<div class="stat-label">Awake</div>
<div class="stat-value" id="stat-awake">0h 0m</div>
</div>
<div class="stat">
<div class="stat-label">Meals</div>
<div class="stat-value" id="stat-meals">0</div>
</div>
<div class="stat">
<div class="stat-label">Pees</div>
<div class="stat-value" id="stat-pees">0</div>
</div>
<div class="stat">
<div class="stat-label">Poos</div>
<div class="stat-value" id="stat-poos">0</div>
</div>
</div>
<div class="lasts">
<div class="last-row"><span>Last pee</span><span id="last-pee"></span></div>
<div class="last-row"><span>Last poo</span><span id="last-poo"></span></div>
<div class="last-row"><span>Last meal</span><span id="last-eat"></span></div>
<div class="last-row"><span>Last sleep</span><span id="last-sleep"></span></div>
<div class="last-row" id="currently-row" hidden><span>Currently</span><span id="currently"></span></div>
</div>
</section>
<section class="timing">
<h2>Bathroom timing <span class="muted-note">(last 7 days)</span></h2>
<div class="lasts">
<div class="last-row"><span>Typical time between pees</span><span id="gap-pee"></span></div>
<div class="last-row"><span>Shortest between pees</span><span id="gap-pee-min"></span></div>
<div class="last-row"><span>Typical time between poos</span><span id="gap-poo"></span></div>
<div class="last-row"><span>Shortest between poos</span><span id="gap-poo-min"></span></div>
</div>
<p class="muted-note timing-hint" id="timing-hint"></p>
</section>
<section class="sleep">
<h2>Sleep windows</h2>
<ul id="sleep-list" class="wake-list"></ul>
<p id="sleep-empty" class="empty">No sleep windows yet for this day.</p>
</section>
<section class="wake">
<h2>Wake windows</h2>
<ul id="wake-list" class="wake-list"></ul>
<p id="wake-empty" class="empty">No wake windows yet for this day.</p>
</section>
<section class="weekly">
<h2>Last 7 days</h2>
<div class="chart">
<div class="chart-title">Sleep (hours)</div>
<svg id="chart-sleep" class="chart-svg" viewBox="0 0 320 160" role="img" aria-label="Sleep hours per day for the last 7 days"></svg>
</div>
<div class="chart">
<div class="chart-title">Daily counts</div>
<svg id="chart-counts" class="chart-svg" viewBox="0 0 320 180" role="img" aria-label="Pee, poo and meal counts per day for the last 7 days"></svg>
<div class="legend">
<span class="lg pee"><span class="sw"></span>Pees</span>
<span class="lg poo"><span class="sw"></span>Poos</span>
<span class="lg eat"><span class="sw"></span>Meals</span>
</div>
</div>
</section>
<section class="weight">
<h2>Weight</h2>
<div class="weight-summary">
<div class="stat">
<div class="stat-label">Latest</div>
<div class="stat-value" id="weight-latest"></div>
</div>
<div class="stat">
<div class="stat-label">Since last</div>
<div class="stat-value" id="weight-change"></div>
</div>
</div>
<div class="chart">
<div class="chart-title">Weight (kg)</div>
<svg id="chart-weight" class="chart-svg" viewBox="0 0 320 180" role="img" aria-label="Weight in kilograms over time"></svg>
<p id="weight-point-info" class="muted-note weight-point-info"></p>
</div>
<ul id="weight-list" class="wake-list"></ul>
<p id="weight-empty" class="empty">No weigh-ins logged yet.</p>
</section>
<section class="history">
<h2>History</h2>
<ul id="event-list" class="event-list"></ul>
<p id="empty-state" class="empty">No events logged for this day.</p>
</section>
</main>
</div><!-- /#app -->
<dialog id="settings-dialog">
<form method="dialog" id="settings-form">
<h3>Puppy settings</h3>
<label>Name
<input type="text" id="settings-name" placeholder="e.g. Rex" autocomplete="off" />
</label>
<label>Birthday
<input type="date" id="settings-birthday" />
</label>
<label class="toggle-row">
<span>Dark mode</span>
<input type="checkbox" id="settings-theme" role="switch" class="switch" />
</label>
<menu>
<button value="cancel" class="ghost">Cancel</button>
<button value="save" id="settings-save">Save</button>
</menu>
<hr class="settings-sep" />
<button type="button" id="delete-account-btn" class="danger danger-block">Delete account…</button>
</form>
</dialog>
<dialog id="delete-account-dialog">
<form method="dialog" id="delete-account-form">
<h3>Delete account</h3>
<p class="danger-text">
This permanently deletes your account and <strong>all its data</strong>
every event, photo and your puppy profile. This can't be undone.
</p>
<label>Confirm your password
<input type="password" id="delete-account-password" autocomplete="current-password" />
</label>
<p id="delete-account-error" class="auth-error" hidden></p>
<menu>
<button value="cancel" class="ghost">Cancel</button>
<button type="button" id="delete-account-confirm" class="danger">Delete forever</button>
</menu>
</form>
</dialog>
<dialog id="note-dialog">
<form method="dialog" id="note-form">
<h3 id="note-title">Add note</h3>
<label>Time
<div class="time-row">
<input type="date" id="note-date" />
<input type="time" id="note-time" lang="en-GB" />
<button type="button" id="note-time-now" class="ghost">Now</button>
</div>
</label>
<label id="note-weight-field" hidden>Weight (kg)
<input type="number" id="note-weight" inputmode="decimal" step="0.01" min="0" placeholder="e.g. 5.2" />
</label>
<label>Note
<textarea id="note-input" rows="4" placeholder="e.g. pee was instant, poo took 5min, ate 300g raw food"></textarea>
</label>
<div class="photo-field">
<input type="file" id="note-photo-input" accept="image/*" capture="environment" hidden />
<button type="button" id="note-photo-btn" class="ghost">📷 Add photo</button>
<button type="button" id="note-photo-clear" class="ghost" hidden>Remove photo</button>
<div id="note-photo-preview" class="photo-preview" hidden></div>
</div>
<menu>
<button value="cancel" class="ghost">Cancel</button>
<button value="save" id="note-save">Save</button>
</menu>
</form>
</dialog>
<dialog id="edit-dialog">
<form method="dialog" id="edit-form">
<h3>Edit event</h3>
<label>Time
<div class="time-row">
<input type="date" id="edit-date" />
<input type="time" id="edit-time" lang="en-GB" />
</div>
</label>
<label id="edit-weight-field" hidden>Weight (kg)
<input type="number" id="edit-weight" inputmode="decimal" step="0.01" min="0" />
</label>
<label>Note
<textarea id="edit-note" rows="4"></textarea>
</label>
<div class="photo-field">
<input type="file" id="edit-photo-input" accept="image/*" capture="environment" hidden />
<button type="button" id="edit-photo-btn" class="ghost">📷 Add photo</button>
<button type="button" id="edit-photo-clear" class="ghost" hidden>Remove photo</button>
<div id="edit-photo-preview" class="photo-preview" hidden></div>
</div>
<menu>
<button value="delete" id="edit-delete" class="danger">Delete</button>
<button value="cancel" class="ghost">Cancel</button>
<button value="save">Save</button>
</menu>
</form>
</dialog>
<dialog id="lightbox">
<button type="button" id="lightbox-close" class="lightbox-close" aria-label="Close">×</button>
<img id="lightbox-img" alt="" />
</dialog>
<script src="app.js"></script>
</body>
</html>