d95cd52086c7b8530f1a99549e6f3820bf06e639
Replace the JSON-file event and config stores with a SQLite database (modernc.org/sqlite, pure-Go so the static build keeps CGO_ENABLED=0). Last-write-wins now rides on the upsert's WHERE clause rather than a Go-side map compare; the sync protocol and HTTP handlers are unchanged. On first start the server auto-imports any legacy events.json/config.json sitting in the data dir, renaming them to *.imported. The -data flag now points at puppy.db; photos still live on the filesystem alongside it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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
updatedAttimestamp. - Mutations (add / edit / delete) happen against
localStoragefirst, 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 onupdatedAtand 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 legacyevents.json/config.jsonsitting 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 inlocalStoragefor offline/instant paint and reconciles with the server by last-write-wins onupdatedAt. 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
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:
{
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).
Description
Languages
JavaScript
52.5%
Go
24.4%
CSS
12.4%
HTML
8.4%
Nix
2.3%