All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 56s
The ingress pointed at scottyah-tls, a secret hand-copied between namespaces and absent from this one, so Traefik fell back to a self-signed certificate. That is invisible behind Cloudflare until the SSL mode is set to Full (strict), which then fails with a 526. Issue into the namespace over DNS-01 instead. Nothing to copy, and the renewal is no longer a thing anyone has to remember. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
130 lines
5.3 KiB
Markdown
130 lines
5.3 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. The certificate is issued by
|
|
cert-manager through the `letsencrypt-prod-cloudflare` cluster issuer, which
|
|
solves a DNS-01 challenge at Cloudflare and writes `fit-tls` into this
|
|
namespace. Nothing is copied in and nothing is renewed by hand.
|
|
|
|
Cloudflare's SSL mode should be Full (strict). Flexible carries the basic-auth
|
|
password to the origin in the clear, and it rides on every request.
|
|
|
|
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.
|