Replace the UI with the Organic design handoff (docs/design_handoff_pansy_ui)
Build image / build-and-push (push) Successful in 31s
Gadfly review (reusable) / review (pull_request) Failing after 1s
Adversarial Review (Gadfly) / review (pull_request) Failing after 1s

The frontend is rebuilt screen by screen from the handoff: warm cream ground,
terracotta + sage accents, Caprasimo over Figtree, every control a pill. Same
React/Vite/TanStack stack and the same lib/ data layer; the presentation is new.

- Tokens: web/src/styles/index.css declares the handoff's styles.css variables
  through Tailwind's @theme under the same names; dark mode is those variables
  overridden on <html> by the handoff's pansy-theme.js, inlined in index.html
  so it runs before first paint. Lucide glyphs at stroke 2.75; a small pill kit
  (Button, Dialog, Field, Seg, Toggle, Tag, toast).
- Login / Register: the centered column over soft accent circles; OIDC button
  and signup footer still follow /auth/providers.
- Gardens: cards with a real SVG plot thumbnail (objects + plant-colored dots
  from /full), a `plan` tag for "<name> — <year>" copies, shares line, Open +
  share/copy/edit/delete; New garden / Share / Plan-a-season dialogs.
- Plants: monogram markers derived from the name (collision-resolved across the
  catalog — replaces emoji icons), category chips, expandable lot cards, the
  scan-packet flow as a two-step dialog that never auto-creates.
- Settings: Appearance (theme seg), Who gets in (read-only sign-in config),
  Garden assistant (self-saving toggle + chat/vision model fields), You.
- Editor: a new canvas with the prototype's pointer model (wheel-to-cursor,
  pinch about the centroid, 3″ snap, one PATCH per drop, semantic-zoom
  monograms/labels), plus corner resize handles; desktop three-card workspace
  (toolkit | plan | rail with Plot/Journal/History/Assistant) and, below 760px
  of container width, the phone chrome (header, peek panel, tool strip, mode
  bar). Seasons as a segmented control over the years with data plus plan
  copies; Undo re-reads history before reverting the newest step.
- Public read-only view and the register page restyled to match.
- GET /settings gains a read-only `auth` view (registration mode, local auth,
  OIDC issuer) so the Settings page can show what's in force.
- README / DESIGN.md / CLAUDE.md updated; @use-gesture/react dropped.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
2026-08-22 19:12:29 -04:00
co-authored by Claude Fable 5
parent 18b36870d4
commit 52b2c09a9e
111 changed files with 6621 additions and 7215 deletions
+28
View File
@@ -39,6 +39,27 @@ type settingsResponse struct {
// Effective is the configuration actually in force after layering settings
// over the environment.
Effective effectiveView `json:"effective"`
// Auth is the sign-in configuration, read-only (see authView).
Auth authView `json:"auth"`
}
// authView is the environment-driven sign-in configuration the Settings page
// shows under "Who gets in": PANSY_REGISTRATION, PANSY_LOCAL_AUTH and the OIDC
// issuer. It is reported so an admin can see what is in force without shell
// access; none of it is editable at runtime (auth policy deploys with the
// environment on purpose — see README). Only the issuer URL is exposed, never
// the client id or secret.
type authView struct {
// Registration is "open" or "closed" — whether local self-service signup is
// allowed. OIDC provisioning ignores it (the IdP gates access).
Registration string `json:"registration"`
// LocalAuth is whether email/password sign-in is offered at all.
LocalAuth bool `json:"localAuth"`
// OIDC is whether single sign-on is fully configured; OIDCIssuer is the
// discovery URL as configured (may be set while OIDC is still incomplete).
OIDC bool `json:"oidc"`
OIDCIssuer string `json:"oidcIssuer"`
OIDCLabel string `json:"oidcLabel"`
}
type effectiveView struct {
@@ -79,6 +100,13 @@ func (h *handlers) settingsPayload(c *gin.Context, st *domain.InstanceSettings)
VisionModel: vis.Model,
VisionReady: vis.Ready(),
},
Auth: authView{
Registration: h.cfg.Registration,
LocalAuth: h.cfg.LocalAuth,
OIDC: h.cfg.OIDCReady(),
OIDCIssuer: h.cfg.OIDC.Issuer,
OIDCLabel: h.cfg.OIDC.ButtonLabel,
},
}, nil
}
+19
View File
@@ -81,6 +81,25 @@ func TestSettingsInheritFromEnv(t *testing.T) {
if eff["hasApiKey"] != true || eff["agentLive"] != true {
t.Errorf("effective = %+v, want a key present and the agent live", eff)
}
// The read-only sign-in view the Settings page renders under "Who gets in".
// These come straight from the environment config, so the shape is what's
// asserted: a registration mode, a local-auth flag, and no secret material.
auth, ok := body["auth"].(map[string]any)
if !ok {
t.Fatalf("settings response has no auth view: %v", body)
}
if reg := auth["registration"]; reg != "open" && reg != "closed" {
t.Errorf("auth.registration = %v, want open or closed", reg)
}
if _, isBool := auth["localAuth"].(bool); !isBool {
t.Errorf("auth.localAuth = %v, want a bool", auth["localAuth"])
}
for _, k := range []string{"clientId", "clientSecret", "oidcClientSecret"} {
if _, present := auth[k]; present {
t.Errorf("auth view exposes %q — secrets must never leave the environment", k)
}
}
}
// TestSettingsUpdateSwapsTheRunner is the core of #79: changing settings takes