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>
125 lines
5.0 KiB
Markdown
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.
|