#52 stores entries; this makes writing one cheap enough that it actually happens. Notes get written standing in the garden holding a phone, usually one-handed — if it takes more than a couple of taps from looking at a bed to typing a sentence, the log stays empty.
Two taps from a selected bed to typing
The journal is a tab in the rail #49 settled. Selecting a bed puts a 📝 Add note button in the inspector, which scopes the journal to that bed and switches to it — and the composer is already open in there rather than behind a second "add" button. Select bed → tap → type.
The observed date defaults to today in the viewer's timezone, not UTC ("today" means the day you're standing in the garden) and stays editable, because you write up Saturday's observations on Sunday.
Discoverability is the other half
A log you have to remember exists is a log nobody reads. So the inspector button carries the note count for that bed, and the Journal tab carries the garden's total — a garden with a season's notes in it no longer looks identical to an empty one.
Those counts come from their own endpoint (GET /gardens/:id/journal/counts), deliberately: the indicator has to work while the journal panel is closed, which is precisely when the entry list hasn't loaded. One grouped query rather than one per bed.
The rest
Filter to one bed via a dropdown, or read the whole garden.
Edit your own entries inline; delete your own, or any if you own the garden — matching the service, where rewriting someone else's observation under their name is a different act from removing it. A viewer sees entries and counts but no composer.
The empty state says what the journal is for rather than "no entries" — it starts empty for a whole season's worth of gardens, and the reason to start is the thing worth saying.
Paging failures surface rather than silently disabling the button.
Verification
New API test for the counts endpoint (per-object tallies, garden-level under key "0", 401 when anonymous). go test ./..., tsc --noEmit, npm run build green.
I'll exercise the real loop against the live deploy after this and #54 merge — write a note on a bed from the inspector, confirm it appears and the count updates.
Closes #53. Phase 1 of #58.
#52 stores entries; this makes writing one **cheap enough that it actually happens.** Notes get written standing in the garden holding a phone, usually one-handed — if it takes more than a couple of taps from looking at a bed to typing a sentence, the log stays empty.
### Two taps from a selected bed to typing
The journal is a tab in the rail #49 settled. Selecting a bed puts a **`📝 Add note`** button in the inspector, which scopes the journal to that bed and switches to it — and the composer is already open in there rather than behind a second "add" button. Select bed → tap → type.
The observed date defaults to **today in the viewer's timezone, not UTC** ("today" means the day you're standing in the garden) and stays editable, because you write up Saturday's observations on Sunday.
### Discoverability is the other half
A log you have to remember exists is a log nobody reads. So the inspector button carries the note count for that bed, and the Journal tab carries the garden's total — a garden with a season's notes in it no longer looks identical to an empty one.
Those counts come from **their own endpoint** (`GET /gardens/:id/journal/counts`), deliberately: the indicator has to work while the journal panel is closed, which is precisely when the entry list hasn't loaded. One grouped query rather than one per bed.
### The rest
- Filter to one bed via a dropdown, or read the whole garden.
- Edit your own entries inline; delete your own, or any if you own the garden — matching the service, where rewriting someone else's observation under their name is a different act from removing it. A viewer sees entries and counts but no composer.
- The empty state says **what the journal is for** rather than "no entries" — it starts empty for a whole season's worth of gardens, and the reason to start is the thing worth saying.
- Paging failures surface rather than silently disabling the button.
### Verification
New API test for the counts endpoint (per-object tallies, garden-level under key `"0"`, 401 when anonymous). `go test ./...`, `tsc --noEmit`, `npm run build` green.
I'll exercise the real loop against the live deploy after this and #54 merge — write a note on a bed from the inspector, confirm it appears and the count updates.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
Live status board. Findings are posted in each model's own comment. Advisory only — does not block merge.
<!-- gadfly-status-board -->
## 🪰 Gadfly — live review status
3/5 reviewers finished · updated 2026-07-21 05:54:15Z
#### `claude-code/sonnet` · claude-code — ✅ done
- ✅ **security** — No material issues found
- ✅ **correctness** — No material issues found
- ✅ **maintainability** — Minor issues
- ✅ **performance** — No material issues found
- ✅ **error-handling** — Minor issues
#### `glm-5.2:cloud` · ollama-cloud — ✅ done
- ✅ **security** — No material issues found
- ✅ **correctness** — No material issues found
- ✅ **maintainability** — Minor issues
- ✅ **performance** — No material issues found
- ✅ **error-handling** — Minor issues
#### `kimi-k2.6:cloud` · ollama-cloud — ✅ done
- ✅ **security** — No material issues found
- ✅ **correctness** — No material issues found
- ✅ **maintainability** — Minor issues
- ✅ **performance** — No material issues found
- ✅ **error-handling** — Blocking issues found
#### `opencode/glm-5.2:cloud` · opencode — ⏳ 3/5 lenses
- 🔄 **security** — running
- ✅ **correctness** — No material issues found
- 🔄 **maintainability** — running
- ✅ **performance** — No material issues found
- ✅ **error-handling** — Minor issues
#### `opencode/kimi-k2.6:cloud` · opencode — ⏳ 4/5 lenses
- ✅ **security** — No material issues found
- 🔄 **correctness** — running
- ✅ **maintainability** — Minor issues
- ✅ **performance** — No material issues found
- ✅ **error-handling** — No material issues found
<sub>Live status board. Findings are posted in each model's own comment. Advisory only — does not block merge.</sub>
All fixed in 4d0f5b1. Three of these were more than polish.
The prop names were backwards
canEdit meant "may delete" and canWrite meant "may rewrite the text" — the opposite of how either word reads, on the one component where that distinction is the permission model. Anyone touching this later would have wired them the wrong way round and it would have looked right. Now canDelete and canRewrite, with the reasoning on the props.
An open draft never re-synced
A refetch after someone else's edit left a stale draft, and Cancel 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; this just keeps the idle case honest.
A 409 left the button permanently broken
useUpdateJournalEntry had no conflict handling, so the component kept its stale version and every retry conflicted again — indistinguishable from a button that does nothing. A conflict now invalidates so the fresh version arrives.
The rest
Saving an empty edit returned silently, which also reads as a broken button. It now says why nothing happened and points at Delete if that's what was meant.
A failed delete's error persisted, so a second attempt looked like a second failure. Cleared on retry.
formatObservedAt let out-of-range parts roll over — 2026-13-45 rendering as February 2027, confidently wrong. Shows the raw string instead.
Scope select guards a non-numeric value becoming a NaN objectId and a cryptic 400; journal total memoized; EditorRail's doc comment no longer calls the journal "later" now that it's here; static class list no longer split across two cn() args.
npm test, tsc --noEmit, npm run build, go test ./... green.
All fixed in `4d0f5b1`. Three of these were more than polish.
### The prop names were backwards
`canEdit` meant *"may delete"* and `canWrite` meant *"may rewrite the text"* — the opposite of how either word reads, on the one component where that distinction **is** the permission model. Anyone touching this later would have wired them the wrong way round and it would have looked right. Now `canDelete` and `canRewrite`, with the reasoning on the props.
### An open draft never re-synced
A refetch after someone else's edit left a stale draft, and Cancel 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; this just keeps the idle case honest.
### A 409 left the button permanently broken
`useUpdateJournalEntry` had no conflict handling, so the component kept its stale version and every retry conflicted again — indistinguishable from a button that does nothing. A conflict now invalidates so the fresh version arrives.
### The rest
- **Saving an empty edit returned silently**, which also reads as a broken button. It now says why nothing happened and points at Delete if that's what was meant.
- **A failed delete's error persisted**, so a second attempt looked like a second failure. Cleared on retry.
- **`formatObservedAt` let out-of-range parts roll over** — `2026-13-45` rendering as *February 2027*, confidently wrong. Shows the raw string instead.
- Scope select guards a non-numeric value becoming a `NaN` objectId and a cryptic 400; journal total memoized; `EditorRail`'s doc comment no longer calls the journal "later" now that it's here; static class list no longer split across two `cn()` args.
`npm test`, `tsc --noEmit`, `npm run build`, `go test ./...` green.
happens. Notes get written standing in the garden holding a phone, usually
one-handed — if it takes more than a couple of taps from looking at a bed to
typing a sentence, the log stays empty.
So the journal is a tab in the rail #49 settled, and selecting a bed puts a
"📝 Add note" button in the inspector that scopes the journal to that bed and
switches to it. The composer is already open in there rather than behind an
"add" button, which makes it two taps from a selected bed to typing.
The observed date defaults to today in the VIEWER's timezone, not UTC — "today"
means the day you are standing in the garden — and stays editable, because you
write up Saturday's observations on Sunday.
Discoverability is the other half. A log you have to remember exists is a log
nobody reads, so the inspector button carries the note count for that bed and
the Journal tab carries the garden's total: a garden with a season's notes in it
no longer looks identical to an empty one. Those counts come from their own
endpoint, deliberately — the indicator has to work while the journal panel is
closed, which is exactly when the entry list hasn't loaded.
Permissions match the service: anyone who can edit the garden can write, the
author can edit their own text, and the author or the garden owner can delete.
A viewer sees the entries and the count but no composer.
The empty state says what the journal is for rather than just "no entries",
since it starts empty for a whole season's worth of gardens and the reason to
start is the thing worth saying.
Closes#53
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
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) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #53. Phase 1 of #58.
#52 stores entries; this makes writing one cheap enough that it actually happens. Notes get written standing in the garden holding a phone, usually one-handed — if it takes more than a couple of taps from looking at a bed to typing a sentence, the log stays empty.
Two taps from a selected bed to typing
The journal is a tab in the rail #49 settled. Selecting a bed puts a
📝 Add notebutton in the inspector, which scopes the journal to that bed and switches to it — and the composer is already open in there rather than behind a second "add" button. Select bed → tap → type.The observed date defaults to today in the viewer's timezone, not UTC ("today" means the day you're standing in the garden) and stays editable, because you write up Saturday's observations on Sunday.
Discoverability is the other half
A log you have to remember exists is a log nobody reads. So the inspector button carries the note count for that bed, and the Journal tab carries the garden's total — a garden with a season's notes in it no longer looks identical to an empty one.
Those counts come from their own endpoint (
GET /gardens/:id/journal/counts), deliberately: the indicator has to work while the journal panel is closed, which is precisely when the entry list hasn't loaded. One grouped query rather than one per bed.The rest
Verification
New API test for the counts endpoint (per-object tallies, garden-level under key
"0", 401 when anonymous).go test ./...,tsc --noEmit,npm run buildgreen.I'll exercise the real loop against the live deploy after this and #54 merge — write a note on a bed from the inspector, confirm it appears and the count updates.
🤖 Generated with Claude Code
https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
🪰 Gadfly — live review status
3/5 reviewers finished · updated 2026-07-21 05:54:15Z
claude-code/sonnet· claude-code — ✅ doneglm-5.2:cloud· ollama-cloud — ✅ donekimi-k2.6:cloud· ollama-cloud — ✅ doneopencode/glm-5.2:cloud· opencode — ⏳ 3/5 lensesopencode/kimi-k2.6:cloud· opencode — ⏳ 4/5 lensesLive status board. Findings are posted in each model's own comment. Advisory only — does not block merge.
All fixed in
4d0f5b1. Three of these were more than polish.The prop names were backwards
canEditmeant "may delete" andcanWritemeant "may rewrite the text" — the opposite of how either word reads, on the one component where that distinction is the permission model. Anyone touching this later would have wired them the wrong way round and it would have looked right. NowcanDeleteandcanRewrite, with the reasoning on the props.An open draft never re-synced
A refetch after someone else's edit left a stale draft, and Cancel 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; this just keeps the idle case honest.
A 409 left the button permanently broken
useUpdateJournalEntryhad no conflict handling, so the component kept its stale version and every retry conflicted again — indistinguishable from a button that does nothing. A conflict now invalidates so the fresh version arrives.The rest
formatObservedAtlet out-of-range parts roll over —2026-13-45rendering as February 2027, confidently wrong. Shows the raw string instead.NaNobjectId and a cryptic 400; journal total memoized;EditorRail's doc comment no longer calls the journal "later" now that it's here; static class list no longer split across twocn()args.npm test,tsc --noEmit,npm run build,go test ./...green.c9bf7ee9a3to32cbe36817