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>
This commit is contained in:
2026-08-01 21:59:01 -07:00
commit faf73c8be5
25 changed files with 3197 additions and 0 deletions

47
internal/config/config.go Normal file
View File

@@ -0,0 +1,47 @@
package config
import (
"fmt"
"os"
"strings"
"time"
)
// Config is the runtime configuration, populated from the environment.
// Everything here is deployment shape; the things that get tuned mid-challenge
// (start date, targets) live in the data file instead, so changing a target
// does not mean a redeploy.
type Config struct {
Addr string
BaseURL string // public origin, e.g. https://fit.scottyah.com
DataPath string // JSON file on the mounted volume
TZ *time.Location
}
// Load reads configuration from the environment, applying defaults. It returns
// an error only for settings that have no safe default.
func Load() (*Config, error) {
c := &Config{
Addr: env("ADDR", ":8080"),
BaseURL: strings.TrimRight(env("BASE_URL", "http://localhost:8080"), "/"),
DataPath: env("DATA_PATH", "data/tracker.json"),
}
// The whole app is date arithmetic, so a wrong timezone silently logs
// entries against the wrong day. Fail loudly rather than fall back to UTC.
tzName := env("TZ_NAME", "America/Los_Angeles")
loc, err := time.LoadLocation(tzName)
if err != nil {
return nil, fmt.Errorf("TZ_NAME %q: %w", tzName, err)
}
c.TZ = loc
return c, nil
}
func env(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}