From d82db48e4be5a2eccb5735855900a637f4c22982 Mon Sep 17 00:00:00 2001 From: Steve Dudenhoeffer Date: Tue, 21 Jul 2026 23:31:10 -0400 Subject: [PATCH 1/2] Rough edges: mobile 100dvh, session-expiry in chat, sticky error toasts, journal date filter (#85) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The safe, self-contained wins from the #85 bundle: - **Mobile viewport.** The editor used `100vh`, which on mobile Safari/Chrome is the LARGEST viewport (URL bar hidden), so with the bar visible the canvas bottom and Fit button were pushed under the browser chrome. `100dvh` fixes it. - **Session expiry mid-chat.** A 401 during a chat turn was mapped to "the assistant is not available right now" — sending the user to debug a config problem that isn't there. Now it says the session expired and redirects to /login, preserving the path, like the rest of the app treats 401. - **Error toasts persist.** Error toasts were the primary report that a mutation failed, yet auto-dismissed at 4s with no way to retrieve them — look away and it's gone. Errors now stay until dismissed; info toasts still time out; both get a close button. - **Journal date filter.** `from`/`to` existed in the API and JournalFilter but had no UI, so "show me last spring" was unreachable. Added two date inputs (with a Clear) that feed the existing filter. No backend change. Deferred to their own follow-ups (too big for this bundle, or need a decision): revision-history retention/pruning, the agent toolbox's missing corrective tools, plop-level journal entries from the UI, and the editor's max-width cap (a layout call worth confirming rather than changing blind). The DESIGN.md API-listing drift the issue mentioned was already fixed in #89. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ --- web/src/components/ui/toast.tsx | 23 +++++++++++---- web/src/editor/JournalPanel.tsx | 45 +++++++++++++++++++++++++++++- web/src/lib/agent.ts | 9 ++++++ web/src/pages/GardenEditorPage.tsx | 5 +++- 4 files changed, 75 insertions(+), 7 deletions(-) diff --git a/web/src/components/ui/toast.tsx b/web/src/components/ui/toast.tsx index 3d41450..5ccb925 100644 --- a/web/src/components/ui/toast.tsx +++ b/web/src/components/ui/toast.tsx @@ -32,21 +32,34 @@ export const toast = { // Param is `item`, not `toast`, so it doesn't shadow the module's `toast` export. function ToastItem({ item }: { item: Toast }) { const dismiss = useToastStore((s) => s.dismiss) + const isError = item.tone === 'error' useEffect(() => { + // Error toasts are the primary report that a mutation failed, so they do NOT + // auto-dismiss — a user who looked away at second 4 would otherwise lose the + // only notice, with nothing to retrieve (#85). Info toasts still time out. + if (isError) return const t = setTimeout(() => dismiss(item.id), 4000) return () => clearTimeout(t) - }, [item.id, dismiss]) + }, [item.id, dismiss, isError]) return (
- {item.message} + {item.message} +
) } diff --git a/web/src/editor/JournalPanel.tsx b/web/src/editor/JournalPanel.tsx index d37c193..15d7b2d 100644 --- a/web/src/editor/JournalPanel.tsx +++ b/web/src/editor/JournalPanel.tsx @@ -42,7 +42,15 @@ export function JournalPanel({ scopeObjectId: number | null onScopeChange: (id: number | null) => void }) { - const filter = scopeObjectId != null ? { objectId: scopeObjectId } : {} + // Date-range narrowing (#85): the backend and JournalFilter already supported + // from/to; they just had no UI. Empty inputs don't filter. + const [from, setFrom] = useState('') + const [to, setTo] = useState('') + const filter = { + ...(scopeObjectId != null ? { objectId: scopeObjectId } : {}), + ...(from ? { from } : {}), + ...(to ? { to } : {}), + } const journal = useJournal(gardenId, filter) const entries = journal.data?.pages.flatMap((p) => p.entries) ?? [] const scopedObject = objects.find((o) => o.id === scopeObjectId) ?? null @@ -70,6 +78,41 @@ export function JournalPanel({ )} +
+ + + {(from || to) && ( + + )} +
+ {canEdit && ( +

{garden.name} -- 2.54.0 From 757ac7394d852f8bfb2c7b45041b191afe69a86f Mon Sep 17 00:00:00 2001 From: Steve Dudenhoeffer Date: Tue, 21 Jul 2026 23:53:18 -0400 Subject: [PATCH 2/2] Address Gadfly findings on #85 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PublicGardenPage had the same 100vh mobile bug I fixed in the editor — 100dvh there too, so the fix is consistent across both full-height pages. - Cap the toast stack at 4 (drop oldest). Now that error toasts don't auto- dismiss, a burst of failures could otherwise grow the stack unbounded and push the newest — the one that just happened — off-screen. - Hoist the From/To date-input styling to a shared dateInputClass const so the two don't drift. Not taken: reusing safeRedirectPath for the 401 redirect (it validates an incoming redirect param, it doesn't build the outgoing one — the two uses don't overlap), and refactoring the three-branch filter spread (it's clear at three fields; a helper earns its keep when there are more). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ --- web/src/components/ui/toast.tsx | 9 ++++++++- web/src/editor/JournalPanel.tsx | 9 +++++++-- web/src/pages/PublicGardenPage.tsx | 2 +- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/web/src/components/ui/toast.tsx b/web/src/components/ui/toast.tsx index 5ccb925..fffa22d 100644 --- a/web/src/components/ui/toast.tsx +++ b/web/src/components/ui/toast.tsx @@ -17,9 +17,16 @@ interface ToastState { let nextId = 1 +// Error toasts no longer auto-dismiss (#85), so a burst of failures could grow +// the stack without bound and push older ones off-screen. Cap it: keep the most +// recent MAX_TOASTS and drop the oldest, so the newest — the one that just +// happened — is always visible. +const MAX_TOASTS = 4 + export const useToastStore = create((set) => ({ toasts: [], - push: (message, tone = 'info') => set((s) => ({ toasts: [...s.toasts, { id: nextId++, message, tone }] })), + push: (message, tone = 'info') => + set((s) => ({ toasts: [...s.toasts, { id: nextId++, message, tone }].slice(-MAX_TOASTS) })), dismiss: (id) => set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) })), })) diff --git a/web/src/editor/JournalPanel.tsx b/web/src/editor/JournalPanel.tsx index 15d7b2d..fba7d63 100644 --- a/web/src/editor/JournalPanel.tsx +++ b/web/src/editor/JournalPanel.tsx @@ -16,6 +16,11 @@ import { import type { EditorObject } from './types' import { objectDisplayName } from './kinds' +// Shared styling for the small From/To date inputs, so the two stay in step and +// don't drift from each other. +const dateInputClass = + 'rounded-md border border-border bg-surface px-1.5 py-1 text-fg outline-none focus-visible:ring-2 focus-visible:ring-accent/40' + /** * The garden's journal: write an entry, read the season back. * @@ -86,7 +91,7 @@ export function JournalPanel({ value={from} max={to || undefined} onChange={(e) => setFrom(e.target.value)} - className="rounded-md border border-border bg-surface px-1.5 py-1 text-fg outline-none focus-visible:ring-2 focus-visible:ring-accent/40" + className={dateInputClass} /> {(from || to) && ( diff --git a/web/src/pages/PublicGardenPage.tsx b/web/src/pages/PublicGardenPage.tsx index 19cd028..7900c00 100644 --- a/web/src/pages/PublicGardenPage.tsx +++ b/web/src/pages/PublicGardenPage.tsx @@ -53,7 +53,7 @@ export function PublicGardenPage() { } return ( -
+

{garden.name} -- 2.54.0