Alexander Heldt 9207aaa4aa Store events and config in SQLite
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.
2026-07-09 16:56:35 +00:00
2026-07-09 16:56:35 +00:00
2026-07-04 09:27:13 +00:00
2026-06-21 17:41:56 +00:00
2026-07-09 16:56:35 +00:00
2026-07-09 16:56:35 +00:00
2026-07-09 16:56:35 +00:00
2026-06-21 17:41:56 +00:00

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

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).
S
Description
No description provided
Readme 1.3 MiB
Languages
JavaScript 52.5%
Go 24.4%
CSS 12.4%
HTML 8.4%
Nix 2.3%