Files
fit/README.md
scott 3168a4ef96
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 2m44s
Rename the app from tracker to fit
The directory, the repo and the hostname are all fit; the module path,
the image, the namespace and the data file were still tracker. Nothing
is deployed yet, so this is free now and would not be later.

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

125 lines
5.0 KiB
Markdown

# fit
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/fit.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/fit.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 fit
kubectl create secret generic fit-basic-auth -n fit --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 fit deploy/fit-dep -- cat /data/fit.json > fit.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.