diff --git a/.env.example b/.env.example index 14da4f9..107b3f6 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ # Where the log is written. In the cluster this is the mounted volume. -DATA_PATH=data/tracker.json +DATA_PATH=data/fit.json # Public origin, and the timezone the day rolls over in. BASE_URL=https://fit.scottyah.com diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml index 960db31..c82ba72 100644 --- a/.gitea/workflows/deploy.yaml +++ b/.gitea/workflows/deploy.yaml @@ -30,12 +30,12 @@ jobs: - name: Build image run: | - IMAGE=harbor.scottyah.com/scottyah/tracker + IMAGE=harbor.scottyah.com/scottyah/fit buildah --isolation chroot bud -t $IMAGE:${{ github.sha }} -t $IMAGE:latest . - name: Push image run: | - IMAGE=harbor.scottyah.com/scottyah/tracker + IMAGE=harbor.scottyah.com/scottyah/fit # Harbor serves a publicly trusted certificate, so TLS is verified. # Disabling verification would send these credentials, and accept the # image, over a connection nothing has authenticated. @@ -52,6 +52,6 @@ jobs: chmod +x kubectl mkdir -p ~/.kube echo "${{ secrets.KUBECONFIG_DATA }}" | base64 -d > ~/.kube/config - sed -i "s|harbor.scottyah.com/scottyah/tracker:latest|harbor.scottyah.com/scottyah/tracker:${{ github.sha }}|" k8s.yaml + sed -i "s|harbor.scottyah.com/scottyah/fit:latest|harbor.scottyah.com/scottyah/fit:${{ github.sha }}|" k8s.yaml ./kubectl apply -f k8s.yaml - ./kubectl rollout status deployment/tracker-dep -n tracker --timeout=120s + ./kubectl rollout status deployment/fit-dep -n fit --timeout=120s diff --git a/.gitignore b/.gitignore index 48551e4..ef59804 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -/tracker +/fit *.env .env /data/ diff --git a/Dockerfile b/Dockerfile index 073f660..c3e217e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,15 +14,15 @@ COPY . . RUN CGO_ENABLED=0 GOOS=linux go build \ -trimpath \ -ldflags="-s -w" \ - -o /out/tracker ./cmd/server + -o /out/fit ./cmd/server FROM scratch -COPY --from=build /out/tracker /tracker +COPY --from=build /out/fit /fit # nobody:nogroup — the only writable path is the mounted volume at /data. USER 65534:65534 EXPOSE 8080 -ENTRYPOINT ["/tracker"] +ENTRYPOINT ["/fit"] diff --git a/README.md b/README.md index eebf563..dfda80b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# tracker +# fit A 75-day challenge, five things a day: the workout, the protein, the calories, the water, and three lines of gratitude. @@ -40,7 +40,7 @@ faces for everything else, so there are no web fonts to serve. Needs Go 1.26. No database. ```sh -export DATA_PATH="$PWD/data/tracker.json" +export DATA_PATH="$PWD/data/fit.json" export TZ_NAME="America/Los_Angeles" go run ./cmd/server @@ -57,7 +57,7 @@ go test ./... | Variable | Required | Notes | | --- | --- | --- | -| `DATA_PATH` | no | The log file. Default `data/tracker.json`, relative to the working directory. The directory is created if missing. | +| `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`. | @@ -76,8 +76,8 @@ 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 +kubectl create namespace fit +kubectl create secret generic fit-basic-auth -n fit --from-file=users=/tmp/users rm /tmp/users ``` @@ -87,7 +87,7 @@ 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 +kubectl exec -n fit deploy/fit-dep -- cat /data/fit.json > fit.json ``` ## Notes on how it's built diff --git a/cmd/server/main.go b/cmd/server/main.go index c254da7..23ae3d7 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -14,9 +14,9 @@ import ( // correctly inside a scratch image, which has no /usr/share/zoneinfo. _ "time/tzdata" - "github.com/scottyah/tracker/internal/config" - "github.com/scottyah/tracker/internal/store" - "github.com/scottyah/tracker/internal/web" + "github.com/scottyah/fit/internal/config" + "github.com/scottyah/fit/internal/store" + "github.com/scottyah/fit/internal/web" ) func main() { diff --git a/go.mod b/go.mod index 178759c..ebb1032 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module github.com/scottyah/tracker +module github.com/scottyah/fit go 1.26.1 diff --git a/internal/config/config.go b/internal/config/config.go index 571f7df..f628697 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -24,7 +24,7 @@ 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"), + DataPath: env("DATA_PATH", "data/fit.json"), } // The whole app is date arithmetic, so a wrong timezone silently logs diff --git a/internal/store/store.go b/internal/store/store.go index adc74e4..1eb5543 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -284,7 +284,7 @@ func (s *Store) Export() ([]byte, error) { func (s *Store) Import(body []byte) error { var next data if err := json.Unmarshal(body, &next); err != nil { - return fmt.Errorf("that file is not a tracker export: %w", err) + return fmt.Errorf("that file is not a fit export: %w", err) } if next.Days == nil { next.Days = map[string]*Day{} @@ -313,7 +313,7 @@ func (s *Store) persist() error { } dir := filepath.Dir(s.path) - tmp, err := os.CreateTemp(dir, ".tracker-*.json") + tmp, err := os.CreateTemp(dir, ".fit-*.json") if err != nil { return fmt.Errorf("create temp file: %w", err) } diff --git a/internal/store/store_test.go b/internal/store/store_test.go index 624b08f..2ee8419 100644 --- a/internal/store/store_test.go +++ b/internal/store/store_test.go @@ -96,7 +96,7 @@ func TestMarks(t *testing.T) { } func TestSaveAndReopen(t *testing.T) { - path := filepath.Join(t.TempDir(), "nested", "tracker.json") + path := filepath.Join(t.TempDir(), "nested", "fit.json") st, err := Open(path) if err != nil { @@ -128,7 +128,7 @@ func TestSaveAndReopen(t *testing.T) { } func TestSaveEmptyDayRemovesIt(t *testing.T) { - path := filepath.Join(t.TempDir(), "tracker.json") + path := filepath.Join(t.TempDir(), "fit.json") st, _ := Open(path) if err := st.SaveDay(Day{Date: "2026-07-01", Workout: true}); err != nil { @@ -145,7 +145,7 @@ func TestSaveEmptyDayRemovesIt(t *testing.T) { // A day handed out must not share memory with the stored one, or editing the // copy would quietly rewrite history. func TestDayIsACopy(t *testing.T) { - path := filepath.Join(t.TempDir(), "tracker.json") + path := filepath.Join(t.TempDir(), "fit.json") st, _ := Open(path) st.SaveDay(Day{Date: "2026-07-01", Protein: ptr(190)}) @@ -158,7 +158,7 @@ func TestDayIsACopy(t *testing.T) { } func TestOpenRefusesUnreadableFile(t *testing.T) { - path := filepath.Join(t.TempDir(), "tracker.json") + path := filepath.Join(t.TempDir(), "fit.json") if err := os.WriteFile(path, []byte("{not json"), 0o644); err != nil { t.Fatal(err) } @@ -168,7 +168,7 @@ func TestOpenRefusesUnreadableFile(t *testing.T) { } func TestDaysFillsGaps(t *testing.T) { - path := filepath.Join(t.TempDir(), "tracker.json") + path := filepath.Join(t.TempDir(), "fit.json") st, _ := Open(path) st.SaveSettings(settings()) st.SaveDay(Day{Date: "2026-06-22", Workout: true}) @@ -189,7 +189,7 @@ func TestDaysFillsGaps(t *testing.T) { } func TestImportRejectsJunkDates(t *testing.T) { - path := filepath.Join(t.TempDir(), "tracker.json") + path := filepath.Join(t.TempDir(), "fit.json") st, _ := Open(path) if err := st.Import([]byte(`{"days":{"last tuesday":{"workout":true}}}`)); err == nil { t.Fatal("Import accepted a key that is not a date") diff --git a/internal/web/handlers.go b/internal/web/handlers.go index a03cb7f..6920481 100644 --- a/internal/web/handlers.go +++ b/internal/web/handlers.go @@ -8,7 +8,7 @@ import ( "strings" "time" - "github.com/scottyah/tracker/internal/store" + "github.com/scottyah/fit/internal/store" ) // gratitudeMax caps one gratitude line. It is a line, not an essay, and a cap @@ -250,7 +250,7 @@ func (s *Server) handleExport(w http.ResponseWriter, r *http.Request) { s.fail(w, http.StatusInternalServerError, "The export did not build: "+err.Error()) return } - name := "tracker-" + s.today() + ".json" + name := "fit-" + s.today() + ".json" w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Disposition", `attachment; filename="`+name+`"`) w.Write(body) diff --git a/internal/web/web.go b/internal/web/web.go index 276d7a4..959bade 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -10,8 +10,8 @@ import ( "net/http" "time" - "github.com/scottyah/tracker/internal/config" - "github.com/scottyah/tracker/internal/store" + "github.com/scottyah/fit/internal/config" + "github.com/scottyah/fit/internal/store" ) //go:embed templates/*.html diff --git a/internal/web/web_test.go b/internal/web/web_test.go index 36de917..c60a5cf 100644 --- a/internal/web/web_test.go +++ b/internal/web/web_test.go @@ -9,8 +9,8 @@ import ( "testing" "time" - "github.com/scottyah/tracker/internal/config" - "github.com/scottyah/tracker/internal/store" + "github.com/scottyah/fit/internal/config" + "github.com/scottyah/fit/internal/store" ) // newServer builds a server whose "today" is fixed, so the day arithmetic is @@ -22,7 +22,7 @@ func newServer(t *testing.T, today string) (*Server, *store.Store) { if err != nil { t.Fatal(err) } - st, err := store.Open(filepath.Join(t.TempDir(), "tracker.json")) + st, err := store.Open(filepath.Join(t.TempDir(), "fit.json")) if err != nil { t.Fatal(err) } @@ -240,11 +240,11 @@ func TestExportThenImport(t *testing.T) { if rec.Code != http.StatusOK { t.Fatalf("status %d", rec.Code) } - if !strings.Contains(rec.Header().Get("Content-Disposition"), "tracker-2026-07-31.json") { + if !strings.Contains(rec.Header().Get("Content-Disposition"), "fit-2026-07-31.json") { t.Errorf("download name = %q", rec.Header().Get("Content-Disposition")) } - fresh, err := store.Open(filepath.Join(t.TempDir(), "tracker.json")) + fresh, err := store.Open(filepath.Join(t.TempDir(), "fit.json")) if err != nil { t.Fatal(err) } diff --git a/k8s.yaml b/k8s.yaml index a3712eb..f4c11f3 100644 --- a/k8s.yaml +++ b/k8s.yaml @@ -2,7 +2,7 @@ apiVersion: v1 kind: Namespace metadata: - name: tracker + name: fit --- # The whole log is one JSON file, so the "database" is this volume. It is @@ -10,8 +10,8 @@ metadata: apiVersion: v1 kind: PersistentVolumeClaim metadata: - name: tracker-data-pvc - namespace: tracker + name: fit-data-pvc + namespace: fit spec: accessModes: - ReadWriteOnce @@ -23,11 +23,11 @@ spec: apiVersion: v1 kind: Service metadata: - name: tracker-svc - namespace: tracker + name: fit-svc + namespace: fit spec: selector: - app: tracker + app: fit ports: - port: 80 targetPort: 8080 @@ -37,8 +37,8 @@ spec: apiVersion: apps/v1 kind: Deployment metadata: - name: tracker-dep - namespace: tracker + name: fit-dep + namespace: fit spec: replicas: 1 # Two pods would both hold the file in memory and overwrite each other's @@ -48,11 +48,11 @@ spec: type: Recreate selector: matchLabels: - app: tracker + app: fit template: metadata: labels: - app: tracker + app: fit spec: imagePullSecrets: - name: harborcred @@ -64,8 +64,8 @@ spec: # write the file it exists to hold. fsGroup: 65534 containers: - - name: tracker - image: harbor.scottyah.com/scottyah/tracker:latest + - name: fit + image: harbor.scottyah.com/scottyah/fit:latest imagePullPolicy: Always ports: - containerPort: 8080 @@ -75,7 +75,7 @@ spec: - name: BASE_URL value: "https://fit.scottyah.com" - name: DATA_PATH - value: "/data/tracker.json" + value: "/data/fit.json" # Every date in the app is a local date, so this decides when the # day rolls over. - name: TZ_NAME @@ -113,7 +113,7 @@ spec: volumes: - name: data persistentVolumeClaim: - claimName: tracker-data-pvc + claimName: fit-data-pvc --- # The app has no login of its own; this is the only thing between the log and @@ -121,22 +121,22 @@ spec: apiVersion: traefik.io/v1alpha1 kind: Middleware metadata: - name: tracker-basic-auth - namespace: tracker + name: fit-basic-auth + namespace: fit spec: basicAuth: - secret: tracker-basic-auth + secret: fit-basic-auth --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: tracker-ingress - namespace: tracker + name: fit-ingress + namespace: fit annotations: traefik.ingress.kubernetes.io/router.entrypoints: websecure traefik.ingress.kubernetes.io/router.tls: "true" - traefik.ingress.kubernetes.io/router.middlewares: tracker-tracker-basic-auth@kubernetescrd + traefik.ingress.kubernetes.io/router.middlewares: fit-fit-basic-auth@kubernetescrd spec: ingressClassName: traefik tls: @@ -151,6 +151,6 @@ spec: pathType: Prefix backend: service: - name: tracker-svc + name: fit-svc port: number: 80