Add push reminders for sleep, pee, poo and meals
A closed PWA has no timers, so reminders are evaluated on the server: the event log is already there (clients sync on every mutation), and a ticker re-checks each enabled rule once a minute and pushes the ones that are due. Two rule shapes. "sleep" measures from the last sleep-end and fires only while the puppy is awake. "pee"/"poo"/"eat" measure from the newest event of that type and stay quiet while the puppy is asleep — otherwise they nag all night, and suppressing them means an overdue rule instead fires promptly on waking, which is when it actually matters. Sleep state is derived exactly the way currentSleepState() does in app.js, tie-break included, so both sides always agree. Rules read the event's own timestamp rather than when it synced, so a pee logged offline at 03:10 cancels the reminder retroactively. Every push carries a tag, so a repeat replaces the previous notification instead of stacking another one on the lock screen. last_fired is server-owned and not writable by a client, so a stale device can't force a re-fire. Web Push is implemented directly rather than pulled in as a dependency: RFC 8291 encryption in the RFC 8188 aes128gcm coding with an RFC 8292 VAPID token, stdlib only, checked against the RFC 8291 test vector. The key is generated into vapid.json beside the DB or supplied via -vapid-key; without one the server logs a warning, skips registering the routes, and the client hides the UI. Subscriptions a push service reports as 404/410 are dropped. PNG icons are added because iOS gates push on a Home Screen install and rejects SVG for apple-touch-icon, and Android has no notification icon without them.
This commit is contained in:
@@ -49,14 +49,20 @@ puppy-tracker/
|
||||
│ ├── go.mod
|
||||
│ ├── go.sum
|
||||
│ ├── main.go # SQLite store, LWW sync, static file serving
|
||||
│ └── auth.go # accounts, sessions, invite-gated registration
|
||||
│ ├── auth.go # accounts, sessions, invite-gated registration
|
||||
│ ├── reminders.go # reminder rules, the evaluation loop, push subscriptions
|
||||
│ ├── webpush.go # VAPID + RFC 8291/8188 message encryption
|
||||
│ ├── pedigree.go # SKK lookup, background crawl, per-dog cache
|
||||
│ └── htmlutil.go # scraping helpers for the pedigree crawl
|
||||
└── src/ # the web app
|
||||
├── index.html
|
||||
├── app.js
|
||||
├── style.css
|
||||
├── sw.js
|
||||
├── manifest.json
|
||||
└── icon.svg
|
||||
├── changelog.json
|
||||
├── icon.svg
|
||||
└── icon-180.png, icon-192.png, icon-512.png
|
||||
```
|
||||
|
||||
## Run locally
|
||||
@@ -94,6 +100,55 @@ events, profile and photos.
|
||||
when you pass `-secure-cookies` (enable it behind a TLS proxy), so passwords
|
||||
aren't sent in the clear.
|
||||
|
||||
## Reminders
|
||||
|
||||
Opt-in push notifications for the two things that are easy to lose track of:
|
||||
"time to sleep" and "nothing logged for a while". Turn them on per rule in
|
||||
**Settings**.
|
||||
|
||||
- **Evaluated on the server.** A closed PWA has no timers, so the browser cannot
|
||||
remind you of anything on its own. The server already holds the event log
|
||||
(clients sync on every mutation), so a goroutine re-checks every enabled rule
|
||||
once a minute and pushes the ones that have come due.
|
||||
- **Two rule shapes.** `sleep` measures from the last `sleep-end` and fires only
|
||||
while the puppy is awake. `pee` / `poo` / `eat` measure from the newest event
|
||||
of that type. Each rule has its own interval and repeats at that interval while
|
||||
it stays overdue.
|
||||
- **Quiet while the puppy sleeps.** The event rules are suppressed whenever the
|
||||
latest sleep boundary says "asleep", which is what keeps them from nagging all
|
||||
night — and means an overdue rule fires promptly on waking instead. The server
|
||||
derives sleep state exactly the way `currentSleepState()` does in `app.js`,
|
||||
tie-break included, so both sides always agree.
|
||||
- **One notification per rule.** Every push carries a `tag`, so a repeat replaces
|
||||
the previous notification instead of stacking another one on the lock screen.
|
||||
- **Late syncs cancel a reminder retroactively.** Rules measure from the event's
|
||||
own timestamp, not from when the server heard about it, so a pee logged offline
|
||||
at 03:10 and synced at 03:40 resets the clock as if it had arrived on time.
|
||||
- **Web Push is implemented directly** (`server/webpush.go`): RFC 8291 message
|
||||
encryption in the RFC 8188 `aes128gcm` content encoding, authorized with an
|
||||
RFC 8292 VAPID token. It is stdlib-only, and checked against the RFC 8291
|
||||
test vector in `webpush_test.go`. Subscriptions the push service reports as
|
||||
`404`/`410` are deleted.
|
||||
|
||||
### Requirements
|
||||
|
||||
- **HTTPS.** Push needs a secure context — the same reverse proxy you need for
|
||||
`secureCookies`.
|
||||
- **On iOS the app must be added to the Home Screen** (16.4+). Safari tabs have
|
||||
no `PushManager` at all; the app detects this and says so instead of showing a
|
||||
toggle that cannot work. iOS also drops subscriptions periodically, so the
|
||||
client re-subscribes and re-registers its endpoint on every launch.
|
||||
- **A VAPID key.** Generated into `vapid.json` next to `puppy.db` on first start,
|
||||
or supplied via `-vapid-key` / `PUPPY_VAPID_KEY`. Browsers pin this key at
|
||||
subscribe time: replacing it invalidates every existing subscription. If no key
|
||||
can be established the server logs a warning and comes up without reminders —
|
||||
the `/api/push/*` and `/api/reminders` routes are simply not registered, which
|
||||
is also how the client knows to hide the UI.
|
||||
|
||||
Settings has a *Send a test notification* button, which is the only practical way
|
||||
to tell "never subscribed" apart from "subscribed but not delivering" — push
|
||||
failures are invisible from the browser side, especially on iOS.
|
||||
|
||||
## Pedigree lookup
|
||||
|
||||
Set your dog's SKK chip or registration number in **Settings** (it rides the
|
||||
@@ -135,6 +190,10 @@ In your system flake:
|
||||
# Registration secret, kept out of the Nix store. The file holds:
|
||||
# PUPPY_INVITE_CODE=some-shared-secret
|
||||
inviteCodeFile = "/run/secrets/puppy-invite-code";
|
||||
# Optional. Without it the server generates and keeps its own Web Push
|
||||
# key in /var/lib/puppy-tracker. The file holds:
|
||||
# PUPPY_VAPID_KEY=base64url-p256-private-key
|
||||
vapidKeyFile = "/run/secrets/puppy-vapid-key";
|
||||
# Enable once you terminate TLS in front of the service.
|
||||
secureCookies = false;
|
||||
};
|
||||
@@ -147,7 +206,8 @@ In your system flake:
|
||||
|
||||
The server runs as a `DynamicUser` systemd unit. Data is stored in a SQLite
|
||||
database at `/var/lib/puppy-tracker/puppy.db` via `StateDirectory` (with photos
|
||||
alongside it under `photos/`).
|
||||
alongside it under `photos/`, and a generated `vapid.json` if no `vapidKeyFile`
|
||||
is set).
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
Reference in New Issue
Block a user