From 32cbe36817c91cfaff67cdcb528c2a2370445166 Mon Sep 17 00:00:00 2001
From: Steve Dudenhoeffer
Date: Tue, 21 Jul 2026 01:54:11 -0400
Subject: [PATCH] Address Gadfly review on the journal UI
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The prop names were backwards. `canEdit` meant "may delete" and `canWrite` meant
"may rewrite the text" — which is the opposite of how either word reads, on a
component where the distinction is the entire permission model. They are now
canDelete and canRewrite.
An open edit draft never re-synced with its entry, so a refetch after someone
else's edit left a stale draft and a Cancel that restored the wrong text. It now
resyncs when the entry changes underneath, but only while not editing, so a
background refetch can't clobber what's being typed — the version guard is what
catches a genuine collision.
A 409 from an edit had no handling, so the component kept its stale version and
every retry conflicted again: a button that just doesn't work. A conflict now
invalidates, so the fresh version arrives.
Saving an empty edit returned silently, which also reads as a broken button. It
says why nothing happened, and points at Delete if that's what was meant. A
failed delete's error now clears on retry rather than making a second attempt
look like a second failure.
formatObservedAt let out-of-range parts roll over — 2026-13-45 rendering as
February 2027, confidently wrong. It shows the raw string instead.
Also: the scope select guards against a non-numeric value becoming a NaN
objectId and a cryptic 400; the journal total is memoized rather than
spread-and-reduced every render; EditorRail's doc comment no longer calls the
journal "later" now that it's here; and a static class list stopped being split
across two cn() arguments for no reason.
Co-Authored-By: Claude Opus 4.8 (1M context)
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
---
web/src/editor/EditorRail.tsx | 6 ++--
web/src/editor/JournalPanel.tsx | 51 ++++++++++++++++++++----------
web/src/lib/journal.ts | 12 +++++--
web/src/pages/GardenEditorPage.tsx | 6 +++-
4 files changed, 53 insertions(+), 22 deletions(-)
diff --git a/web/src/editor/EditorRail.tsx b/web/src/editor/EditorRail.tsx
index 2aa76b4..a836274 100644
--- a/web/src/editor/EditorRail.tsx
+++ b/web/src/editor/EditorRail.tsx
@@ -4,9 +4,9 @@ import { cn } from '@/lib/cn'
/**
* The editor's side rail, and the answer to "four things want one rail".
*
- * The inspector, history, and (later) the journal and chat panel all want the
- * same strip of screen. Rather than each bolting on its own chrome, they are
- * tabs in one rail — which keeps the canvas one width instead of a different
+ * The inspector, history, journal, and (later) the chat panel all want the same
+ * strip of screen. Rather than each bolting on its own chrome, they are tabs in
+ * one rail — which keeps the canvas one width instead of a different
* width per panel, and means adding the journal or chat is adding a tab.
*
* Two constraints shaped it:
diff --git a/web/src/editor/JournalPanel.tsx b/web/src/editor/JournalPanel.tsx
index a9827e9..d37c193 100644
--- a/web/src/editor/JournalPanel.tsx
+++ b/web/src/editor/JournalPanel.tsx
@@ -4,7 +4,6 @@ import { Button } from '@/components/ui/Button'
import { TextArea } from '@/components/ui/TextArea'
import { TextField } from '@/components/ui/TextField'
import { errorMessage } from '@/lib/api'
-import { cn } from '@/lib/cn'
import {
formatObservedAt,
today,
@@ -55,7 +54,10 @@ export function JournalPanel({
{objects.length > 0 && (