- 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)
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 toimagesjsonb array; reject if already 10 images.removePlantImage(input: { id, url })— removes URL from array; if it wasprimary_image_url, set primary to first remaining or null.setPrimaryImage(input: { id, url })— setsprimary_image_url; url must already be inimages.
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.tsxfrom 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.
containerIdquery-param pre-fill (from task 71 "Add plant to container" button).
species-search.tsx (client component)
- Debounced combobox calling a
/api/garden/species-searchroute handler. FiresonSelect(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:
create→Added plant "${name}"update→Updated "${name}"delete→Removed plant "${name}"
Search
Register search on the garden.plant entity using ilike on name || ' ' || coalesce(scientific_name, '').
Share links
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_KEYis absent. - Up to 10 images upload; primary image can be set and changed; individual images can be removed.
- Plant list on
/gardengroups by container with an "Unassigned" bucket. garden.plantentity is registered with search and share support.- Activity log entries appear for create / update / delete.
- Plant share link renders the read-only view correctly.