steveandClaude Opus 4.8 782f2394b2
Build image / build-and-push (push) Successful in 16s
Gadfly review (reusable) / review (pull_request) Successful in 5m53s
Adversarial Review (Gadfly) / review (pull_request) Successful in 5m53s
Units correctness: exact imperial entry, garden grid at dimension scale
Three compounding defects in the measurement layer (#47), measured against the
live Home garden rather than estimated.

Entry was quantized to whole centimeters even though every measurement column is
REAL and the server only range-checks. A 12in grid stored 30cm — an 11.811in
grid, short by 0.189in per cell, and because the error is systematic it
accumulates: 4.61in of drift across a 24ft garden. cmFromFtIn / cmFromMeters /
cmFromSpacing now convert exactly, rounding only at 1e-6 cm to clear IEEE-754
dust (12 x 2.54 is 30.479999999999997 in binary). Display-side rounding is
untouched, so cm quantization still can't show through in a field.

The garden grid was entered at spacing scale: "Grid size (in)" sat directly under
"Width (ft)", two adjacent numeric fields at two different scales distinguished
only by a suffix. Home ended up on a 3cm grid, almost certainly "1" typed meaning
one foot. The garden grid is a layout concern, so it moves to dimension scale and
reads "Garden grid (ft)"; the bed grid in the object inspector is a plant-spacing
concern and deliberately stays at cm/in. A sub-10cm garden grid now draws a soft
warning rather than being silently accepted.

Snap could stay active while the grid stopped drawing: the canvas faded the grid
out below 6px per cell, so Home ran with snapToGrid on, no grid drawn, and no
snapping you could feel. Replaced the fade with coarsening — visibleGridStepCm
picks the smallest multiple of the true grid (2x, 5x, 10x...) whose cells are
legible, so every line drawn is still a real grid line and something aligned to
the grid is always visible. When the drawn step isn't the true grid the canvas
says so; a degenerate grid it can't draw at all says that instead of nothing.

Closes #47

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
2026-07-21 00:38:06 -04:00

pansy

Self-hostable garden planner: drag beds, bags, and containers onto a real-scale field, click into them to place freeform plops of plants, and zoom out to see what's planted where. Go backend + React frontend, one static binary.

See DESIGN.md for the architecture. Work is tracked in this repo's issues — start from the tracking epic.

Quickstart

Prerequisites: Go 1.26+, Node 20+.

Develop

Run the Go API and the Vite dev server together (Vite proxies /api → the API):

make dev

Then open http://localhost:5173. Or run the two halves in separate terminals for independent restarts:

make dev-api   # Go API on :8080
make dev-web   # Vite dev server on :5173

Build & run

Produce the single static binary with the web build embedded, then run it:

make build
./pansy

Open http://localhost:8080 — one process serves both the JSON API and the app.

Test

make test

Configuration

All configuration is via environment variables; every value has a default, so ./pansy runs with none set.

Variable Default Description
PANSY_PORT 8080 TCP port the HTTP server listens on.
PANSY_DB ./pansy.db SQLite database file path (created if absent).
PANSY_BASE_URL (empty) Externally-visible base URL; used to derive the OIDC redirect URI.
PANSY_REGISTRATION open open or closed — gates local self-service signup.
PANSY_LOCAL_AUTH true Enable local password auth. Set false for pure-OIDC.
PANSY_OIDC_ISSUER (empty) OIDC issuer/discovery URL (Authentik). Enables SSO when set.
PANSY_OIDC_CLIENT_ID (empty) OIDC client ID.
PANSY_OIDC_CLIENT_SECRET (empty) OIDC client secret.
PANSY_OIDC_BUTTON_LABEL Sign in with Authentik Label for the OIDC button on the login page.
PANSY_TRUSTED_PROXIES (none) Comma-separated proxy CIDRs/IPs to trust for client-IP resolution.

Local email/password auth is live (POST /api/v1/auth/register, /auth/login, /auth/logout, GET /auth/me, GET /auth/providers); the session is an HttpOnly cookie (Secure when PANSY_BASE_URL is https). The first account registered becomes admin, and it may register even when PANSY_REGISTRATION=closed to bootstrap the instance.

OIDC (Authentik-first) is live too: set PANSY_OIDC_ISSUER, PANSY_OIDC_CLIENT_ID, PANSY_OIDC_CLIENT_SECRET, and PANSY_BASE_URL (needed for the redirect URI). Register PANSY_BASE_URL + /api/v1/auth/oidc/callback as the redirect URI in your IdP. GET /auth/oidc/login starts an authorization-code + PKCE flow; first login provisions a user just-in-time (a matching verified email links to an existing local account instead of duplicating it). Provider discovery is lazy, so a briefly-unreachable IdP never blocks startup or local auth. Set PANSY_LOCAL_AUTH=false for pure-Authentik deployments (local register/login are then rejected and hidden from /auth/providers).

Docker & deployment

CI (.gitea/workflows/build-image.yml) builds the single-binary image and pushes it to the Gitea registry on every branch push:

Ref Tag
main gitea.stevedudenhoeffer.com/steve/pansy:latest
any other branch gitea.stevedudenhoeffer.com/steve/pansy:<branch-name>
every build gitea.stevedudenhoeffer.com/steve/pansy:sha-<short> (immutable; use to pin)

The image runs as a non-root user, serves on :8080, and stores the SQLite database on the /data volume. Run it directly:

docker run -d --name pansy \
  -p 8080:8080 \
  -v pansy-data:/data \
  gitea.stevedudenhoeffer.com/steve/pansy:latest

Or as a Komodo/Compose stack:

services:
  pansy:
    image: gitea.stevedudenhoeffer.com/steve/pansy:${PANSY_TAG:-latest}
    ports:
      - "8080:8080"
    volumes:
      - pansy-data:/data
    environment:
      PANSY_BASE_URL: https://pansy.example.com
      # PANSY_OIDC_ISSUER: ...   # once auth (#5) lands
    restart: unless-stopped
volumes:
  pansy-data:

Pin PANSY_TAG to a sha-<short> tag for reproducible deploys, or leave it at latest to track main.

S
Description
No description provided
Readme
2.6 MiB
Languages
Go 62.7%
TypeScript 36.8%
Dockerfile 0.2%
CSS 0.1%
Makefile 0.1%