diff --git a/flake.nix b/flake.nix index 84a6b58..5fa02b2 100644 --- a/flake.nix +++ b/flake.nix @@ -59,7 +59,9 @@ }; devShells.default = pkgs.mkShell { - packages = [ pkgs.go pkgs.python3 ]; + # nodejs is here for `node --check` on src/*.js — the frontend has no + # build step, so this is a syntax-check tool, not a dependency. + packages = [ pkgs.go pkgs.python3 pkgs.nodejs ]; shellHook = '' echo "puppy-tracker dev shell" echo " cd server && go run . -static ../src -data /tmp/puppy-events.json" diff --git a/src/app.js b/src/app.js index 9b1e0e9..b667da5 100644 --- a/src/app.js +++ b/src/app.js @@ -1989,17 +1989,30 @@ } // Dim the quick actions that don't fit the current sleep state — a nudge - // toward the likely next tap, never a block: everything stays clickable so - // corrections (a missed sleep-end, a mid-nap pee) are always possible. + // toward the likely next tap. The one boundary that would just repeat the + // latest sleep event (a second "sleep start" while already asleep, or a + // second "sleep end" while already awake) is disabled outright, since it + // can only produce a zero-length window. Everything else stays clickable so + // corrections (a mid-nap pee) are never blocked, and a genuinely missed + // boundary is still fixable from the event log, which accepts any time. function renderActionHints(events) { const { state } = currentSleepState(events); document.querySelectorAll("button.action").forEach(btn => { const type = btn.dataset.type; - const unlikely = + const repeat = + state === "asleep" ? type === "sleep-start" + : state === "awake" ? type === "sleep-end" + : false; // no sleep history yet — either boundary is a fine first event + const unlikely = !repeat && ( state === "asleep" ? type !== "sleep-end" : state === "awake" ? type === "sleep-end" - : false; // no sleep history yet — no hints to give + : false // no sleep history yet — no hints to give + ); btn.classList.toggle("unlikely", unlikely); + btn.disabled = repeat; + btn.title = repeat + ? (type === "sleep-start" ? "Already asleep" : "Already awake") + : ""; }); } diff --git a/src/changelog.json b/src/changelog.json index 14cf02d..8733af7 100644 --- a/src/changelog.json +++ b/src/changelog.json @@ -1,4 +1,5 @@ [ + { "date": "2026-08-18", "text": "The sleep button that would just repeat the last one is now disabled: while asleep you can only tap ⏰ Sleep end, and while awake only 😴 Sleep start — no more accidental double taps creating zero-length sleep windows. If you did miss a boundary, you can still add it at the right time from the event log" }, { "date": "2026-08-02", "text": "Added free-text notes: tap 📝 Note to jot down things that happened on a day — vaccinations, vet visits, milestones — with a date, optional photo, and any text. All your notes are collected in a new Notes section that stays visible whatever day you're viewing, so you can see at a glance when things like a tick vaccination were done" }, { "date": "2026-08-02", "text": "The Daily counts chart now has Pees / Poos / Meals checkboxes so you can focus on just the metrics you care about — untick the rest to see, say, only poos; your choice is remembered" }, { "date": "2026-08-01", "text": "Logging a pee or poo now sets off 💧/💩 fireworks that shoot up from the bottom of the screen — a little celebration you can switch off in Settings (and it honours a reduced-motion preference)" }, diff --git a/src/style.css b/src/style.css index daac039..3cd45f9 100644 --- a/src/style.css +++ b/src/style.css @@ -272,6 +272,17 @@ button.action.note { background: var(--note); } but fully tappable, so corrections are never blocked. */ button.action.unlikely { opacity: 0.4; } +/* The boundary that would repeat the latest sleep event is disabled outright + (see renderActionHints) — greyed out rather than merely dimmed, so it reads + as unavailable instead of just discouraged. */ +button.action:disabled { + opacity: 0.35; + filter: grayscale(1); + cursor: not-allowed; +} +button.action:disabled:active { transform: none; } +button.action:disabled:hover { filter: grayscale(1); } + button.ghost { background: transparent; color: var(--text);