Add PlantDB design spec

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Stephan Kerkman 2026-05-27 19:23:20 +02:00
commit 5b2cc87540

View file

@ -0,0 +1,228 @@
# PlantDB — Design Spec
**Date:** 2026-05-27
**Stack:** Django 5 + Bootstrap 5 + HTMX
**Deployment:** Docker Compose on homelab
---
## 1. Overview
PlantDB is a mobile-first family plant and flower registry. It tracks both indoor and outdoor plants, stores care information per species, surfaces a pruning calendar, and integrates with the Perenual API to auto-populate species data. No user accounts — a single shared instance for the whole family.
---
## 2. Architecture
```
Browser (mobile-first)
│ Bootstrap 5 + HTMX (partial page updates)
Nginx (reverse proxy + static/media files)
Gunicorn + Django 5
SQLite (single file, Docker volume)
└── Perenual API (external, cached on first lookup)
```
**Key principles:**
- Server-rendered Django templates throughout; HTMX handles dynamic interactions (species search live results, pruning log updates) without a separate API layer.
- Perenual API is called only once per species — results are cached in the `Species` table. No recurring API cost after initial lookups.
- All uploaded media (personal plant photos) stored in a Docker volume and served by Nginx.
---
## 3. Data Model
### `Species`
Populated from Perenual API or entered manually. One row per plant species.
| Field | Type | Notes |
|---|---|---|
| `id` | PK | |
| `common_name` | CharField | |
| `scientific_name` | CharField | |
| `description` | TextField | |
| `watering` | CharField | e.g. "Weekly", "Twice a week" |
| `sunlight` | CharField | e.g. "Full sun", "Partial shade" |
| `shadow_tolerance` | CharField | e.g. "Low", "Medium", "High" |
| `max_height_cm` | IntegerField | nullable |
| `growth_rate` | CharField | e.g. "Slow", "Moderate", "Fast" |
| `growth_season` | CharField | e.g. "SpringSummer" |
| `pruning_months` | JSONField | Default schedule, e.g. `[2, 6, 8]` — pre-fills the Plant field |
| `api_image_url` | URLField | Image from Perenual, nullable |
| `perenual_id` | IntegerField | Unique, nullable — prevents duplicate fetches |
### `Plant`
One row per physical plant owned by the family.
| Field | Type | Notes |
|---|---|---|
| `id` | PK | |
| `name` | CharField | User's nickname, e.g. "Big rose on back fence" |
| `species` | FK → Species | Nullable — plant can exist without a species link |
| `location` | CharField | Free text, e.g. "Living room windowsill" |
| `is_indoor` | BooleanField | |
| `pruning_months` | JSONField | This plant's actual pruning schedule — pre-filled from species, editable per plant |
| `photo` | ImageField | Personal photo, stored in media volume, nullable |
| `notes` | TextField | Free text, nullable |
| `date_added` | DateField | Auto-set on creation |
### `PruningLog`
One row per pruning event, logged by the family.
| Field | Type | Notes |
|---|---|---|
| `id` | PK | |
| `plant` | FK → Plant | |
| `pruned_on` | DateField | |
| `notes` | TextField | nullable |
---
## 4. Pages & Views
### Dashboard (home `/`)
- Summary tiles: total plant count, number of plants due for pruning this month.
- Upcoming pruning list: overdue plants first (red), then due this month (amber).
- Buttons: "View all plants", "Add plant".
- HTMX: none needed — static server render on load.
### Plant List (`/plants/`)
- Scrollable list of all plants, each showing name, location, indoor/outdoor badge, and thumbnail.
- Search bar at top — HTMX live search filters the list on each keystroke (targets the list partial).
- Filter chips: All / Indoor / Outdoor.
- FAB-style "+" button links to Add Plant.
### Plant Detail (`/plants/<id>/`)
- Full-width personal photo hero (falls back to API species image if no personal photo).
- Name, scientific name (italic), indoor/outdoor badge.
- Location line.
- Care chips: 💧 watering · ☀️ sunlight · 📏 max height.
- Species reference image (small, below care info) if API image available and personal photo also exists.
- Pruning strip: recommended months + last pruned date. Amber highlight if due this month, red if overdue.
- "Log pruning" button → inline HTMX form (date + optional notes), updates pruning strip on success.
- Pruning history accordion: list of all `PruningLog` entries for this plant.
- Edit and Delete actions.
### Add Plant (`/plants/add/`)
Two-step flow:
**Step 1 — Species search (optional)**
- Search input with HTMX live lookup against Perenual API (debounced, 300ms).
- Results show common name, scientific name, thumbnail.
- Selecting a result caches the species in the DB (if not already cached) and pre-fills the form.
- "Skip — enter manually" link bypasses to step 2 with a blank form.
**Step 2 — Plant form**
- Nickname field (required).
- Location field (free text).
- Indoor/Outdoor toggle.
- Care fields (watering, sunlight, etc.) — pre-filled from species if selected, all editable.
- Pruning months — pre-filled from species, shown as a 12-checkbox month picker.
- Photo section: "Take photo" (camera capture via `<input type="file" accept="image/*" capture="environment">`). The species API image, if available, is always shown automatically on the plant detail as a reference — no separate action needed.
- Notes textarea.
- Save button.
### Edit Plant (`/plants/<id>/edit/`)
Same form as Add, pre-populated.
### Pruning Calendar (`/pruning/`)
- Grouped by urgency:
1. **Overdue** (red) — plants with a recommended pruning month in the past and no log entry since.
2. **This month** (amber) — plants with current month in `pruning_months`.
3. **Next month** (grey) — plants due next month.
4. **Later this year** — collapsed section, expandable.
- Each entry: plant name, location, recommended months, last pruned date. Tap to open plant detail.
- Month strip at top (scrollable) to jump to any month and see what's scheduled.
### Species Detail (`/species/<id>/`) — optional read-only
- Shows all species info as reference. Linked from plant detail. Not editable directly (edit via the plant form).
---
## 5. Perenual API Integration
- **Endpoint used:** `GET https://perenual.com/api/species-list?q=<name>&key=<API_KEY>`
- **API key:** stored in Django settings via environment variable `PERENUAL_API_KEY`.
- **Cache strategy:** on first successful lookup, a `Species` row is created with `perenual_id` set. Any subsequent search that returns a species already in the DB skips the API entirely.
- **Free tier limit:** 100 requests/day. Given that lookups only happen when adding new plants (not on every page view), this is not a practical constraint for a family app.
- **Fallback:** if the API is unreachable or returns an error, the add-plant form stays open with an empty species form for manual entry. No hard dependency on the API.
---
## 6. Pruning Logic
A plant is considered **overdue** for pruning if:
- It has at least one `pruning_month` that fell within the past 12 months, AND
- There is no `PruningLog` entry with `pruned_on` falling within that month's window.
A plant is **due this month** if the current month is in `Plant.pruning_months` and not yet logged this month.
Using a 12-month rolling window (rather than calendar-year) correctly handles plants due in December when checked in January.
The pruning calendar view computes this in a Django queryset annotated with the current date — no background tasks or scheduled jobs needed.
---
## 7. Deployment
### Docker Compose
```
services:
web: Django + Gunicorn
nginx: Reverse proxy + static/media serving
volumes:
db: SQLite file
media: Uploaded plant photos
```
- Accessible on the local network via homelab IP or a local hostname (e.g. `plantdb.home`).
- No SSL required for LAN use.
- `docker compose up -d` starts everything; `docker compose down` stops it.
- Database backup: copy the SQLite volume file. Simple enough to add to an existing homelab backup routine.
### Environment Variables
| Variable | Purpose |
|---|---|
| `PERENUAL_API_KEY` | Perenual API key |
| `SECRET_KEY` | Django secret key |
| `DEBUG` | `False` in production |
| `ALLOWED_HOSTS` | Homelab hostname/IP |
---
## 8. Static & Media Files
- Static files (CSS, JS, Bootstrap): collected via `manage.py collectstatic`, served by Nginx.
- Media files (plant photos): stored in `/media/` inside the container, mounted as a Docker volume, served by Nginx under `/media/`.
---
## 9. Dependencies
| Package | Purpose |
|---|---|
| `django` | Web framework |
| `gunicorn` | WSGI server |
| `django-htmx` | HTMX integration helpers |
| `pillow` | Image handling for `ImageField` |
| `requests` | Perenual API calls |
No frontend build step — Bootstrap 5 and HTMX loaded via CDN or bundled as static files.
---
## 10. Out of Scope
- User authentication / accounts
- Watering or feeding reminders
- Push notifications
- Multi-language support
- Public internet access / SSL (LAN only)
- Offline / PWA mode