228 lines
8.7 KiB
Markdown
228 lines
8.7 KiB
Markdown
# 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. "Spring–Summer" |
|
||
| `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
|