Files
famapp/docs/tasks/72-garden-plants.md
T
ginnoir 2fd0677c5f feat: add garden module infrastructure (task 70)
- Add MinIO service to compose.yaml with named volume
- Add generic /api/uploads POST+GET route with auth, 5 MB, image/* guards
- Add src/lib/minio.ts singleton client with ensureBucket helper
- Add garden Drizzle schema: containers, plants, care_logs, care_schedules, species_cache
- Register garden module in src/modules/index.ts and src/lib/db.ts
- Apply migration 0015_garden_schema.sql
- Add MINIO_* and PERENUAL_API_KEY to .env.example
- Add task briefs 70-75 for the full garden feature arc
- Add uploads.spec.ts E2E test (RED → GREEN on route auth guard)
2026-06-01 19:23:19 -05:00

5.3 KiB

72 — Garden plants

Goal

Implement full CRUD for individual plants. Each plant has a name, category, care notes, health status, up to 10 photos, an optional container assignment, and species data pulled from the Perenual API when the user searches for a known species.

Depends on

  • 70 (schema, MinIO, upload route), 71 (containers — container assignment picker)

Scope

Perenual API wrapper (src/modules/garden/server/species-lookup.ts)

Perenual (perenual.com) provides a free-tier plant species API. Key endpoint: GET /api/species-list?key=<KEY>&q=<query>.

Implement:

export type SpeciesSuggestion = {
  id: string;
  common_name: string;
  scientific_name: string;
  watering: string; // e.g. "frequent", "average", "minimum"
  sunlight: string[]; // e.g. ["full sun", "part shade"]
  cycle: string; // e.g. "Perennial"
  default_image_url: string | null;
};

export async function searchSpecies(query: string): Promise<SpeciesSuggestion[]>;
export async function getSpeciesById(id: string): Promise<SpeciesSuggestion | null>;

Cache results for 24 hours in a garden_species_cache table (columns: species_id text pk, data jsonb, cached_at timestamptz). Add this table to the schema in src/modules/garden/schema.ts and include it in the migration from task 70 (or add a new migration).

If the API is unreachable or returns an error, log the failure and return [] / null. PERENUAL_API_KEY from env; if absent, searchSpecies returns [] silently so the app works without the key.

Server (src/modules/garden/server/)

Add to actions.ts:

  • createPlant(input) — Zod-validated, getCurrentSession, insert, logActivity, revalidatePath("/garden").
  • updatePlant(input: { id, ...partials }) — patch, household check, logActivity.
  • deletePlant(input: { id }) — household check, logActivity; care logs + schedules cascade-delete via DB.
  • addPlantImage(input: { id, url }) — appends URL to images jsonb array; reject if already 10 images.
  • removePlantImage(input: { id, url }) — removes URL from array; if it was primary_image_url, set primary to first remaining or null.
  • setPrimaryImage(input: { id, url }) — sets primary_image_url; url must already be in images.

Add to queries.ts:

  • listPlants({ containerId? }) — all plants in household optionally filtered by container. Include last-care timestamps per type via a lateral subquery.
  • getPlant(id) — full plant data + container name + care log summary (last 5 entries) + active schedules.

UI (src/modules/garden/components/)

plant-list.tsx (server component)

  • Grouped by container first; "Unassigned" group for plants with no container.
  • Each card: primary image thumbnail, name, health badge, "last watered X days ago", overdue care indicator.
  • Replaces the placeholder in the "Plants" tab on /garden/page.tsx from task 71.

plant-detail.tsx (server component)

  • Three-tab layout: Info, Gallery, Care (Care tab filled in by task 73).
  • Info tab: all fields, container link, health/stage badges, species info.
  • Gallery tab: photo grid, primary image star-toggle, delete individual image, upload new image.

plant-form.tsx (client component)

  • Fields: name (required), category (select), container (select, nullable), health status (select), growth stage (select).
  • Species search combobox: debounced search → searchSpecies → select → auto-fills scientific name, sunlight, watering notes. User can override.
  • Care notes textareas: watering notes, fertilizing notes, general notes.
  • Acquisition date picker.
  • Image uploader: drag-and-drop or click, previews, max 10. First uploaded image auto-set as primary.
  • containerId query-param pre-fill (from task 71 "Add plant to container" button).

species-search.tsx (client component)

  • Debounced combobox calling a /api/garden/species-search route handler. Fires onSelect(suggestion) on pick.

Pages (src/app/garden/)

  • plants/new/page.tsx<PlantForm /> in create mode.
  • plants/[id]/page.tsx<PlantDetail /> with edit/delete controls.
  • plants/[id]/edit/page.tsx<PlantForm /> in edit mode.

Activity log

renderActivity for garden.plant:

  • createAdded plant "${name}"
  • updateUpdated "${name}"
  • deleteRemoved plant "${name}"

Register search on the garden.plant entity using ilike on name || ' ' || coalesce(scientific_name, '').

Plants are shareable (read-only). The shared view shows name, scientific name, primary image, gallery, health status, and care notes. Add loadPlantForShare(id) in src/modules/garden/server/share-queries.ts.

Out of scope

  • Care log UI and schedules (task 73).
  • Dashboard widgets (task 75).

Acceptance criteria

  • Create / edit / delete plants works end-to-end.
  • Species search returns Perenual results and auto-fills form fields on selection.
  • App works correctly via manual entry when PERENUAL_API_KEY is absent.
  • Up to 10 images upload; primary image can be set and changed; individual images can be removed.
  • Plant list on /garden groups by container with an "Unassigned" bucket.
  • garden.plant entity is registered with search and share support.
  • Activity log entries appear for create / update / delete.
  • Plant share link renders the read-only view correctly.