"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.
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.
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
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.
Design: DESIGN.md
Context
"Take notes through the season on each bed / plant." There is free-text
noteson 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:
notesis what this thing is; a journal entry is what happened, and when. Keep both.Schema
garden_idis NOT NULL even when the entry is about a bed or a plop. That is the whole trick: permission checks reuserequireGardenRoleunchanged and no new ACL path is invented.object_id/planting_idnarrow the target; the garden always anchors it.observed_atis distinct fromcreated_at— you write up Saturday's observations on Sunday, and the date that matters is Saturday's.Scope
0008_journal.sql.internal/store/journal.go+internal/service/journal.go: CRUD, version-guarded. List by garden with optionalobjectId/plantingId/ date-range filters,observed_at DESC.observed_atdefaults to today when omitted; body length-capped like other free text.GET,POST /gardens/:id/journal,PATCH,DELETE /journal/:id.Design notes
plants.notesalready covers the simple case. If cross-garden variety observations turn out to matter, that's a separate table, not a nullablegarden_id.recordRevisioninto this service.Out of scope
Key files
internal/store/migrations/·internal/service/shares.go(role rules to mirror) ·internal/api/api.goDependencies
None.
Acceptance criteria
ErrNotFound.observed_atbackdating works and orders correctly against entries written later.go test ./...green.