Grow journal: time-stamped notes on gardens, beds and plantings #52

Closed
opened 2026-07-21 04:18:13 +00:00 by steve · 0 comments
Owner

Design: DESIGN.md

Context

"Take notes through the season on each bed / plant." There is free-text notes on gardens, objects and plants today, but it is a single mutable field — writing "powdery mildew on the west bed" overwrites what you wrote in June. A season log needs entries, each with its own date.

The distinction that matters: notes is what this thing is; a journal entry is what happened, and when. Keep both.

Schema

CREATE TABLE journal_entries (
    id          INTEGER PRIMARY KEY,
    garden_id   INTEGER NOT NULL REFERENCES gardens (id)        ON DELETE CASCADE,
    object_id   INTEGER          REFERENCES garden_objects (id) ON DELETE CASCADE,
    planting_id INTEGER          REFERENCES plantings (id)      ON DELETE CASCADE,
    author_id   INTEGER NOT NULL REFERENCES users (id)          ON DELETE CASCADE,
    body        TEXT    NOT NULL,
    observed_at TEXT    NOT NULL,                               -- 'YYYY-MM-DD'
    version     INTEGER NOT NULL DEFAULT 1,
    created_at  TEXT    NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
    updated_at  TEXT    NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
);
CREATE INDEX idx_journal_garden ON journal_entries (garden_id, observed_at DESC);
CREATE INDEX idx_journal_object ON journal_entries (object_id);

garden_id is NOT NULL even when the entry is about a bed or a plop. That is the whole trick: permission checks reuse requireGardenRole unchanged and no new ACL path is invented. object_id/planting_id narrow the target; the garden always anchors it.

observed_at is distinct from created_at — you write up Saturday's observations on Sunday, and the date that matters is Saturday's.

Scope

  • Migration 0008_journal.sql.
  • internal/store/journal.go + internal/service/journal.go: CRUD, version-guarded. List by garden with optional objectId / plantingId / date-range filters, observed_at DESC.
  • Editor role to write; viewer role to read. An author may edit their own entries; the garden owner may delete any (consistent with how sharing already works).
  • observed_at defaults to today when omitted; body length-capped like other free text.
  • API: GET,POST /gardens/:id/journal, PATCH,DELETE /journal/:id.

Design notes

  • Entries about a plant across gardens ("this variety always bolts early") don't fit here — they're catalog-level, and plants.notes already covers the simple case. If cross-garden variety observations turn out to matter, that's a separate table, not a nullable garden_id.
  • Journal entries stay out of #48's revision history (decided 2026-07-21). They're already append-shaped and individually versioned, and undoing a note is just deleting it — running them through change sets would add a layer that buys nothing. Don't wire recordRevision into this service.

Out of scope

  • Photos/attachments. Wanted eventually; needs a storage decision (blob column vs filesystem vs object store) that shouldn't be smuggled in here.
  • Full-text search across entries.
  • Weather auto-capture.
  • Revision-history integration, per above.

Key files

internal/store/migrations/ · internal/service/shares.go (role rules to mirror) · internal/api/api.go

Dependencies

None.

Acceptance criteria

  • An entry can be attached to a garden, a bed, or a single plop, and lists correctly under each.
  • Deleting a bed removes its entries but leaves the garden's own entries intact.
  • A viewer can read and cannot write; a stranger gets ErrNotFound.
  • observed_at backdating works and orders correctly against entries written later.
  • go test ./... green.
Design: [DESIGN.md](https://gitea.stevedudenhoeffer.com/steve/pansy/src/branch/main/DESIGN.md) ## Context "Take notes through the season on each bed / plant." There is free-text `notes` on gardens, objects and plants today, but it is a single mutable field — writing "powdery mildew on the west bed" overwrites what you wrote in June. A season log needs entries, each with its own date. The distinction that matters: `notes` is *what this thing is*; a journal entry is *what happened, and when*. Keep both. ## Schema ```sql CREATE TABLE journal_entries ( id INTEGER PRIMARY KEY, garden_id INTEGER NOT NULL REFERENCES gardens (id) ON DELETE CASCADE, object_id INTEGER REFERENCES garden_objects (id) ON DELETE CASCADE, planting_id INTEGER REFERENCES plantings (id) ON DELETE CASCADE, author_id INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, body TEXT NOT NULL, observed_at TEXT NOT NULL, -- 'YYYY-MM-DD' version INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) ); CREATE INDEX idx_journal_garden ON journal_entries (garden_id, observed_at DESC); CREATE INDEX idx_journal_object ON journal_entries (object_id); ``` **`garden_id` is NOT NULL even when the entry is about a bed or a plop.** That is the whole trick: permission checks reuse `requireGardenRole` unchanged and no new ACL path is invented. `object_id`/`planting_id` narrow the target; the garden always anchors it. `observed_at` is distinct from `created_at` — you write up Saturday's observations on Sunday, and the date that matters is Saturday's. ## Scope - [ ] Migration `0008_journal.sql`. - [ ] `internal/store/journal.go` + `internal/service/journal.go`: CRUD, version-guarded. List by garden with optional `objectId` / `plantingId` / date-range filters, `observed_at DESC`. - [ ] Editor role to write; viewer role to read. An author may edit their own entries; the garden owner may delete any (consistent with how sharing already works). - [ ] `observed_at` defaults to today when omitted; body length-capped like other free text. - [ ] API: `GET,POST /gardens/:id/journal`, `PATCH,DELETE /journal/:id`. ## Design notes - Entries about a **plant across gardens** ("this variety always bolts early") don't fit here — they're catalog-level, and `plants.notes` already covers the simple case. If cross-garden variety observations turn out to matter, that's a separate table, not a nullable `garden_id`. - **Journal entries stay out of #48's revision history** (decided 2026-07-21). They're already append-shaped and individually versioned, and undoing a note is just deleting it — running them through change sets would add a layer that buys nothing. Don't wire `recordRevision` into this service. ## Out of scope - Photos/attachments. Wanted eventually; needs a storage decision (blob column vs filesystem vs object store) that shouldn't be smuggled in here. - Full-text search across entries. - Weather auto-capture. - Revision-history integration, per above. ## Key files `internal/store/migrations/` · `internal/service/shares.go` (role rules to mirror) · `internal/api/api.go` ## Dependencies None. ## Acceptance criteria - An entry can be attached to a garden, a bed, or a single plop, and lists correctly under each. - Deleting a bed removes its entries but leaves the garden's own entries intact. - A viewer can read and cannot write; a stranger gets `ErrNotFound`. - `observed_at` backdating works and orders correctly against entries written later. - `go test ./...` green.
steve added the backend label 2026-07-21 04:18:13 +00:00
steve closed this issue 2026-07-21 05:40:17 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: steve/pansy#52