# puppy-tracker A tiny offline-first PWA for tracking your puppy's sleep, meals, pees, poos, and weight. The browser is the primary client; a small Go server provides a shared source-of-truth and sync between devices. ## How sync works - Each event has a UUID and an `updatedAt` timestamp. - Mutations (add / edit / delete) happen against `localStorage` first, so the app keeps working when offline. Deletes are recorded as tombstones so they can propagate. - On app load, on `online`, on every mutation (debounced), and every 60 s, the client POSTs its full event list to `/api/events/sync`. The server merges it with its own copy using last-write-wins on `updatedAt` and returns the merged set. - The server keeps its copy in a SQLite database (`puppy.db`); events and the shared profile are separate tables, and last-write-wins is enforced by the upsert itself. On first start it auto-imports any legacy `events.json` / `config.json` sitting alongside it, renaming them to `*.imported`. - Service worker bypasses cache for `/api/*` so writes always hit the server when online; static assets are still cached for offline use. - The puppy's name and birthday are a shared profile stored on the host (`GET`/`PUT /api/config`), so a new device picks them up automatically instead of being configured per-client. The client caches the last-seen values in `localStorage` for offline/instant paint and reconciles with the server by last-write-wins on `updatedAt`. The age shown in the header (in weeks and months) is derived from the birthday. A status pill in the header shows `syncing…` / `synced 2m ago` / `pending` / `sync error` / `offline`. Tap it to force-sync. ## Layout ``` puppy-tracker/ ├── flake.nix # packages (server, static, default), devShell, nixosModule ├── module.nix # systemd unit, StateDirectory, hardening ├── server/ │ ├── go.mod │ ├── go.sum │ └── main.go # SQLite store, LWW sync, static file serving └── src/ # the web app ├── index.html ├── app.js ├── style.css ├── sw.js ├── manifest.json └── icon.svg ``` ## Run locally ```sh nix run # http://localhost:8080, data in $XDG_DATA_HOME/puppy-tracker PUPPY_ADDR=:9000 nix run # custom port # Hot-iterate (data in /tmp): nix develop -c sh -c 'cd server && go run . -static ../src -data /tmp/puppy.db' ``` ## Use it on NixOS In your system flake: ```nix { inputs.puppy-tracker.url = "path:/path/to/puppy-tracker"; outputs = { self, nixpkgs, puppy-tracker, ... }: { nixosConfigurations.my-host = nixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ puppy-tracker.nixosModules.default { services.puppy-tracker = { enable = true; port = 8080; openFirewall = true; }; } ]; }; }; } ``` 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/`). ## Notes - No auth. Intended for a home LAN. If exposing publicly, terminate TLS and authenticate with a reverse proxy in front (Caddy / nginx / Tailscale Funnel).