Compare commits

..

3 Commits

Author SHA1 Message Date
Alexander Heldt 01ad078b2a Stamp "now" events to the second, not the minute
The time input has no seconds and fromDateTimeInputs floors to :00, so an
event logged at "now" was stored up to ~59s in the past — the big-clock
counter would jump to e.g. "0:40" the instant a sleep boundary was logged.

Track whether the user has actually touched the date/time fields (reset in
openNoteDialog and the "Now" button, set on manual change/input). When
untouched, noteDialogAt() returns the exact Date.now(); an explicitly
picked time is still parsed from the inputs (minute precision is fine there).
2026-07-10 12:36:30 +00:00
Alexander Heldt 8f4034ef47 Fix "Currently" row disagreeing with the big clock about sleep state
The "Currently" row derived its state from isCurrentlyAsleep() (ascending
stable sort, then fold) while the big clock used currentSleepState()
(max-by-`at` with a strict >). When two sleep boundary events shared the
same `at`, the two broke the tie differently, so the row could show
"Asleep" during a wake window while the clock said "Awake".

Make both read from the single currentSleepState() source, and give it a
deterministic tie-breaker: for equal `at`, the later updatedAt (most
recently logged boundary) wins. Remove the now-unused isCurrentlyAsleep().
2026-07-10 12:33:00 +00:00
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
+26 -14
View File
@@ -394,23 +394,20 @@
return total;
}
function isCurrentlyAsleep(events) {
const sorted = [...events].sort((a, b) => a.at - b.at);
let asleep = false;
for (const e of sorted) {
if (e.type === "sleep-start") asleep = true;
else if (e.type === "sleep-end") asleep = false;
}
return asleep;
}
// Current state derived from the *latest* sleep event. Used by the live
// counter at the top of the page.
// Current state derived from the *latest* sleep event. The single source of
// truth for both the big clock and the "Currently" row. For two boundary
// events sharing the same `at` (common once "now" events are minute-floored),
// the one logged later (higher updatedAt) wins, so the tie resolves the same
// way everywhere it's read.
function currentSleepState(events) {
let latest = null;
for (const e of events) {
if (e.type !== "sleep-start" && e.type !== "sleep-end") continue;
if (!latest || e.at > latest.at) latest = e;
if (!latest ||
e.at > latest.at ||
(e.at === latest.at && (e.updatedAt || 0) > (latest.updatedAt || 0))) {
latest = e;
}
}
if (!latest) return { state: null, since: 0 };
return {
@@ -603,7 +600,7 @@
const row = document.getElementById("currently-row");
const currently = document.getElementById("currently");
if (isCurrentlyAsleep(events)) {
if (currentSleepState(events).state === "asleep") {
row.hidden = false;
currently.textContent = "😴 Asleep";
} else {
@@ -1277,6 +1274,10 @@
let pendingType = null;
let notePhotoBlob = null; // pending blob for the dialog (not yet committed)
let notePhotoURL = null; // current preview object URL
// Whether the user has manually touched the date/time fields. While false the
// dialog logs at the exact current millisecond rather than the minute-floored
// input, so a just-logged event doesn't look up to ~59s old.
let noteTimeEdited = false;
function clearNotePhoto() {
notePhotoBlob = null;
@@ -1291,6 +1292,7 @@
function openNoteDialog(type) {
pendingType = type;
noteInput.value = "";
noteTimeEdited = false;
const now = Date.now();
noteDate.value = toDateInput(now);
noteTime.value = toTimeInput(now);
@@ -1304,6 +1306,9 @@
}
function noteDialogAt() {
// Untouched time → stamp the exact current instant (sub-minute accurate).
// Once the user picks a time, honor the input (minute precision is fine).
if (!noteTimeEdited) return Date.now();
const parsed = fromDateTimeInputs(noteDate.value, noteTime.value);
return Number.isFinite(parsed) ? parsed : Date.now();
}
@@ -1312,6 +1317,13 @@
const now = Date.now();
noteDate.value = toDateInput(now);
noteTime.value = toTimeInput(now);
noteTimeEdited = false; // "Now" means log at the current instant again
});
[noteDate, noteTime].forEach(el => {
const markEdited = () => { noteTimeEdited = true; };
el.addEventListener("change", markEdited);
el.addEventListener("input", markEdited);
});
notePhotoBtn.addEventListener("click", () => notePhotoInput.click());