- 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)
145 lines
4.6 KiB
Markdown
145 lines
4.6 KiB
Markdown
# 75 — Garden dashboard, widgets, and manifest completion
|
||
|
||
## Goal
|
||
|
||
Complete the garden module: full manifest registration, two dashboard widgets, search integration, quick-add polish, and a Playwright happy-path test.
|
||
|
||
## Depends on
|
||
|
||
- 70–74 (all previous garden tasks)
|
||
|
||
## Scope
|
||
|
||
### Manifest completion (`src/modules/garden/manifest.tsx`)
|
||
|
||
Replace the task-70 stub with the full manifest.
|
||
|
||
**Entities:**
|
||
|
||
```typescript
|
||
entities: [
|
||
{
|
||
type: "garden.container",
|
||
label: { singular: "Container", plural: "Containers" },
|
||
share: { canShare: true, defaultCapabilities: ["read"] },
|
||
search: { search: searchContainers },
|
||
resolveUrl: (id) => `/garden/containers/${id}`,
|
||
loadForShare: loadContainerForShare,
|
||
renderSharedView: ...,
|
||
renderActivity: ...,
|
||
},
|
||
{
|
||
type: "garden.plant",
|
||
label: { singular: "Plant", plural: "Plants" },
|
||
share: { canShare: true, defaultCapabilities: ["read"] },
|
||
search: { search: searchPlants },
|
||
resolveUrl: (id) => `/garden/plants/${id}`,
|
||
loadForShare: loadPlantForShare,
|
||
renderSharedView: ...,
|
||
renderActivity: ...,
|
||
},
|
||
]
|
||
```
|
||
|
||
`garden.schedule` is internal (reminders only) — not registered as a shareable or searchable entity.
|
||
|
||
**Quick adds** (consolidate from tasks 71–73):
|
||
|
||
- `{ id: "garden.add-plant", label: "Add plant", icon: "leaf", url: "/garden/plants/new" }`
|
||
- `{ id: "garden.add-container", label: "Add container", icon: "box", url: "/garden/containers/new" }`
|
||
- `{ id: "garden.log-care", label: "Log plant care", icon: "droplets", url: "/garden?logCare=1" }`
|
||
|
||
**Nav**: `{ href: "/garden", label: "Garden", icon: "sprout" }`.
|
||
|
||
---
|
||
|
||
### Widget 1: `garden.care-due`
|
||
|
||
**Title**: "Plants needing care" | **Category**: "Garden"
|
||
**Default size**: `{ w: 4, h: 4 }` | **Min size**: `{ w: 3, h: 2 }`
|
||
|
||
Config schema:
|
||
|
||
```typescript
|
||
z.object({
|
||
containerIds: z.union([z.literal("all"), z.array(z.string().uuid())]),
|
||
daysAhead: z.number().int().min(0).max(30).default(0),
|
||
});
|
||
```
|
||
|
||
Default config: `{ containerIds: "all", daysAhead: 0 }`.
|
||
|
||
`resolveConfigOptions`: returns `{ containers: [{ id, name }] }`.
|
||
|
||
Render:
|
||
|
||
- Empty state if nothing is due: "All plants are on schedule."
|
||
- Otherwise: compact list sorted by days overdue. Each row: plant name, care type icon, urgency badge ("X days overdue" / "due today"), inline "Log care" button (calls `logCare` action + revalidates — no navigation).
|
||
- Truncate at 10 rows; "View all" link to `/garden`.
|
||
- Rows with `next_due_at` within `daysAhead` days show as upcoming in a lighter style below overdue rows.
|
||
|
||
---
|
||
|
||
### Widget 2: `garden.overview`
|
||
|
||
**Title**: "Garden overview" | **Category**: "Garden"
|
||
**Default size**: `{ w: 3, h: 2 }` | **Min size**: `{ w: 2, h: 2 }`
|
||
|
||
Config schema: `z.object({})`.
|
||
|
||
Render:
|
||
|
||
- Stat row: total plants, total containers, overdue care count.
|
||
- "Next care" line: "Next: water [Plant] in N days" / "today" / "overdue".
|
||
- Link to `/garden`.
|
||
|
||
---
|
||
|
||
### Widget component (`src/modules/garden/components/plant-widget.tsx`)
|
||
|
||
Server component implementing both widgets, following the pattern in `src/modules/lists/components/list-widget.tsx`.
|
||
|
||
---
|
||
|
||
### Search adapters
|
||
|
||
Implement `searchContainers` and `searchPlants` using `ilike` on name fields, returning `SearchResult[]`. Both appear in the command palette.
|
||
|
||
---
|
||
|
||
### Notification body improvement (optional)
|
||
|
||
If `tickReminders` in `src/modules/_core/reminders.ts` can be extended without structural changes — add a registry of entity-type resolvers and register `"garden.schedule"` to resolve to `"Time to water [plant name]"`. If it requires core changes, defer and note as known limitation.
|
||
|
||
---
|
||
|
||
### Playwright E2E test (`tests/garden.spec.ts`)
|
||
|
||
Happy path:
|
||
|
||
1. Log in as the seeded user.
|
||
2. Create container "Living Room Shelf".
|
||
3. Create plant "Pothos" in that container (manual entry, no species lookup).
|
||
4. Add watering schedule: every 7 days.
|
||
5. Log care: watered.
|
||
6. Verify schedule `next_due_at` updates to ~7 days from now.
|
||
7. Confirm plant appears grouped under "Living Room Shelf" on `/garden`.
|
||
8. Add `garden.care-due` widget to the default dashboard.
|
||
9. Confirm no overdue items (just watered).
|
||
|
||
## Out of scope
|
||
|
||
- Health trend charts.
|
||
- Weather-based watering adjustments.
|
||
- Social/community features.
|
||
|
||
## Acceptance criteria
|
||
|
||
- [ ] Both widgets register and appear in the widget picker.
|
||
- [ ] `garden.care-due` shows overdue plants sorted by urgency; inline log button works.
|
||
- [ ] `garden.overview` shows correct counts.
|
||
- [ ] Both entities appear in command palette search.
|
||
- [ ] All three quick-adds appear in the `+` sheet.
|
||
- [ ] Garden nav link appears in sidebar and bottom nav.
|
||
- [ ] Playwright happy-path test passes.
|