Gadfly on #107:
- ClearBed reported a failure twice — ConfirmModal's inline Alert AND
useClearObject's own onError toast. Dropped the toast from useClearObject
(its only caller is that modal now), so the failure shows once, inline in
the dialog where the action is.
- LeaveGarden's onConfirm silently resolved (closing the dialog as if it
worked) if me.data was missing, relying on confirmDisabled to prevent it.
Throw instead, so a drift in that guard surfaces an error rather than a
fake success.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
Three tidy-ups from the audit's deferred list:
- Extracted a ConfirmModal primitive (message + Cancel/Confirm, owning the
busy lock + inline error) and folded the five hand-rolled confirm dialogs
onto it: DeleteGarden, LeaveGarden, DeletePlant, DeleteSeedLot, ClearBed.
Each is now just its message + mutation + labels. Bonus: ClearBed now
shows a failure inline instead of swallowing it. (CopyGarden stays on
Modal — it has a name field, not a plain confirm.)
- Removed components/PageStub.tsx — dead scaffolding, imported nowhere.
- The garden editor was squeezed into the max-w-5xl reading measure the
other pages use; the canvas routes (editor + public garden) now go
edge-to-edge on desktop, and the top bar matches so the brand aligns with
the editor's left edge. Mobile was already full-width, so it's unchanged.
Verified live: the Delete-garden confirm renders/cancels; the desktop editor
now uses the full viewport width. tsc + vitest + build green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
Closes#44. Two independent grids (garden + per-bed), each with a size and a snap toggle; snapping defaults off. See PR #45 for details.
Co-authored-by: Steve Dudenhoeffer <[email protected]>
Per-garden public read-only link: unauthenticated GET /api/v1/public/gardens/:token (no requireAuth, no OIDC), owner-only enable/rotate/disable, and a /g/$token page rendering GardenCanvas read-only. Review fixes: redact plant owner ids, Cache-Control: no-store, 400 on malformed body, shared resetTransient store action.
Closes#41.
Co-authored-by: Steve Dudenhoeffer <[email protected]>
Fixes from the PR #27 adversarial review (considered; not graded).
Correctness / error-handling
- Modal: focus once on mount and attach the keydown listener once (latest
onClose/busy via refs), so a parent re-render (e.g. a background refetch)
no longer re-fires focus() and steals it from the input being typed in.
- Modal takes a `busy` prop; while a save/delete is in flight, Escape and
backdrop clicks don't dismiss it (form/delete modals pass busy=pending),
so an action can't be interrupted mid-flight and lose its error.
- Form validates the *converted* centimeter values against the server's
[1cm, 100m] bounds, so sub-cm / over-100m sizes fail client-side with a
clear message instead of a generic server error.
- displayFromCm rounds imperial to 0.1 ft so an 8 ft garden (244 cm)
prefills as "8", not "8.01".
Maintainability
- Extracted ui/field.ts (useFieldId + fieldControlClass) shared by
TextField/TextArea/Select. Added a `danger` Button variant, used by the
delete modal; card edit/delete buttons gained focus-visible rings.
- useUpdateGarden takes the id in its mutation variables (no dummy 0 during
create). GardensPage shares a close handler; dropped a redundant zod
annotation; 409 rebase reuses dimString; renamed `del` -> `deletion`.
Verified in a browser: 8 ft round-trips to a prefilled "8"; a sub-cm value
is rejected client-side with the modal staying open. tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi
The /gardens page is now home base, backed by the #7 API.
- lib/units.ts: cm <-> meters/feet conversion + formatCm/formatDimensions
(metric "m", imperial "ft/in"). Minimal for #8; #9's geometry lib
consolidates.
- lib/gardens.ts: zod gardenSchema + useGardens/useCreateGarden/
useUpdateGarden/useDeleteGarden (mutations invalidate the list) and
conflictGarden() to pull the fresh row out of a 409.
- GardensPage: loading/empty/error states; empty state prompts a first
garden; responsive card grid (single column on phones).
- GardenCard: name, dimensions formatted per the garden's unit, notes
preview; body links into /gardens/:id, edit/delete kept out of the link.
- GardenFormModal: create/edit with a unit selector; dimensions are entered
in the chosen unit and converted to cm (switching units converts the
current values). A 409 rebases the form onto the server's fresh row.
- DeleteGardenModal: confirmation before removing.
- ui/: Modal (Escape/backdrop close), TextArea, Select.
Verified in a real browser against the embedded binary: create appears
without reload; edit persists (version 2), form prefilled from stored cm
converted back to the garden's unit; delete confirms then removes -> empty
state; an imperial 4 ft x 8 ft garden stores 122 x 244 cm and shows
"4' 0" x 8' 0"". tsc --noEmit clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi