Grow journal: time-stamped notes on gardens, beds and plantings (#52)
Build image / build-and-push (push) Successful in 7s
Gadfly review (reusable) / review (pull_request) Canceled after 8m27s
Adversarial Review (Gadfly) / review (pull_request) Canceled after 8m27s

"Take notes through the season on each bed and plant." There is already
free-text notes on gardens, objects and plants, but it is a single mutable
field: writing "powdery mildew on the west bed" overwrites what you wrote in
June. The distinction worth keeping is that notes says what this thing IS, while
a journal entry says what HAPPENED, and when. Both stay.

garden_id is NOT NULL even when an entry is about a bed or a single plop, and
that is the whole trick: permission checks reuse requireGardenRole unchanged and
no second ACL path is invented. object_id/planting_id narrow the target; the
garden always anchors it. Because the garden anchors permission, the target is
checked to actually live in that garden — otherwise a valid object id from
someone else's garden would be storable here and then leak through the list
read. A plop-level entry that also names an object must name the right one, or
the two filters would disagree about what an entry is about.

observed_at is distinct from created_at: you write up Saturday's observations on
Sunday, and Saturday is the date that matters. Listing orders by observation,
newest first, with id breaking ties inside a day.

Roles follow the existing shape. Editors write, viewers read, strangers get
ErrNotFound. An author may edit their own entries; the garden owner may DELETE
any entry in their garden but may not edit one — rewriting somebody else's
observation under their name is a different act from removing it.

Deliberately not wired into the revision history, per the decision on #52:
entries are already append-shaped and individually versioned, and undoing a note
is just deleting it. There's a test asserting journal writes produce no change
sets, so that decision can't quietly reverse itself later.

Closes #52

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
This commit is contained in:
2026-07-21 01:27:30 -04:00
co-authored by Claude Opus 4.8
parent 2a8fe24766
commit 635d5e3770
9 changed files with 881 additions and 6 deletions
+26
View File
@@ -157,6 +157,32 @@ type Revision struct {
After *string `json:"after,omitempty"`
}
// JournalEntry is one time-stamped observation: what happened, and when.
// Distinct from the mutable `notes` on a garden/object/plant, which says what
// the thing IS — writing a new note overwrites the old one, while entries
// accumulate.
//
// GardenID is set even when the entry is about a bed or a plop, so permission
// checks reuse the garden role machinery unchanged; ObjectID/PlantingID narrow
// the target without inventing a second ACL path.
type JournalEntry struct {
ID int64 `json:"id"`
GardenID int64 `json:"gardenId"`
ObjectID *int64 `json:"objectId,omitempty"`
PlantingID *int64 `json:"plantingId,omitempty"`
AuthorID int64 `json:"authorId"`
Body string `json:"body"`
// ObservedAt is when it happened ('YYYY-MM-DD'), which is not when it was
// written down: you write up Saturday's observations on Sunday.
ObservedAt string `json:"observedAt"`
Version int64 `json:"version"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
// AuthorName is resolved for display by the service, never persisted.
AuthorName string `json:"authorName,omitempty"`
}
// RevertConflict reports one entity a revert deliberately left alone, because
// reverting it would have discarded a change made after the change set being
// reverted. The rest of the change set still reverts.