Files
fit/README.md
scott faf73c8be5 Track five things a day for seventy-five days
The workout, the protein, the calories, the water, and three lines of
gratitude. One page per day, a card of seventy-five tiles that fill by
how many of the five landed, and a missed day left as a gap rather than
a reset.

Single Go binary with no dependencies. The log is one JSON file on a
mounted volume, written atomically and never silently replaced when it
fails to parse, since it is the one thing here that cannot be recreated.
Auth is Traefik basic auth at the ingress; the app has no login of its
own, so every POST checks Sec-Fetch-Site to stop another origin posting
with those credentials.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 21:59:01 -07:00

125 lines
5.0 KiB
Markdown

# tracker
A 75-day challenge, five things a day: the workout, the protein, the calories,
the water, and three lines of gratitude.
Single Go binary, no dependencies, one JSON file on a volume. Auth is Traefik's
basic auth at the ingress; the app itself has no login.
## What it does
- **One page per day.** Open it, tick the workout, type three numbers, write
three lines. Ticking a box saves in the background — no page reload.
- **Targets, not just numbers.** Protein and water fill towards a floor to
reach; calories fill towards a ceiling not to cross. A day is complete when
all five are hit.
- **The card.** Seventy-five tiles, fifteen across, each filling from the
bottom by how many of that day's five landed. A missed day stays a gap — it
does not reset the count.
- **Yesterday is editable.** Back-arrow to any past day and fill it in. You
cannot log the future.
- **Export and restore** from the settings page, so the log is never only in
one place.
- **Installable.** Add to the home screen and it opens without browser chrome.
## Design
Municipal pool tile: a pale aqua ground with grout lines, deep navy-teal ink,
chlorine blue for anything that fills, and the orange of backstroke flags for
the day you are standing on. It follows the system light/dark setting.
Each measurable is a band that is its own meter — the fill *is* the band, not a
bar next to it — and the track ends where the target is. Go past a ceiling and
the whole band turns hot.
Typography is Avenir Next Condensed for display and the system UI and monospace
faces for everything else, so there are no web fonts to serve.
## Running it locally
Needs Go 1.26. No database.
```sh
export DATA_PATH="$PWD/data/tracker.json"
export TZ_NAME="America/Los_Angeles"
go run ./cmd/server
```
Then open <http://localhost:8080>. The first load asks for the start date and
the targets; everything after that is the day page.
```sh
go test ./...
```
## Configuration
| Variable | Required | Notes |
| --- | --- | --- |
| `DATA_PATH` | no | The log file. Default `data/tracker.json`, relative to the working directory. The directory is created if missing. |
| `TZ_NAME` | no | Default `America/Los_Angeles`. Decides when the day rolls over; a wrong value files entries against the wrong date. |
| `BASE_URL` | no | Public origin. Default `http://localhost:8080`. |
| `ADDR` | no | Listen address, default `:8080`. |
The start date, the length, and the three targets are **not** environment
variables. They live in the data file and are edited from `/settings`, so
changing a target does not need a deploy.
## Deploying
Follows the same shape as the other apps in `~/dev`: push to `main`, Gitea
Actions builds with buildah, pushes to Harbor, and applies `k8s.yaml`.
Before the first deploy, create the basic-auth secret. This is the only thing
guarding the log, so pick a real password:
```sh
htpasswd -nbB scott 'a-real-password' > /tmp/users
kubectl create namespace tracker
kubectl create secret generic tracker-basic-auth -n tracker --from-file=users=/tmp/users
rm /tmp/users
```
Point `fit.scottyah.com` at the cluster and Traefik terminates TLS with the
existing `scottyah-tls` secret.
To pull a copy of the log without the browser:
```sh
kubectl exec -n tracker deploy/tracker-dep -- cat /data/tracker.json > tracker.json
```
## Notes on how it's built
- **The file is the database.** Seventy-five rows for one person do not justify
Postgres. The file is read into memory at boot and written back on every
change: temp file in the same directory, fsync, rename, fsync the directory.
A crash mid-write leaves the previous file intact rather than half of a new
one.
- **A file that does not parse is a fatal error, not a fresh start.** Silently
replacing an unreadable log with an empty challenge would destroy the one
thing here that cannot be recreated. Boot also leaves a `.bak` copy of
whatever last parsed.
- **Blank is not zero.** The three numbers are pointers, so a day with no
calories entered does not read as a perfect calorie day.
- **Dates are local dates, everywhere.** No time of day is stored. Arithmetic
parses them in UTC so a daylight-saving change cannot make a day 23 or 25
hours long and shift the numbering.
- **Nothing needs JavaScript.** The form posts and the page reloads. With
JavaScript the meters move as you type and the save happens in the
background, flushed when the tab goes away.
- **Static assets are content-hashed.** `/static/app.css?v=…` changes whenever
the file does, so a deploy is never stuck behind a cached stylesheet.
- **Writes must come from this site.** Browsers attach basic-auth credentials
to cross-origin form posts, so any other page could otherwise post to
`/import` and replace the log. Every `POST` checks `Sec-Fetch-Site`.
## Things deliberately left out
- Accounts, sessions and passwords in the app. One person, one basic-auth
prompt at the edge.
- A reset rule. Missing a day leaves a gap; it does not send you back to day 1.
- Weights, reps, photos, and measurements. This tracks whether the five things
happened, not what the workout was.