# puppy-tracker A tiny offline-first PWA for tracking your puppy's sleep, meals, pees, and poos. 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. - Service worker bypasses cache for `/api/*` so writes always hit the server when online; static assets are still cached for offline use. 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 │ └── main.go # JSON-file 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-events.json' ``` ## 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 at `/var/lib/puppy-tracker/events.json` via `StateDirectory`. ## 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).