# πΏ Verdant β Self-Hosted Garden Journal
*Logo designed by Kate πΏ*
[](https://github.com/Elyld/Verdant/actions/workflows/test.yml)
[](https://github.com/Elyld/Verdant/releases)
[](https://github.com/Elyld/Verdant/pkgs/container/verdant)
[](LICENSE)
Your garden, logged. Plant profiles with care reminders, a daily observation log
with a calendar heatmap, photo albums with slideshows, a seed stash and seedling
tracker, a drag-and-drop backyard planner, NFC tag shortcuts, harvest records,
a season-in-review dashboard with a yield-vs-spend scorecard, cost and pest
tracking, a phone-friendly quick-log page, CSV import from other trackers,
one-click backup & restore, Immich photo imports, and a morning Discord digest β
all in a single Docker container. FastAPI + SQLite, no build step, no cloud.

> **THIS IS 100% VIBE CODED.** This is just a personal project, built by
> someone with no coding knowledge. It works, it's loved, and it's a little
> feral in places. π±
*Screenshots below show demo data.*
## Features
### π§ Navigation β sidebar, custom mobile bar
On a desktop the nav is a slim left sidebar grouped into five labeled
sections β **Grow** (Today, Plants, Seeds, Seedlings, Planner), **Track**
(Garden Logs, Calendar, Quick Log, Photos, Pantry), **Manage** (Costs, Pests,
Fertilizers, Review, Import, Tags), **Assistant** (Garden Assistant), and
**Read** (Blog & Stories) β with no dropdown menus anywhere. The header stays
slim: logo, the season picker, and the weather strip with any active NOAA
alerts up top. On your phone you get a bottom tab bar instead: pick up to
**5** of your own sections in the order you tap them, right on the
π± *Mobile tab bar* card of the Settings page (tap a chip to add it, tap
again to remove β no dropdowns there either), and everything else stays one
tap away under **More**, which opens a bottom sheet of the remaining
sections. Out of the box the bar is Quick Log, Plants, Calendar, Planner,
and Settings β so Settings is always one tap away on your phone.


### βοΈ Today β your garden's morning brief
Today is the landing page: a greeting, the date, and everything that matters
right now. **π§ Care due** lists what's overdue or due today for watering and
feeding across all your plants. **π§Ί Harvest forecast** predicts what's ready
from planting dates + the crop guide (clearly labeled estimates β your eyes
are the final judge). Below that, the weather strip, any active NOAA alerts,
and a **βοΈ frost countdown** to your first fall frost β or a gentle prompt to
set it in Settings if you haven't. One glance, then grab your gloves.
The weather strip ends with a **π§ watering-advice line**: Verdant computes
real evapotranspiration (FAO-56 Penman-Monteith via PyETo) from your forecast
and tells you, in plain words, whether today calls for a normal round, a
deep soak, or putting the hose down β with the ET number in inches for the
curious. And care reminders are **π§οΈ weather-aware**: when real rain is
expected today or tomorrow, watering tasks show a rain-hold badge and sink to
the bottom of the list instead of nagging β the sky's got those. The morning
Discord digest says the same.

### π
Calendar β daily log at a glance
A GitHub-style heatmap of every observation, watering, and fertilization β
all twelve months of the season picked up top. Click any day to see exactly
what happened: health scores, notes, pests, weather. Changing the Season
picker re-renders the whole year.
### π± Plants β profiles, care cadence, timelines
Every plant gets a profile: variety, species, location, planted date, days to
maturity, light needs, and custom care intervals. Verdant computes what's
**overdue, due today, or coming up** for watering and feeding. Each profile
also has a photo timeline and a per-plant timelapse view.
Photos you assign on the π― Match photos page appear in a **Photos** gallery
on the plant's profile, with a lightbox on click. Plants follow the
**season year** picker in the header β switch seasons without deleting
anything; last year's plants stay for history, rotation warnings, and the
season review. And each plant can record which **seed packet** it was grown
from, tracing it back through the packet
to the invoice that bought it.
Adding a plant? Hit **π Look up growing info** on the form β search the
crop guide (30 curated crops plus **8,200 edible plants from OpenPlantDB**,
public domain, with variety-level detail down to Fatalii and the other
superhots), pick your crop, and sun, days to maturity, spacing,
sowing depth, and germination timing fill themselves in (only the empty
fields; your typing is never overwritten). Latin names are searchable too. The guide also blends in
**π Growstuff community data**: when real gardeners' results exist for a
crop, maturity timing is the community median (tomatoes show ~92 days from
227 gardens, not the generic guide number), and the source is spelled out
under every detail. Every crop detail also answers
**βcan I still plant this?β** β a π± planting calculator that works back from
your first frost date (minus the days to maturity and a 14-day buffer) and
tells you it's still time, cutting it close, or too late for a fall harvest.
Got a mystery plant? The **π Identify** button (needs a free PlantNet API
key, set in Settings) sends one photo and suggests matches with confidence
scores β a tap assigns the winning name. Your photo is only ever sent when
you tap Identify, never before.





### π Garden Logs β the daily log
Log health (1β10), watering, pest sightings, and freeform notes per plant, per
day β with photos attached. Feedings live here too, with the feeding form
suggesting from your π§ͺ Fertilizer shelf. New observations are automatically
stamped with the current weather at your garden (free Open-Meteo data, no API
key needed).

### πΈ Photos β albums & slideshows
Albums with fullscreen slideshows. Upload directly, import from a URL, pull
images into blog posts, or import whole albums from your Immich server
(batched, so even 600+ photo albums import without timing out). Re-importing
an album reuses its existing Verdant album instead of duplicating it, and
**π§Ή Merge duplicates** folds any older double-imports into one. On the
**π― Match photos** page you can flip through imported photos and assign each
one to a plant or a seed packet β plant photos show up in a gallery on the
plant's profile.

The π― Match photos page (linked from the Photos tab) is where imported
photos get claimed: flip through them one by one, see each photo's taken
date, camera, GPS, and tags, and assign it to a plant or a seed packet with
a keystroke. Bulk-assign a whole day's photos at once when you already know
what they are.

#### π·οΈ Photo tagging & attribution
A photo can be **tagged and linked at the same time** β tags never wipe a
plant assignment. On the Match page, the **Tags, notes & container** card
offers the seeded vocabulary as checkboxes (plus free-text tags), a
caption/note field, and a container picker, so a bed's build photos can
live on the bed even while they also show a plant. Tags show as chips on
the Match page and in the slideshow, and the Match page can filter by tag.
The vocabulary (`GET /api/album-images/vocabulary`):
| Tag | What it covers |
| --- | --- |
| `seed-starting` | Trays, cups, grow lights, seedlings |
| `flower` | Flowers, buds, husks (no variety recoverable) |
| `plan` | Hand-drawn layouts, sketches, graph paper |
| `harvest` | Harvest spreads, plates, curing crops |
| `wide-shot` | Full-garden / yard overviews |
| `soil-prep` | Mulch, cracked ground, bed building, fill |
| `pest-disease` | Bugs, mildew, leaf damage, sunscald |
| `supplies` | Soil bags, bins, pots, pallets, setups |
| `structure` | Trellises, cages, greenhouse, raised beds |
| `unidentified` | Nothing recoverable |
The same release also lands the smaller attribution items: `POST
/api/containers/` now honours `grid_x`/`grid_y` (clamped inside the grid,
422 on overlap β otherwise it still auto-places); plants gain a **Culled**
status for healthy-but-removed plants, and every status change stamps
`status_changed_at`; observations can leave `health_scale` unset instead
of silently recording a default 5 (averages skip unset scores); pest
entries gain a severity and a resolved date, stamped automatically when
you hit Resolve; the Photos page warns about duplicate album names and
offers the merge, and Immich gains a **Sync all albums** action that
stamps `last_synced_at` per album β and the batch import no longer
demands `album_id` for `offset > 0`, resolving the local album from its
Immich source instead.
> Screenshots for the new tagging card, duplicate-name banner, and
> sync-all button are still to be captured on a live instance β this
> release was built in a sandbox with no headless browser available, so
> no screenshots were faked for it.
#### ποΈ Legacy albums & seasons
Albums created before source tracking existed used to be permanently
orphaned: no `source_url`, so sync-metadata refused them and there was no
way to fix that. Now an Immich import that names such an album stamps its
`source_url` onto it, and `PATCH /api/albums/{id}` accepts `source_url`
directly (empty clears it) β either adoption makes sync-metadata work
again. Imports also recover dates: when an Immich asset carries no date,
the filename is parsed instead (`YYYYMMDD_HHMMSS`, including mid-name,
and epoch-millis names), and `POST /api/albums/{id}/backfill-dates` does
the same for existing rows without needing Immich at all. Only blank
dates are filled, never overwritten, and filenames with no recoverable
date stay dateless rather than getting an invented one. **π§Ή Merge
duplicates** now folds same-name albums with no source too β by file
content, so an album whose photos are all already in a bigger same-name
album is dropped as safe to delete β and when it merges nothing it says
why instead of reporting a clean no-op. `GET
/api/albums/{id}/duplicate-shots` lists same-timestamp groups in one album
(the low-res + full-res copies of one shot) for a human to judge. Photos
named `chatgpt-*` are flagged AI-generated on import and excluded from
the Match triage queue and the yearbook. Plants gain a `season_year`
field (set it via PATCH) and the plant list takes `?season_year=`, which
matches the explicit value, else the earliest planting season, else the
`date_planted` year β so a reconstructed season (say 2025) separates from
the live roster. 2.65.1 hardens the generated flag for upgraded
databases: startup backfills any NULL flags to 0, and the API coerces a
NULL flag to false instead of rejecting the response β v2.65.0 500'd
every album endpoint on databases upgraded from before the flag existed,
because `ALTER TABLE ADD COLUMN` leaves existing rows NULL.
There's also a dedicated full-screen slideshow page β pick an album, pick an
interval, and let the garden scroll by on a TV or tablet.

### πͺ Vendors β where the seeds came from
A read-only directory of every place your seeds came from, built from the
stash: packet counts and year ranges per vendor, with a jump straight into
the stash filtered to that vendor. (The old manual seed-source list was
redundant next to the stash β it's gone; the data underneath is untouched.)

### π± Seed stash β the binder, catalogued
Every packet you own, inventoried: a photo of the packet, vendor (with link),
type, year bought, and how many seeds are left. Searchable and filterable β
including a π· filter for which packs have photos. Packet photos can be
uploaded or picked straight from your photo library (e.g. an Immich album of
packet shots), and the β flip button shows the growing info on the back of
the packet. Crooked scan? The βοΈ Crop button on each side lets you straighten
a packet photo in place β the library original stays untouched. Stick an NFC tag on the binder and tapping it opens the
"add packet" form.



### π Order assistant β winter seed ordering, minus the spreadsheet
Come ordering season, the **Order assistant** tab pulls it all together: every
stash packet with its age ("bought 2024 Β· 2 yrs old") and your one-tap
**grow again?** rating β π Skip, π Grow again, β Favorite (tap again to
clear) β plus last year's seed spend broken down by vendor, and a wishlist
where each variety shows the last vendor and date you ordered it (resolved
from your invoices). Tick the varieties you want and they roll up into an
order list per vendor. No new state to maintain: ratings live on the packets,
the wishlist is its own list, and invoices are the source of truth.
At the top of the tab sits the **π² Chaos pick** card: one random experimental
plant per year, always something you've never grown (32 curated oddballs β
cucamelon, salsify, orach, luffaβ¦), drawn from a list that excludes your whole
growing history and every prior year's pick. One pick per year, stable until
you hit **Reroll** β no vetoes, but rerolls are unlimited and never repeat
within the year. **Add to my order** drops it onto the order list as a checked
wishlist item. The chat knows it too: ask "what's my chaos pick?" or say
"reroll my chaos pick" (it confirms in chat before replacing the pick).

### βοΈ Blog β garden stories
Markdown blog posts with photo galleries, for the season's stories β first
harvests, experiments, lessons learned. The blog lives under **Read** in the
nav (at `/blog`); the βοΈ Today page is the landing page now.

### π Season Review β your year in the garden
Totals, averages, best days, most productive plants β a year-end (or
anytime) dashboard of everything you grew and logged. The **π yield
leaderboard** keeps weighed and counted harvests on separate boards (all
weights converted to ounces, so grams never get added to ounces), and the
**βοΈ Season scorecard** answers "was it worth growing?": yield vs. spending
per variety, ranked β tag purchases to a plant on the Costs page to split
costs per variety.


### ποΈ Vacation caretaker sheet β hand the garden off
Going away? The **ποΈ Caretaker sheet** button on the Today page opens a
printable, phone-friendly page (`/api/caretaker-sheet`) built for the person
watching your garden: π§ what to water (with each plant's cadence and how
overdue it is), π§Ί what's ready to pick, π pests to keep an eye on, π your
latest notes, and a spot for emergency contacts. Open it on their phone or
print it for the fridge.

### π°οΈ This week last year β the garden's memory
The morning digest now closes with a **π°οΈ This week last year** section: what
you were doing this exact week last year β journal notes, harvests, and the
photos you took (attached as real images in the Discord message, not
filenames). The chat can answer it too ("what did I do this time last
year?") via the `this_week_last_year` tool.
### π± Growth stall detector β "is it still growing?"
Ask the chat "is my tomato still growing?" and the `growth_check` tool pulls
the plant's photos from the last ~6 weeks and asks your vision-capable chat
model to compare them: visible growth, stall, or can't tell β with likely
causes when it stalls (pot-bound? light? nutrition?). Advisory only, and it
says so when there aren't enough photos to judge.
### π² Frost-night gamble β one bold call
When frost threatens tonight, hedging is for cowards. The digest carries a
**π² Frost-night gamble** block with one bold call: π± **COVER** the tender
plants, or π§Ί **HARVEST NOW** what's ripe. It triggers on tonight's forecast
low β€ 38 Β°F or a first-frost date 0β3 days out; β€ 32 Β°F with ripe harvests
waiting means HARVEST NOW, otherwise COVER. Advisory β your garden, your call.
Also available as the `frost_gamble` chat tool.
### π Season yearbook β the keepsake PDF
The Review page's **π Yearbook** button downloads a keepsake PDF of the
season: year stats, per-variety tables, and photo pages. Print it, shelve it
next to the seed catalogs. The season story is grounded in the journal: it
summarises your actual observations, pest log, plant notes, and harvests,
and never invents a pest, a weather event, or a cause of death. Harvest
weights show "no weights recorded" where a harvest was logged without a
weight β a missing weight is never printed as 0 oz.

### π° True cost β was it cheaper than the store?
The Review page's **π° True-cost verdict** section answers the question every
gardener asks in October: total spend, total pounds grown, and homegrown $/lb
vs. honest grocery-store estimates β per variety, with fun verdicts ("cheaper
than the store! π±", "a labor of love π"). Grocery baselines are clearly
labeled estimates. The chat's `true_cost` tool answers "was it worth it" the
same way.

### π Plant autopsy β the coroner interview
When a plant dies, the chat doesn't just log it β it plays coroner first: up
to 3 quick questions (what did it look like at the end? sudden or gradual?
weather or pests involved?), then files a structured autopsy note via the new
`record_autopsy` tool (confirm-before-save, like every write). Death notes
attach to the variety's performance history, so when the order assistant asks
"grow again?" next winter, it remembers what killed it.
### πΊοΈ Backyard Planner β build your backyard
Lay out your actual growing space on a real grid (1 cell = 1 ft, resizable):
grow bags, raised beds, pots, planters, cattle panel arches, pallets β each
drawn at its true footprint, so a 4Γ8 bed actually looks 4Γ8 next to a 10-gal
bag. Drag containers and they snap to the grid (positions save automatically,
and you can't drop one on top of another). A container holds **many plants**
now β a bed lists everything in it β and the **π rotation check** flags when
you put the same plant family where it grew last season. Copy last season's
layout into the new year and shuffle things around. Flip to the **3D view** and
the yard renders as a soft miniature diorama β smooth-shaded beds, pots and
bags on a grass platform with a subtle 1-ft grid, and little foliage clusters
for plants (tomatoes even grow tiny red fruit). Hit **πΆ Walk** to drop to
eye level and stroll between the beds with WASD + mouse (Shift to sprint).
Walk mode is **creative mode**: a hotbar (Q switches between
**BLOCKS** and **PLANTS**, 1β9 or the mouse wheel picks a slot, everything is
β) lets you build the garden by walking around it β **left-click breaks** the
aimed planting or container (breaking a planted container asks first), and
**right-click places** the selected item on the aimed grass cell β a green/red
ghost preview shows exactly where it'll land β or plants the selected seedling
into the aimed container. **E** edits the aimed container, ESC releases the
mouse. Every placement and break goes through the real API and edits your real
garden. Walk mode now stars a little **π§βπΎ gardener**: a clay-tinted character
(CC0 Quaternius model) with a straw hat who walks the garden in **third person**
β **V** toggles between first-person and over-the-shoulder, **Space** jumps,
and **F** tends the nearest bed: the Working animation plays, thirsty beds get
a blue water arc (logged through the real watering API), harvest-ready crops
pop with a floating "+N" (logged through the real harvest API). Idle/Walk/Run/
Jump/Working all crossfade via the model's real animation clips. Orbiting still works too, and
everything renders on demand so it sips battery. A slim **π€οΈ weather ribbon** runs under
the site header on every page β current conditions, tonight's low, tomorrow's
high, rain chance, and wind gusts. On the planner, **garden alerts** underneath
it read your actual data: frost warnings that name your tender containers, heat
alerts for the thirsty pots and bags, rain-skip nudges when you watered recently,
spray wash-off warnings, wind alerts for the arches, and a tomato blight watch.
Set your coordinates on the Settings page to light the ribbon up; until then it
stays quietly hidden. When the National Weather Service has active alerts for
your garden point, a severity-tinted β οΈ pill appears in the ribbon β click it
for each alert's timing and details (free, no key; cached 15 minutes).

Hit **π₯ Yield** to tint every container by last season's harvest weight
(darker = heavier), so the spots that earned their keep jump out. Open any
container and the **π± companion hints** suggest good and bad neighbors from a
curated, extension-service-sourced list.








Empty containers don't sit idle: **π succession ideas** read the season,
your first-frost date, and the crop guide, then suggest what each free
container can still fit before frost β sow-by dates included, rotation-aware
(same family that just grew there is skipped). In October it'll honestly tell
you nothing beats the frost and point you at garlic instead.


### π·οΈ NFC Tags β tap it, log it
Blank NFC tags turn physical things into shortcuts: tap the fertilizer
bottle and the feeding form opens with that fertilizer pre-selected; tap the
seed binder and the "add packet" form opens; tap a grow bag and that plant's
profile opens. The Tags page manages every tag β write its URL onto a blank
tag once with any NFC writer app, then your phone opens it with a tap, no
app needed.

### π§ͺ Fertilizers β the shelf
Your fertilizer products live here now: name, NPK ratio, what each is best
for. The Garden Logs feeding form suggests from the shelf, and NFC tags can
point straight at a bottle.

### π± Seedlings β the indoor workstation
Start seeds inside without losing track: every batch records variety, tray,
location, warming mat, and grow light, with a stage pipeline from sowing to
transplant and one-tap sprout logging. Germination progress bars (including
days-to-sprout) show what's working, and a nudge flags batches that go quiet
for three weeks. Finished batches keep their stats so next year's setup
repeats what worked.

### β‘ Quick Log β log it from the garden
A phone-first page for when you're standing in the garden with dirty hands:
one-tap watering per location ("Water all"), a harvest +/β stepper with an
optional **weight** input and unit (oz/g/lb/kg, prefilled from your default β
leave it blank and it's a plain count), and today's entries at a glance.
Every save pops a **β© Undo** toast (about 8 seconds) β fat-fingered a
watering from the garden? One tap takes it back. Watering, harvests, and
notes can all be undone.
NFC tags can drop you straight here. And when your hands are too dirty to tap
through forms: **π€ βTell Verdant what you didβ** β type a sentence like
βwatered the tomatoes and harvested 3 peppersβ, or tap π€ **Talk** and just
say it with dirty hands β the browser transcribes as you talk and keeps
listening through pauses until you tap Stop. Your chosen AI provider then
drafts the log entries for you to confirm: a small model running on *your
own machine* via **Ollama** (nothing leaves your network), or a cloud model
through **OpenRouter** (your key stays in Verdant's database on your server,
and it's only called when you ask it to interpret). Nothing is saved until
you confirm; opt-in on the Settings page. (Voice needs a secure https
connection, so on the LAN it works through the Cloudflare tunnel.)
**ποΈ Voice quick-log** is the no-AI sibling: tap Talk, say βwatered the
tomatoesβ or βharvested 3 peppersβ, tap Stop, and it parses the command β
plant matched from your growing list, quantity pulled out β into a one-tap
Save. No model needed, so it works even when the local AI is offline.



**π± Garden assistant.** The **Garden Assistant tab** (nav β Assistant) is a
proper home for the chat agent β no more floating bubble. It keeps full
**conversation threads** on the server, so a browser refresh never loses
your place, and every confirm-before-save **draft** persists too: drafts
waiting on you sit in a ββ³ Waiting on youβ strip until you confirm or
discard each one. The agent also has a **persistent self** β three files it
reads into its prompt every turn: **Persona** (who it is, how it talks),
**Operating notes** (lessons it learns about your garden), and **Memory**
(durable facts you've told it to keep). Tell it something worth keeping
and it proposes the addition through the new `propose_memory_write` tool β
still confirm-before-save, never silent. You can read and edit all three
files directly in the βAbout this agentβ section on the tab.
An always-ready agent that knows your garden: what's
growing and where, per-plant care history (last watered / fed / harvested),
what's due, what's ready to harvest, recent activity, and today's weather.
It can look things up on its own (care history, notes, seed stash, planner,
reminders) and take action: tell it what you did and it drafts the log
entries; ask it to add a seed packet, change a plant's status, or move a
plant to another container and it drafts that too β all under the same
confirm-before-anything-is-saved contract as βTell Verdantβ. It also has
seasonal coaching (frost countdown, what to plant/harvest now), a memory
notebook it can save to and recall from (always confirm-before-save), and
year-over-year variety performance from your harvest logs. Ask it to
**remind you at a date** ("remind me to plant carrots on Oct 12") and it
drafts a dated reminder β you confirm, and it shows up in the chat context,
the morning digest, and the `upcoming_reminders` tool. Memory notes are just
notes and can never schedule anything; the assistant knows the difference.
Attach a photo to
a message and a vision-capable model (e.g. DeepSeek v4.1 flash on OpenRouter)
can see it for pest ID or ripeness checks. Ask it to βrecap my tomatoes'
seasonβ and the new `season_recap` tool gathers that plant's photos across
the season (matched album photos + observation photos), picks up to 8 spread
across the months, and has the vision model narrate the story β growth
milestones, health, harvest moments. There's also a π Season recap button
on each plant's detail card that shows the story with its photo timeline;
βSave as noteβ files it as an observation (it asks you to confirm first).
Photos only ever go to your already-configured AI provider β the same one
the chat uses. Threads and drafts live server-side, so the
conversation survives a browser refresh β not just page navigation within
the tab. It runs on whichever AI provider
you've picked (Ollama or OpenRouter); the OpenRouter model picker lists every
available model with free ones marked.




### π₯« Pantry β what happened after harvest
Verdant used to track everything up to picking day, then the food vanished
from the story. The **Pantry** page closes the loop: log what you preserved β
canned, frozen, dehydrated, fermented, gave away, or just ate fresh β with
amounts in and out, where it's stored, and an optional link back to the
harvest it came from. Tick one box and the finished amount lands in your
**pantry inventory**, which you decrement as you use things up ("Used 2 bags
of frozen peppers") until the shelf is bare. The full preservation history
stays even after the last jar is gone.

### π° Costs β was it worth growing?
Every garden expense in one place, broken down by category β rows are
editable in place. Tag a purchase
to a plant and the Season Review scorecard splits costs per variety, so you
can finally answer whether the peppers beat the grocery store. Below the
expenses, a **π§Ύ Invoices** section keeps the paper trail: vendor, order
date, order number, total, and the PDF receipt, each optionally linked to
the expense it documents β and both invoices and expenses can be **tagged
with the seed packets** they bought (with smart suggestions matched from the
order text), so the Order assistant knows exactly what came from where.
Invoices are searchable (vendor, order number, items), filterable by source,
sortable, and editable in place. A **π· Scan receipt** button photographs a
paper receipt and reads the vendor, date, total, and items into the form β
nothing is saved until you confirm.



### π Pests β the treatment log
What showed up, what you sprayed or squashed, and whether it worked β a
running log per pest so next year's battle plan writes itself. It now opens
with a **pest & disease guide**: 40 common garden pests and diseases with ID
signs, organic and conventional treatments, and prevention, compiled from
university extension guidance and working fully offline. Search it by pest or
by plant (βtomatoβ), tap βLog a sightingβ to start a log entry, and picking a
plant in the log form surfaces its common issues as quick-pick chips.

### π₯ CSV Import β bring your own data
Moving from another garden tracker? The **Import** page (nav bar β Import)
walks you through it: pick what you're importing (locations, plants,
fertilizers, seed sources, watering logs, fertilization logs, harvests, invoices),
upload the CSV, review a preview with row counts and warnings, then import.
Imports are idempotent β re-running the same file skips what's already there,
so you'll never get duplicates.

See [Importing from CSV](#importing-from-csv) below for the expected columns.
### πΎ Backup & Restore β the whole garden in a zip
One click downloads a zip containing the full database plus every uploaded
photo β or uncheck the photos box for a small, fast database-only backup.
Restoring is the reverse: upload the zip, and you're back (a photo-less
backup leaves your current photos untouched). Keep one somewhere safe
before upgrades.

### βοΈ Settings β your garden's particulars
USDA zone, last/first frost dates (exact dates beat zone averages),
temperature units, which day your calendar week starts on, default harvest
weight unit, slideshow autoplay interval, and the morning digest schedule β
all on one page. Don't know your zone? Hit **π― Detect from coordinates**
and Verdant looks it up from your saved garden coordinates (offline, from a
built-in dataset). Your **garden coordinates** live here too (with a
π *Use my location* button that fills them in from your browser, falling
back to a one-time city-level lookup) β they power the Planner's weather
strip and stamp new observations with the current conditions. The frost
dates drive the "days to first frost" countdown in the header and tune the
seed-starting calendar on the Plants page. The site-wide weather ribbon
also watches the National Weather Service for your garden point: when there's
an active alert, a severity-tinted β οΈ pill appears β click it for
the timing and details.



### π
Morning Digest β Discord
An optional daily "morning garden check" sent to a Discord channel via webhook:
what's overdue, what's due today, what's coming up. Off by default β turn it on
from the π
Morning digest card on the Settings page (the send time uses the
timezone you pick there, defaulting to your browser's zone), or set
`DIGEST_ENABLED=true` and `DISCORD_WEBHOOK_URL` in the environment.
Optional: turn on "Let the AI write a short morning briefing" in the same card
(`DIGEST_AI_BRIEFING=true`) to have your configured AI provider write a warm
1β2 sentence briefing on top of the data sections β it falls back to data-only
if the provider is unreachable. The digest also flags the first fall frost
when it's within two weeks, and includes a π Reminders section for any
dated reminders you set via the chat assistant (due/overdue first). New in
2.51.0: a π² **Frost-night gamble** block (one bold call when frost threatens
tonight) and a closing π°οΈ **This week last year** section.
Digest reliability notes:
- **Photos, not filenames**: the "this week last year" photos are attached as
real image files (up to 4; oversized phone photos are downscaled, missing
files are skipped quietly).
- **Tidy by design**: near-duplicate reminders are collapsed, reminders about
the same plant are grouped under one heading, and text is never cut
mid-word. If a busy day would exceed Discord's 2000-character limit, the
least-urgent sections (AI briefing, then time-travel) are dropped instead of
failing the send.
- **Skips are handled**: the webhook send retries once on transient failures;
a failure in one section degrades to a partial digest rather than no digest;
and if the app restarts past send time with no successful send recorded that
day, a catch-up digest goes out immediately. The Settings page shows "Last
digest sent" (and the last error, if any) next to the digest controls.
### π Immich integration
Browse albums on your own Immich server and import their photos straight into
Verdant β no downloading and re-uploading. Needs `IMMICH_BASE_URL` and
`IMMICH_API_KEY` (see [Configuration](#configuration)).
## Tips & tricks
A few workflows that aren't obvious until you've lived in the app a while:
- **NFC-tag everything you touch.** A tag on the fertilizer bottle opens the
feeding form with that product pre-selected; one on the seed binder opens
"add packet"; one per grow bag opens that plant's profile. Write the tag's
URL once with any NFC writer app β no app needed to read them.
- **Quick Log belongs on your phone's home screen.** It's built for dirty
hands: one-tap watering per location, a harvest stepper, and it works fine
as a home-screen bookmark over your LAN.
- **Match photos with the keyboard.** On the π― Match photos page, `β`/`β`
flip through photos and number keys pick the plant β a 600-photo album goes
fast. Bulk-assign a whole day when you know what it is.
- **Merge duplicates after an Immich import.** If an old import ran twice,
π§Ή Merge duplicates folds the copies into the fullest album instead of you
deleting things by hand.
- **Tag costs to a plant.** The Season scorecard can only split spending per
variety if purchases are tagged β do it at entry time and the year-end
"was it worth growing?" math is free.
- **Photograph the back of the packet too.** The growing info on the reverse
is the part you actually need in April; the β flip button keeps both sides
on the packet card.
- **Copy the season in the planner.** January: copy last season's layout,
drag things to their new spots, and the rotation check tells you where you
planted the same family last year.
- **CSV imports are re-runnable.** The importer skips rows it already has
(exported IDs are preserved), so re-importing a fixed file never creates
duplicates. Import in the suggested order: locations β plants β the rest.
- **Database-only backups are fast.** Uncheck the photos box for a small zip
you can grab before every upgrade; keep a full one with photos somewhere
safe monthly.
## Quick start (Docker β recommended)
The included `docker-compose.yml` is a plug-n-play base setup β the prebuilt
image from GitHub Container Registry, two folders for your data, nothing else
to configure:
```bash
docker compose up -d
```
Open . Interactive API docs: .
That's genuinely the whole setup: no `.env` file needed, no build step, and
your data lives in the `./data` and `./uploads` folders next to the compose
file, so it survives rebuilds and restarts. Paste the same file into Dockge
or Portainer and it just works there too.
### πͺ Windows installer (.msi)
No Docker? Grab `Verdant--x64.msi` from the latest
[release](https://github.com/Elyld/Verdant/releases), double-click it, and
follow the prompts β no Python or Docker needed. It installs Verdant to
Program Files, adds a Start menu shortcut, and opens your garden journal at
. Your garden data lives in your own
`%LOCALAPPDATA%\Verdant` folder, so uninstalling never touches it and
installing a newer `.msi` upgrades cleanly. The installer is built
automatically with every release (see `installer/`).
### Reaching Verdant outside your LAN (Cloudflare Tunnel)
Verdant is tunnel-ready: no sign-on inside the app, Cloudflare handles
identity at the edge.
1. In the Cloudflare dashboard: **Zero Trust β Networks β Tunnels** β create
a tunnel, add a public hostname pointing at `http://garden:8000`, and copy
the tunnel token.
2. In `docker-compose.yml`: comment out the `ports:` block on the garden
service (so the app is *not* on your LAN directly β only the tunnel can
reach it), and uncomment the `cloudflared` service at the bottom.
3. In your `.env`: `TUNNEL_TOKEN=` and `TUNNEL_MODE=true`.
4. `docker compose up -d`.
5. Add a **Cloudflare Access** policy (e.g. email one-time PIN) on the
hostname β that's the sign-on, handled by Cloudflare, not Verdant.
`TUNNEL_MODE=true` flips the secure preset: HSTS header on, `/docs` off, and
the tunnel's proxy headers trusted so rate limiting sees real client IPs.
(Keep `ports:` if you still want LAN access too β the tunnel keeps working,
but LAN clients bypass Cloudflare Access.)
To change the host port, point Verdant at your Immich server, or turn on the
Discord digest, copy `.env.example` to `.env` and fill in what you need, then
re-run `docker compose up -d`.
The image (`ghcr.io/elyld/verdant:latest`, also tagged per release) is published
automatically by the `Publish Docker image` workflow on every push to `main`;
it builds for both `linux/amd64` and `linux/arm64` (handy for a Raspberry Pi).
Works great in Dockge, Portainer, or plain `docker compose`.
### Persistence
`docker-compose.yml` bind-mounts two host directories, so nothing is lost on
rebuild or restart:
| Host path | Container path | Contents |
|-------------|----------------|---------------------------|
| `./data` | `/data` | `garden.db` (SQLite, WAL) |
| `./uploads` | `/uploads` | Uploaded images |
### Upgrading
Pull and recreate β your data lives in the bind-mounted folders, not the
image:
```bash
docker compose pull && docker compose up -d
```
On startup Verdant automatically adds any missing database columns, so old
databases (even v1.x) upgrade cleanly without manual migrations.
## Quick start (local, no Docker)
```bash
python3 -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --reload
```
Open .
## Configuration
All optional β Verdant runs fine with none of these set. Copy `.env.example`
to `.env` (or set them in your Dockge stack) for the ones you want.
| Variable | Default | Meaning |
|----------|---------|---------|
| `GARDEN_PORT` | `3113` | Host port for the web UI (compose only) |
| `GARDEN_DATA_DIR` | `./data` | Directory holding `garden.db` |
| `GARDEN_UPLOAD_DIR` | `./uploads` | Image storage root |
| `GARDEN_DATABASE_URL` | `sqlite:////garden.db` | Full DB URL override |
| `GARDEN_PUBLIC_URL` | `http://localhost:3113` | Base URL used to resolve relative URLs in "Import from URL" |
| `IMMICH_BASE_URL` | (unset) | Your Immich server URL, e.g. `http://192.168.0.50:2283` (enables Immich import) |
| `IMMICH_API_KEY` | (unset) | Immich API key (Account Settings β API Keys). Needs `album.read`, `asset.view`, `asset.download` |
| `GARDEN_LAT` / `GARDEN_LON` | (unset) | Fallback for your garden's coordinates β prefer the Settings page (π Use my location button). Stamps new observations with current weather + powers the site-wide weather ribbon (free Open-Meteo data, no key needed) |
| `DIGEST_ENABLED` | `false` | Set `true` to enable the morning Discord digest |
| `DISCORD_WEBHOOK_URL` | (unset) | Discord webhook URL (Server Settings β Integrations β Webhooks) |
| `DIGEST_TIME` | `08:00` | When the digest sends (24h `HH:MM`, in `DIGEST_TIMEZONE`) |
| `DIGEST_TIMEZONE` | (unset) | IANA timezone for the digest send time, e.g. `America/Chicago`; blank = server local |
### Immich tips
- If Verdant runs in Docker on the same machine as Immich, use
`http://host.docker.internal:2283` (not a bare LAN IP) as
`IMMICH_BASE_URL` so the container can reach back to your host.
- The API key needs **`asset.download`** in addition to `album.read` /
`asset.view` β without it, imports list albums fine but every photo fails
with a 403. Verdant tells you exactly this in the UI if it happens.
- Import captures each photo's **XMP keyword tags** (Immich reads XMP
sidecars into tags) plus date taken, camera, and GPS, so the Match page can
show you what each photo is while you assign it.
### Weather stamping
Set your garden coordinates on the **Settings page** (preferred β there's a
π *Use my location* button that fills them in from your browser, falling back
to a one-time city-level lookup via ip-api.com when you click it) or with the
`GARDEN_LAT` / `GARDEN_LON` env vars (kept as a fallback; Settings win). Find
coordinates by hand by clicking your spot on the map at
. Every new observation is then stamped with the
temperature and conditions at log time, and the Planner shows its weather
strip. Free, no API key.
### Discord digest
1. In Discord: Server Settings β Integrations β Webhooks β New Webhook, pick
a channel, copy the webhook URL.
2. On Verdant's Settings page: tick the π
Morning digest box, paste the URL,
pick a send time and timezone (blank = your browser's timezone).
Or use env vars: `DIGEST_ENABLED=true`, `DISCORD_WEBHOOK_URL`,
`DIGEST_TIME`, `DIGEST_TIMEZONE`.
3. Save β no restart needed. Every morning you get overdue / due-today /
coming-up care tasks.
4. Test anytime: `GET /api/digest/preview` to see the message,
`POST /api/digest/send` to send one on demand.
## Importing from CSV
The Import page (`/import`) accepts CSVs exported from other garden trackers.
Suggested order (so linked records resolve): **Locations β Plants β
Fertilizers β Seed sources β Watering β Fertilization β Harvests.**
Expected columns (headers are matched case-insensitively; extra columns are
ignored):
| Import | Columns |
|--------|---------|
| Locations | `location_id`, `name`, `type`, `light`, `notes` |
| Plants | `plant_id`, `variety_name`, `species`, `category`, `status`, `location` (name or id), `date_planted`, `date_started_indoors`, `days_to_maturity`, `light`, `notes`, `water_every_days`, `feed_every_days` |
| Fertilizers | `fertilizer_id`, `name`, `npk_ratio`, `best_for`, `notes` |
| Seed sources | `source_id`, `source`, `variety`, `type`, `acquired_date`, `notes` |
| Watering logs | `log_id`, `date`, `location`, `plant`, `method`, `amount` (parsed to number + unit when it looks like one, e.g. `2 gal`), `notes` |
| Fertilization logs | `log_id`, `date`, `fertilizer`, `npk_ratio`, `amount_used` (free text), `plant`, `notes` |
| Harvests | `harvest_id`, `date`, `plant`, `quantity`, `unit`, `weight`, `weight_unit` (oz/g/lb/kg β defaults to oz) |
| Invoices | `vendor`, `order_number`, `order_date`, `total`, `items`, `notes` |
Notes:
- **Preview first.** Every upload shows row counts, warnings, and sample rows
before anything is written. Harvest rows without a recognizable plant get a
per-row plant picker in the preview.
- **IDs are preserved.** If your CSV has IDs like `LOC-01` or `PL-042`,
they're kept β and re-importing the same file skips existing rows instead
of duplicating them.
- Blank rows are ignored; UTF-8 files with a BOM work fine.
## Backup & restore
**Backup:** open the Backup page (`/backup`) β Download. You get a zip with
`data.json` β every table's rows, all 30+ of them, auto-discovered so new
features are covered without anyone remembering a list β plus a `files/`
tree with every uploaded photo, packet scan, and invoice PDF (uncheck the
box for a tiny database-only backup). Store one before every upgrade.
**Restore:** on the same page, upload a backup zip. Verdant replaces the
database with the backup's contents; uploads are replaced too, unless the
backup was made without photos β then your current photos are left alone.
The zip is fully validated before anything is touched, so a corrupt file
can't wipe your garden.
There's also a JSON export per table via the API (see `/docs`) if you'd
rather script it.
## API
Full interactive docs at `/docs` when the app is running. Highlights:
| Method | Path | Purpose |
|--------|------|---------|
| `GET` | `/api/plants` | List plant profiles (with care status) |
| `POST` | `/api/plants` | Create a plant |
| `GET/PATCH/DELETE` | `/api/plants/{id}` | Read / update / delete |
| `GET` | `/api/plants/reminders` | Overdue / due today / coming up care tasks |
| `GET/POST` | `/api/observations` | List / log an observation (+ photos) |
| `GET` | `/api/stats/calendar` | Day-by-day activity for the heatmap |
| `GET/POST` | `/api/fertilizations` | Fertilization log |
| `GET/POST` | `/api/watering` | Watering log |
| `GET/POST` | `/api/harvests` | Harvest records |
| `GET/POST` | `/api/seed-sources` | Seed sources |
| `GET/POST` | `/api/locations` | Garden locations |
| `GET/POST` | `/api/albums` | Photo albums |
| `POST` | `/api/albums/{id}/import` | Import photos (upload or URL list, batched) |
| `GET` | `/api/immich/albums` | List albums on your Immich server |
| `POST` | `/api/immich/albums/{id}/import` | Import an Immich album (batched, idempotent) |
| `POST` | `/api/import/preview` | Preview a CSV import |
| `POST` | `/api/import/run` | Run a CSV import |
| `GET` | `/api/backup/download` | Download full backup zip |
| `POST` | `/api/backup/restore` | Restore from backup zip |
| `GET` | `/api/digest/preview` | Preview the morning Discord digest |
| `POST` | `/api/digest/send` | Send the digest now |
| `GET` | `/api/stats/review` | Season-in-review aggregates |
| `GET` | `/api/health` | Healthcheck (includes version) |
### API tokens (for Hermes / scripts)
Verdant's API needs no credentials from the web UI β it runs on your own
server behind your own network. For outside programs (a local assistant on
the same PC, a cron script), create a **scoped API token** on the Settings
page (π API tokens section):
1. Give it a name (e.g. `Hermes`) and tick the scopes it needs.
2. Copy the secret β it's shown **once** and never stored in plaintext
(only a SHA-256 hash is kept).
3. Send it as `Authorization: Bearer verdant_β¦` on every request.
The rule: **no header β the request works exactly as the web UI does;
header present β the token must be valid and carry the needed scopes.**
A bad or revoked token gets `401`; a valid token without the scope gets `403`.
| Scope | Allows |
|-------|--------|
| `*` | Full control β everything, read and write |
| `read` | All GET endpoints |
| `invoices:write` | Create/edit/delete invoices (incl. packet links) |
| `seed_packets:write` | Create/edit/delete seed packets (incl. photos) |
| `plants:write` | Create/edit/delete plants |
| `harvests:write` | Log/edit/delete harvests |
| `expenses:write` | Create/edit/delete expenses |
| `observations:write` | Log/edit/delete observations (incl. photos) |
| `watering:write` | Log/edit/delete watering & fertilization logs |
Example for a shell script:
```bash
TOKEN="verdant_paste_your_secret_here"
curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8000/api/invoices/ | head -c 200
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"vendor":"Baker Creek","total":42.50}' http://localhost:8000/api/invoices/
```
Revoke a token anytime on the Settings page β programs using it stop
working immediately. Token endpoints: `GET/POST /api/api-tokens/`,
`POST /api/api-tokens/{id}/revoke`, `GET /api/api-tokens/scopes`.
## Upload security
- File type is decided by **magic bytes**, not by the client's filename or
`Content-Type` (jpg, png, gif, webp, bmp only β otherwise `415`).
- Stored filenames are random hex, so a hostile filename cannot traverse
directories or overwrite anything (`../../etc/passwd.png` becomes
`/uploads/posts/3/.png`).
- 8 MB cap per file, enforced while streaming; partial files are removed on failure.
- Deletes are confined to the uploads root by a resolved-path check.
## Layout
```
verdant/
βββ app/
β βββ main.py FastAPI app, page routes, static + /uploads mounts
β βββ version.py single source of truth for the version
β βββ database.py engine, session dep, init_db (WAL, FK pragma, auto column sync)
β βββ models.py SQLModel tables
β βββ schemas.py request/response models
β βββ storage.py secure image save/delete
β βββ digest.py morning Discord digest builder
β βββ routers/ posts, observations, plants, locations, fertilizations,
β β watering, harvests, seed_sources, albums, immich,
β β stats, backup, import_csv, digest
β βββ static/ app.js (vanilla JS, no build step), styles
β βββ templates/ one standalone HTML page per tab
βββ docs/screenshots/ README screenshots (demo data)
βββ tests/ pytest API tests + JS unit checks
βββ Dockerfile
βββ docker-compose.yml GHCR image, host volumes, healthcheck
βββ .env.example every supported setting, documented
```
Palette: **sage green**, **navy blue**, **beige** β everywhere, always.
## Data model
- **locations** β `id, location_id, name, type, light, notes`
- **plants** β `id, plant_id, variety_name, species_type, category, status, location_id, date_planted, days_to_maturity, water_every_days, feed_every_days, notes`
- **observation_logs** β `id, date, plant_id, plant_name, health_scale (1β10), watering_status, pest_sightings, notes, temp_c, weather_summary`
- **fertilization_logs** β `id, date, fertilizer_id, fertilizer_name, npk_ratio, amount_used, plant_id, notes`
- **watering_logs** β `id, date, location_id, plant_id, method, amount, notes`
- **harvests** β `id, harvest_id, date, plant_id, quantity, unit, weight_grams, notes`
- **fertilizers** β `id, fertilizer_id, name, npk_ratio, best_for, notes`
- **seed_sources** β `id, source_id, source, variety, type, acquired_date, linked_plant_id, notes`
- **posts / post_images** β markdown blog posts with photo galleries
- **albums / album_images** β photo albums (uploads, URL imports, Immich imports)
Deleting a post, observation, or album cascades to its images and removes the
files from disk.
## Tests
```bash
pip install pytest httpx
pytest -q
```
Covers CRUD across every router, multi-image upload and static serving, cascade
file cleanup, non-image and traversal-filename rejection, validation bounds,
the stats aggregates, backup/restore round-trips, CSV import (preview + run,
idempotency, dedup), the digest builder, and the automatic DB column upgrade.
JS checks run separately β see `tests/test_photos.js`, `tests/test_import.js`,
`tests/test_day_modal.js`.
## Troubleshooting
### Immich import says "Imported 0 photos"
Your API key is almost certainly missing the **`asset.download`**
permission. Go to Immich β Account settings β API keys β edit your key and
tick `asset.download` (you need `album.read` and `asset.view` too). Verdant
now surfaces this directly in the import toast instead of failing silently.
### Immich section doesn't appear in the UI
Set `IMMICH_BASE_URL` and `IMMICH_API_KEY` in your `.env` (copy from
`.env.example`), then `docker compose up -d` again. If Verdant runs in Docker
on the same machine as Immich, use `http://host.docker.internal:2283` β not a
bare LAN IP β so the container can reach back to your host.
### Old database, new version: tabs error after upgrading
Fixed in v2.4.1+: Verdant adds missing columns automatically on startup, so
databases from v1.x upgrade cleanly. If you're on an older image, just pull
the latest.
### Changes not taking effect after editing compose/.env
`docker compose down && docker compose up -d` β containers don't pick up env
changes on a plain restart. Then hard-refresh the browser (Ctrl+F5).
### The digest never arrives
Check `DIGEST_ENABLED=true` is set (it's off by default), the webhook URL is
correct, and the container's clock/timezone matches yours β `DIGEST_TIME` is
server local time. Test with `POST /api/digest/send`.
## Changelog
- **2.64.0** β **π·οΈ Photo tagging & attribution.** Photos can be tagged
*and* linked: `tags` (array), `notes`, and `container_id` are writable on
PATCH `/api/album-images/{id}` and POST `/api/album-images/bulk-assign`
without disturbing plant/seed-packet links, plus a `?tag=` filter (exact
token, case-insensitive) and `GET /api/album-images/vocabulary` with the
seeded 10-tag vocabulary. Match page gains tag checkboxes, a notes field,
a container picker, and tag chips; the slideshow shows chips + notes.
Also: container POST honours `grid_x`/`grid_y` (422 on overlap); plants
gain a Culled status + `status_changed_at`; observations allow an unset
`health_scale`; pests gain severity + auto-stamped resolved date; Photos
page warns on duplicate album names; Immich gains sync-all with
`last_synced_at` per album and the batch import resolves the local album
by source when `album_id` is omitted. Planner L/U shapes and the
plant-row suggestion screen are deferred (see the PR description).
- **2.63.0** β **A gardener walks your 3D garden.** Walk mode grew a
third-person character: a warm clay-tinted Quaternius figure (CC0, 8 real
animation clips) in a procedural straw hat, ~5.5ft tall, strolling the
diorama with an over-the-shoulder follow camera. **V** toggles first-person
β third-person (the character hides in first person), **Space** jumps with
light custom physics, and **F** tends the nearest bed within range: the
Working clip plays, then thirsty beds get a blue water-particle arc (posted
to the real watering-log API), harvest-ready crops pop with a floating
"+N crop" sprite (posted to the real harvests API), otherwise a friendly
"nothing needed here" toast. Idle/Walk/Run crossfade with movement, the
character faces its walk direction, and the same smooth accel/damping +
wall-slide collision moves it β it can't walk through beds. Day/night
lighting tints it like everything else.
- **2.62.0** β **Real 3D models in the planner + first-person fixes.** The
diorama now uses 53 curated CC0 models (Kenney Nature Kit, Quaternius):
wooden picket fences (auto-joining runs, L-corners, gates), stone path
tiles, hexagonal planters, an oak fruit tree, and β the big one β **real
crop plants that grow through the season**: corn goes seedlingβmature over
four stages picked from each planting's dates, carrots/melons/pumpkins/
turnips/leafy greens/grains get their own models, and **harvest-ready
plantings show the actual produce** (tomato, carrot, corn, pumpkin, pepper)
on the bed. Planted beds get a garden-sign name stake plus a little
deterministic corner decor (bushes, flowers, rocks, mushrooms). Every model
falls back to the procedural version if it ever fails to load.
**Cattle-panel arch rebuilt the way real panels work**: long tunnels are now
a series of ~4ft panel arches with small gaps (not one stretched tube), and
the wire mesh is finer β walking through one in first person finally reads
right. **Walk mode decluttered**: floating name labels are orbit-only now;
in first person you get just the label of whatever's under your crosshair
within ~10ft. **Movement is smooth** (velocity accel/damping, wall sliding
without hard stops or lunge), the look control has a sane sensitivity, and
there's a **βΆ fullscreen toggle** for the 3D view (Esc exits).
Archway asset scout: no fitting CC0 archway turned up (castle-kit pieces
are externally textured and stylistically wrong), so none were forced in.
- **2.61.0** β **Creative-mode building in the planner, reskinned as a soft
miniature diorama.** Walk mode grew a hotbar: 22 slots across three rows
(8/8/6) β 16 BLOCKS (bed, bag, pot, planter, pallet, arch, cold frame,
lamp, fence, bucket, path, rain barrel, compost bin, hoop house, fence gate,
bird bath) and 6 PLANTS (tomato, pepper, basil, lettuce, carrot, flower),
all infinite. **Left-click breaks** the aimed planting or container β
breaking a container that holds plantings asks for confirmation first, and
the ground itself can't be broken. **Right-click places** the selected item
on the aimed grass cells with a green/red ghost preview (per-kind
footprints: the hoop house is 4Γ8, the shed 4Γ6), or plants the selected
seedling into the aimed container. **Q** switches tabs, **1β9**/wheel picks
slots, **E** edits the aimed container. Everything goes through the real
API; the 3D scene refreshes after each change without leaving first person.
The whole 3D view is now smooth-shaded and softly textured (64px
LinearFilter canvas textures, MeshStandardMaterial) β no more blocky voxels:
rounded foliage blobs with real fruit spheres, smooth torus-arc trellises,
tapered terracotta pots, gabled shed roofs, and soft sphere-clouds.
World dressing: **βοΈ/π day/night toggle** (gradient sky shader with drifting
clouds, starfield and glowing lamps at night), plus new placeables β **π‘
lamp posts** (real point lights at night, capped at 8 for perf), **βοΈ
chain-link fence** (segments auto-connect into runs), **πͺ£ 5-gal buckets**,
**πͺ¨ stone path slabs** (flat enough to walk over), **π’οΈ rain barrels**,
**π€ compost bins**, **π hoop houses** (translucent plastic skin over
smooth ribs), **πͺ fence gates** (mesh into adjacent fence runs),
**π¦ bird baths**, **π³ fruit trees** (round canopy + apple spheres),
**ποΈ garden sheds** (4Γ6, solid), **ποΈ pergolas** (2Γ4, walk-under),
**π§± stone edging** (auto-connecting low walls), **π¦ sprinklers** (smooth
water arcs), and **π§ drip lines** (auto-connecting ground tubes). Graphics
pass: **bloom postprocessing** (lamps, sun and fireflies glow), **tall grass
+ wildflowers** scattered on empty cells (seeded, stable across rebuilds),
**fireflies** (40 points, night only), warmer sun + tuned hemisphere light.
Lively pass: saturated palette (rich grass, warm wood, punchy terracotta),
denser grass tufts clustered near beds, fuller plant clusters with vivid
fruit, **butterflies** (8, day only β flapping wings, wandering paths),
shed with steep gabled roof + window, thicker arch ribs.
Planning tools: **π measuring tape** in orbit mode β
click two ground points for a live "12.5 ft" readout (ESC clears). Game
feel: **particle bursts** when things break and tiny WebAudio **place/break
sounds** (π toggle, default on).
- **2.60.0** β **βοΈ The planner goes Minecraft.** The 3D view is now a voxel
world: 16Γ16 pixel textures with crisp NearestFilter rendering, grass-block
ground with dirt sides, plank-sided raised beds, terracotta pots, fabric
grow bags, a voxel trellis for arches, blocky clouds, and cross-quad pixel
plants per species (tomatoes grow tiny red fruit). Plus **πΆ Walk mode**:
first-person strolling with WASD + mouse, Shift to sprint, collision with
beds (but you can walk under arches), click-to-edit from the crosshair.
- **2.59.4** β **π Scoped API tokens.** Outside programs (like a local
assistant on the same PC) can now use Verdant's API with a bearer token
instead of poking the database directly. Create one on the Settings page
with exactly the scopes it needs (`*`, `read`, or per-domain write
scopes); the secret is shown once and only its hash is stored. Requests
without a token work exactly as before β the web UI is untouched.
- **2.59.3** β **β‘ Scale-audit performance fixes.** A simulated commercial
grow (2,800 plants, 40,000 logs, 500 seed packets) found the Plants page
taking 104s and the Costs page 22s to load. Fixed: a reminders-endpoint
N+1 (82.7s β 0.25s, output verified byte-identical), a per-card cover-photo
request storm (one bulk `/api/plants/covers` call now), half a million
eagerly-rendered packet checkboxes on the Costs page (now built lazily on
open), and ~2,200 per-row packet-link requests (bulk endpoints). Plants
loads in ~8s, Costs in ~8s. Everything else scaled fine.
- **2.59.2** β **ποΈ Honest eyes.** The garden agent's prompt used to tell the
model "you CAN see" an attached photo even when the configured model is
text-only β a setup for hallucinated photo descriptions. Verdant now checks
what the model actually supports (Ollama's `/api/show` capabilities, or
OpenRouter's input modalities) and, when it can't see, the prompt says so
plainly and asks you to describe the photo instead.
- **2.59.1** β **π Simulation-audit fixes.** Three bugs found by the 3-year
gardening simulation: the garden agent's chat was sending your message to
the model twice on every turn (silently doubling token use); harvest CSV
imports without a "Log ID" column silently dropped the plant assignments
you picked; and the new 12-month calendar rendered its heat legend twelve
times instead of once.
- **2.59.0** β **βοΈ Crop packet photos, π
a real calendar, one Season to rule them all.**
The Seeds catalog gains a crop tool: open a packet, hit **βοΈ Crop** on the
front or back photo, drag the area you want, and it replaces the packet's
saved photo (your library/Immich original is never touched) β no more
re-importing when a scan needs straightening. The Calendar page now shows
all 12 months of the selected season instead of a fixed 3-month window, and
the header **Season** picker is finally the single source of truth: Calendar,
Review, and Planner all follow it (Review's and Planner's own year controls
stay in sync both ways). The header itself got tidier β slimmer, frost badge
inline with the title instead of dangling below it.
- **2.58.0** β **π― Match photos to seed packets too.** The Match page's
"This photo showsβ¦" dropdown was plants-only β now it's two groups,
**πΏ Plants** and **π« Seed packets**, so a photo can be claimed by the
packet it came from. Plant and packet are mutually exclusive per photo;
assigning one clears the other, and the unassigned filter respects both.
The `seed_packet_id` filter joins `GET /api/album-images/` too.
- **2.57.0** β **π§ The big taskbar is gone.** The desktop nav moves out of
the header into a slim left sidebar β same five groups (**Grow**, **Track**,
**Manage**, **Assistant**, **Read**), each page a full-width row with room
to breathe, the current page highlighted. The header is just logo (a touch
bigger now), season picker, and the weather strip β notifications and NOAA
alerts stay up top where they were. Phones keep the same bottom tab bar.
- **2.56.0** β **π§Ύ Invoices grow up, backups you can trust.** **πΎ Backup fixed:** the backup auto-discovers every table, but
its photo detection only knew two filename columns β so **seed packet
photos were silently left out of every backup**. Photo detection now
covers any `*_path` column, the same "no list to forget" philosophy as the
tables. If you downloaded a backup before this fix, your packet photos
weren't in it β grab a fresh one. **ποΈ Season year:** a year picker now
lives in the header and the whole app follows it β costs drop to $0 for a
fresh year, plants and garden logs show that season's entries, and last
year's data is one click away instead of deleted. Seeds stay
year-independent, as they should. The endless Garden Logs histories are
also **paged** (25 at a time, newest first) instead of scrolling forever.
Then, four accounting-workflow upgrades
on the Costs page and around it. **Search, filter, sort:** the invoice list
now searches vendor, order number, and items together, filters by source
(manual / CSV / Gmail), and sorts by date, vendor, or total
(`GET /api/invoices/` gains `q`, `source`, `sort`, `order`). **Editing:**
invoices finally have an edit button β the form fills in, PATCHes on save,
and cancels cleanly, just like expenses always could. **π· Receipt scanning:**
not every purchase comes through email, so the invoice form has a **Scan
receipt** button: photograph a paper receipt and the vision model reads the
vendor, date, order number, total, and items into a review card β one tap
fills the form, nothing is saved until you confirm
(`POST /api/invoices/scan`, new `app/receipt_scan.py`; uses the same AI
settings as the garden agent, so there's nothing new to configure).
**π± Plants link to seed packets:** the plant form has a "Grown from seed
packet" dropdown, so a plant traces back to its packet and through it to
the invoice that bought it (`Plant.seed_packet_id`; the plant profile shows
the link). **πͺ Vendors, not sources:** the redundant Seed Sources tab is
now a read-only vendor directory built from the stash β every place your
seeds came from, with packet counts and a jump into the stash. The old
add/edit/delete source form is gone; the underlying data is untouched.
- **2.55.1** β **πΏ The real logo.** Verdant finally has its logo β the
navy V over fern fronds, designed by Kate β and it now appears everywhere
the placeholder used to: header, footer, favicon (plus an
`apple-touch-icon` for phone home screens). The old
`logo-placeholder.svg` is gone.
- **2.55.0** β **β
Books integrity check.** The Costs page now carries an
integrity badge that answers "do these numbers actually add up?". Two layers,
one report (`GET /api/books/check`): a **structural audit** that runs live
against Verdant's own records β invoices with no backing expense, invoice
totals that disagree with their linked expense, $0 expenses, unknown
categories, invoices that came from email but lost their link, and duplicate
order numbers β plus an **email-vs-books reconciliation** posted by the host
mail reconciler (`POST /api/books/ledger`, stored in the new `book_checks`
table). The badge colour is the worst severity present (error/warn/info) and
expands to a per-finding list, each linking to the invoice or expense to fix.
New `app/books_audit.py`, new `app/routers/books.py`.
- **2.54.0** β **π± Invoices grow the seed stash.** An invoice's line items can
now be turned into seed packets in one click: on the Costs page, **β seed
packets from items** reads the invoice's items, normalizes each variety
(handling the Seed Savers "Category, Variety", Territorial "- SEED / size",
and "β 50 Seeds" formats), infers a category, and shows a preview before
writing anything. Varieties already in the stash β including near-matches
like "Patio Baby" vs "Patio Baby Eggplant" β are **linked** to the existing
packet instead of duplicated; genuinely new ones are **created** and linked
to the invoice, so each packet carries its order date, vendor, and cost
provenance. Containers, grow bags, and supplies are never turned into
packets. New `POST /api/invoices/{id}/derive-packets` (preview by default,
`apply: true` to write; idempotent). New `app/seed_extract.py` extractor.
- **2.53.0** β **π± The assistant grows a memory.** The chat agent is now a
persistent agent instead of a stateless chatbot: it keeps three files it
reads every turn β **Persona** (who it is), **Operating notes** (lessons
about your garden), and **Memory** (durable facts) β editable on the new
tab or via the `propose_memory_write` chat tool (confirm-before-save, as
always; the agent can never write silently). Conversations are full
**threads** stored server-side, and every draft persists until you confirm
or discard it β a browser refresh loses nothing. The floating π± chat
bubble is gone, replaced by a proper **Garden Assistant tab** (nav β
Assistant) with a thread list, a "β³ Waiting on you" draft strip, and an
"About this agent" section. New `agent_files`, `agent_conversations`,
`agent_messages`, and `agent_drafts` tables (auto-migrated) plus
`GET/PUT /api/agent/files` and the `/api/agent/conversations|drafts`
endpoints.
- **2.52.0** β **π² Chaos garden pick.** One random experimental plant per year,
always something you've never grown: 32 curated oddballs (cucamelon, salsify,
orach, luffa, crosneβ¦), with the whole growing history and every prior year's
pick excluded. New π² Chaos pick card on the Order assistant tab with Reroll
(never repeats within the year) and "Add to my order" (lands on the order
list as a checked wishlist item); `GET/POST /api/chaos-pick` endpoints; new
`chaos_pick` chat tool ("what's my chaos pick?", "reroll my chaos pick" β
reroll confirms in chat before replacing). No LLM needed, works offline.
- **2.51.0** β **π€ͺ The "wild ideas" batch.** Seven outside-the-box features,
all advisory, all wired into the chat assistant as new tools:
**ποΈ Vacation caretaker sheet** β a printable, phone-friendly handoff page
(`/api/caretaker-sheet`, button on the Today page) with what to water, what's
ready to pick, pests to watch, and your notes + emergency contacts;
**π°οΈ This week last year** β the morning digest now closes with what you were
doing this same week last year (notes, harvests, photos), and the chat can
answer "what did I do this time last year?";
**π± Growth stall detector** β ask "is my tomato still growing?" and the chat
compares the plant's recent photos with your vision model to spot a stall
and suggest likely causes;
**π² Frost-night gamble** β when frost threatens tonight the digest carries
one bold call (π± COVER or π§Ί HARVEST NOW), triggered by tonight's forecast
low β€ 38 Β°F or a first-frost date 0β3 days out;
**π Season yearbook** β a π Yearbook button on the Review page downloads a
keepsake PDF of the season (stats, per-variety tables, photo pages);
**π° True cost** β a new Review section (and chat tool) that answers "was it
cheaper than the grocery store?": homegrown $/lb vs. honest grocery-store
estimates, per variety, with fun verdicts;
**π Plant autopsy** β when a plant dies, the chat plays coroner first (up to
3 quick questions: what did it look like at the end? sudden or gradual?
weather or pests involved?) then logs a structured autopsy note via the new
`record_autopsy` tool; death notes attach to the variety's performance
history so next year's "grow again?" decision remembers what killed it.
- **2.50.0** β **π Dated reminders.** The gap the gardener hit: the chat
could save memory notes but had no way to schedule a reminder β it would
say "I'll try to remind you" with no tool to do it. Fixed for real: new
`set_reminder` write tool (confirm-before-save, like everything else) β
"remind me to plant carrots on Oct 12" now drafts a real dated reminder
you confirm in the chat. Reminders live in a new `user_reminders` table
(auto-created on startup), surface proactively in the chat's garden context
(due/overdue/next 7 days), appear in the morning Discord digest under a
π Reminders section (due/overdue first, trimmed to fit Discord's message
cap), and are listed by the new `upcoming_reminders` read tool. Past dates
and bad formats are rejected with a friendly message. Honesty fixes: the
note draft card now says "Saves to Garden Logs under Notebook" so it's
clear where memories live, and `save_memory_note`'s description says
plainly that memories are notes, not reminders.
- **2.49.0** β **π Season recap.** Your garden photos tell the story: a new
`season_recap` chat tool (and a π Season recap button on each plant's
detail card) gathers a plant's photos across the season β Immich album
matches plus observation photos β picks up to 8 spread across the months,
and asks your vision-capable chat model to narrate it: growth milestones,
health observations, harvest moments, in a warm neighbor tone. The card
view shows the story with its photo timeline; βSave as noteβ files it as
an observation, confirm-before-save as always. Read-only otherwise, and it
degrades gracefully when a plant has no photos or the model can't do
vision. Privacy: photos only ever go to your already-configured LLM
provider β the same one the chat's photo-vision feature uses. (No new
screenshots: one small button + narrative.)
- **2.48.0** β **π€ AI powerhouse.** The assistant got eyes, a memory, and a
coaching voice. **Eyes:** attach a photo to a chat message and the model
can see it β pest ID, ripeness, plant health (works with vision models like
DeepSeek v4.1 flash via OpenRouter; Ollama gets base64 images best-effort).
**Morning briefing:** the Discord digest can now open with a short
AI-written briefing in a friendly neighbor tone, on top of the usual data
sections β opt-in via the new "AI briefing" checkbox in Settings (falls
back to data-only if the provider is down), and the digest now also warns
you when the first fall frost is within two weeks. **New chat tools:**
`season_advice` (frost countdown, what to plant/harvest now in zone 6b,
garlic planting reminder, seed-order hints), `save_memory_note` /
`recall_notes` (a confirm-before-save memory notebook β nothing saved
without your OK), and `variety_performance` (year-over-year harvest totals
per variety). **Voice logging:** new `POST /api/ai/voice-log` endpoint β
the client does speech-to-text and the server turns the transcript into
confirm-before-save drafts, same as the /interpret pipeline.
- **2.47.0** β **πΏ Fern + detail pass.** The nav fern is now lush filled
fronds instead of stick-figure outlines, and the Today dashboard got a
proper header: a fern hero banner with the greeting and date, section
labels (Up next / Forecast / Season), count badges on the care and harvest
cards, richer empty states, and a "Right now" label on the weather card.
Also fixed the Windows installer build β the version number was reaching
the installer tool as literally `$version`, and it needs a 4-part version,
so the build script now quotes it and pads it (2.47.0 β 2.47.0.0) behind
the scenes. Releases ship a working .msi from here on.
- **2.46.0** β **π¬ Chat bubble, movable + π» .msi that actually ships.** The
π± assistant button can now be dragged anywhere on screen (tap still opens
the chat, position is remembered), and on phones the chat opens as a
full-height slide-over panel you dismiss by tapping outside it. Also fixed:
the Windows installer workflow never fired for auto-created releases, so no
release ever got its .msi β the release job now builds and attaches it
directly, so every release from here on ships one.
- **2.45.0** β **π
Digest fixes.** Two reasons the morning Discord digest
could silently never arrive: saving the digest settings only (re)started the
scheduler when the send *time* changed (enabling it or pasting the webhook
alone did nothing β now any digest field re-arms it), and the send time was
interpreted in the server's timezone (UTC in Docker, i.e. 3 AM Central).
The Settings page now has a timezone field that defaults to your browser's
zone, so 08:00 means 08:00 your time.
- **2.44.0** β **π± Garden assistant, smarter.** The chat agent can now look
things up on its own (per-plant care history, notes search, seed stash,
planner, reminders) and take action beyond logging: adding seed packets,
changing a plant's status, moving plants between planner containers β every
write still a draft you confirm first. Per-plant care history (last watered
/ fed / harvested) is now part of what it knows, and the OpenRouter model
field is a real dropdown listing every model with free ones marked.
- **2.43.0** β **π± Garden assistant chat.** A floating chat button on every
page (toggle on the Settings page): an always-ready agent that knows your
garden β growing plants and where they live, what's due, what's ready to
harvest, the last 14 days of activity, and today's weather. Ask questions or
tell it what you did; it drafts log entries you confirm before anything is
saved. Runs on your chosen AI provider (Ollama or OpenRouter), keeps the
conversation across page navigation within the tab.
- **2.41.0** β **π₯« Pantry & the post-harvest loop.** Log what you preserved
(canned, frozen, dehydrated, fermented, gave away, ate fresh) with amounts
in/out and storage location, optionally linked to the harvest; one checkbox
stocks the finished amount into a **pantry inventory** you decrement as you
use things up. **π Succession ideas** on the planner suggest what each
empty container can still fit before first frost β sow-by dates, rotation
aware, honest in October (it'll point you at garlic). **π§οΈ Weather-aware
care**: watering reminders hold with a rain badge when real rain is
expected, on the Today page and in the morning digest. **ποΈ Voice
quick-log**: speak "watered the tomatoes", tap Save β no AI model needed.
- **2.40.0** β **πͺ Windows installer (.msi).** No Docker? Download
`Verdant--x64.msi` from the release page, double-click, follow the
prompts. It installs Verdant to Program Files with a Start menu shortcut β
no Python or Docker needed β and opens the journal at
. Garden data lives in the user's own
`%LOCALAPPDATA%\Verdant` folder, and installing a newer `.msi` upgrades
cleanly. The installer is built automatically by GitHub Actions with every
release (PyInstaller bundle + WiX Toolset, see `installer/`); the build
script also runs by hand on any Windows machine with Python and WiX.
- **2.42.0** β **π€ OpenRouter provider for the AI features.** βTell Verdant
what you didβ now works with two providers: **Ollama** on your own machine
(unchanged default β nothing leaves your network) or **OpenRouter** cloud
models, picked from a new provider dropdown on the Settings page. For
OpenRouter you paste an API key (stored in Verdant's own database, never
shown back β leave the field blank to keep the saved key) and any model id
from openrouter.ai/models; Test connection validates the key through
OpenRouter's free key-check endpoint and reports what it finds. Interpret,
status, and the Quick Log card are all provider-aware, and the plumbing is
now a single `app/llm.py` module so future AI features don't care which
provider is behind them.
- **2.39.0** β **Local AI: model dropdown + kinder timeouts.** The model name
field on Settings is now a dropdown listing your Ollama server's installed
models when it's reachable (falls back to typing when it's not). The
interpret call also waits longer for cold model loads (120s) and says so
when it times out, instead of a bare "couldn't reach the model."
- **2.38.3** β **The interpret step shows the real error too.** When the model
call failed it just said "couldn't reach the model," hiding e.g. Ollama's
`model not found`. The toast now includes what the server actually said.
- **2.38.2** β **Local AI test actually connects now.** The status check was
POSTing to Ollama's `/api/tags`, which is GET-only β every test failed with
405 even when Ollama was fine. It uses GET now (the error-detail line from
2.38.1 is what caught it).
- **2.38.1** β **Local AI test shows the real error.** The Test connection button
used to swallow the failure and just say "couldn't connect." It now prints
the underlying error too β connection refused (Ollama isn't listening),
timed out (firewall), or a hostname that doesn't resolve β so a failed test
actually tells you what's wrong.
- **2.38.0** β **π Pest & disease guide.** The Pests page opens with a built-in
guide: 40 common vegetable-garden pests and diseases β ID signs, organic and
conventional treatments, prevention β compiled from university extension
guidance, fully offline, no API key. Search by pest or by plant, tap βLog a
sightingβ to pre-fill a log entry, and picking a plant in the log form shows
its common issues as quick-pick chips.

- **2.37.0** β **π€ Voice input for Quick Log.** The βTell Verdant what you didβ
card grew a **Talk** button for dirty-hands garden sessions: tap it, narrate
what you did, tap Stop, and your words land in the box ready for the local
model to turn into draft entries. It uses the browser's built-in speech
recognition β nothing is sent anywhere but your own browser β and it keeps
listening through pauses until you stop it. One caveat: browsers only allow
it over a secure (https) connection, so on the LAN it works through the
Cloudflare tunnel, not plain http.

- **2.36.0** β **πΆοΈ 8,200-crop guide + calmer nav ferns.** The crop guide's
backbone is now **OpenPlantDB** (public domain): 8,204 edible plants β
vegetables, herbs, berries, and fruit β with variety-level detail, so
"Fatalii Pepper" and the other superhots are finally in there, each with
germination/maturity ranges, spacing, sun, and real growing directions.
The 30 curated crops keep their hand-written varieties and family info
and win any name collisions; Growstuff community medians still layer on
top where they exist, and every detail spells out its source (built-in
guide vs OpenPlantDB vs your seed stash). Latin names are searchable too β
"Capsicum chinense" finds your peppers. Also: the header fern watermark
is now one flowing, non-repeating scene tucked under the nav words
instead of a tiled pattern.

- **2.35.1** β **π§ Growstuff cache reliability fix.** The community-data
cache used `0.0` as its "stale" marker against `time.monotonic()`, which
starts near zero on a freshly booted machine β so the first minutes of a
fresh server could silently ignore cached community data. The marker is
now a proper invalidation instead.
- **2.35.0** β **π Community growing data, zone auto-detect, plant ID, and
a smarter weather card.** The crop guide now blends in **Growstuff
community data**: when real gardeners' results exist for a crop, the
days-to-maturity shown (and used in the planting calculator and harvest
estimates) is the community median β e.g. tomatoes show ~92 days from 227
gardens instead of the generic guide number β with the source spelled out
on every screen. Settings grew a **π― Detect from coordinates** button
that looks up your USDA hardiness zone from your saved garden coordinates.
The Plants page has a **π Identify** button (needs a free PlantNet API
key in Settings): snap a mystery plant, and it suggests matches with
confidence scores β a tap assigns the name. And the Today weather card
now ends with a **π§ watering-advice line** computed from real
evapotranspiration (PyETo, FAO-56) against your forecast. Also in this
release: the nav bar's fern watermark was redrawn with longer, more
detailed fronds. Also fixed: a partial settings save through the API no
longer resets the settings you didn't send to their defaults (an
explicitly sent empty value still clears that one field).




- **2.34.0** β **βοΈ Today view, Quick Log undo, planting calculator.** A new
**Today** landing page: greeting + date, π§ care-due list (overdue + due
watering/feeding), π§Ί harvest forecast from planting dates + the crop
guide (labeled estimates), weather strip, NOAA alerts, and a βοΈ frost
countdown. Quick Log saves now pop a **β© Undo** toast (~8s) β watering,
harvests, and notes can all be taken back. And the crop-lookup dialog grew
a π± **planting calculator**: pick a crop and it works back from your
first frost date (minus days to maturity and a 14-day buffer) to say
whether there's still time to sow. The blog moved to `/blog` under Read.


- **2.33.0** β **π Crop lookup: varieties + your seed stash.** The lookup now
searches 48 curated varieties (Cherokee Purple, Sungold, Marketmore 76β¦)
with their own maturity timing, matches your own seed packets (vendor, year,
and quantity shown), and every result says where it came from β the built-in
crop guide or your seed stash. Picking a variety or packet fills the plant
form, variety name included.
- **2.32.1** β **π€ Local AI connection help.** The Test connection button now
also tells you whether your configured model is actually pulled (with the
`ollama pull` command to fix it), a trailing `/v1` on the server URL is
ignored (the app adds the `/api/β¦` paths itself), and unreachable
`localhost` URLs explain the Docker gotcha: use
`http://host.docker.internal:11434` (now resolvable on Linux too, via
`extra_hosts` in the compose file) with `OLLAMA_HOST=0.0.0.0` on Ollama.
- **2.32.0** β **βοΈ Settings joins the mobile tab bar.** The bottom tab bar
now fits up to **5** of your sections, and **Settings** is one of the
defaults (Quick Log, Plants, Calendar, Planner, Settings) β no more
hunting for it on your phone. The π± *Mobile tab bar* card on the Settings
page lets you swap any of the five.
- **2.31.0** β **π Crop lookup β growing info on tap.** The plant form has a
new βLook up growing infoβ button: search the built-in crop guide (30
common garden crops, offline β no API, no key), pick a candidate, and the
form fills in sun, days to maturity, spacing, sowing depth, and germination
timing. Only empty fields are filled β anything already typed is left
alone. The guide lives in `app/data/crops.json`, so itβs easy to extend.

- **2.30.0** β **π€ βTell Verdant what you didβ β local AI logging (opt-in).**
New card on Quick Log: type a sentence like βwatered the tomatoes and
harvested 3 peppersβ and a small model running on **your own machine**
(Ollama-compatible, e.g. `qwen3:4b`) turns it into draft log entries β
plant, quantity, product, notes β which you review and confirm before
anything is saved. Nothing leaves your server. Configure it on the Settings
page (off by default): server URL, model name, and a Test connection
button. The model only ever produces drafts; the confirm step writes
through the normal log endpoints, so validation and history work exactly
as usual. When your Ollama server is reachable, the model field becomes a
dropdown listing your installed models β no typing, no typos.

- **2.29.0** β **β οΈ National Weather Service alerts in the ribbon.** The
site-wide weather ribbon now shows a severity-tinted β οΈ pill when the NWS
has active alerts for your garden point (Freeze Warning, Wind Advisory,
β¦) β click it to expand a panel with each alert's timing and details.
Free, no key (api.weather.gov), cached 15 minutes server-side, and it
degrades silently: no pill when there are no alerts, coords unset, or the
service is unreachable. Needs garden coordinates set (Settings page), like
the rest of the ribbon.

- **2.28.0** β **π§ Navigation refresh.** The desktop nav is now four labeled
groups (Grow / Track / Manage / Read) with an icon on every tab and a faint
fern watermark β no dropdown menus anywhere. On phones there's a bottom tab
bar instead of the cramped top bar: it opens with Quick Log, Plants,
Calendar, Planner + a fixed **More** tab, and you can make it yours on the
new π± *Mobile tab bar* card in Settings β tap up to 4 section chips in the
order you want them (tap again to remove; numbered 1β4), and the bar
follows. More opens a bottom sheet with every other section, never
duplicating your picks.
- **2.27.0** β **π€οΈ The weather strip goes site-wide.** The forecast ribbon
(current conditions, tonight's low, tomorrow's high, rain chance, wind gusts)
now lives under the site header on **every** page instead of only the
planner β slim, subtle, and rendered from the cached forecast endpoint, so
it's cheap. It stays hidden until your garden coordinates are set (Settings
page β no nagging), and the planner keeps its garden-alerts box, which reads
your actual container data.
- **2.26.0** β **π Garden coordinates move to Settings.** Latitude/longitude
are now first-class Settings-page fields (saved like everything else, no env
vars needed) with a **Use my location** button: it tries your browser's
geolocation first, and falls back to a one-time city-level IP lookup via
ip-api.com β click-only, never in the background, nothing stored beyond the
coordinates. Settings outrank the `GARDEN_LAT` / `GARDEN_LON` env vars (kept
as fallback), so a stale env var can never shadow what you picked in the UI.
The planner's unconfigured-weather hint now points at Settings too.
- **2.25.0** β **π Winter seed-order assistant.** New tab on the Seeds page
that pulls ordering season together: a ratings table for the stash (how old
each packet is, one-tap "grow again?" ratings β π Skip, π Grow again,
β Favorite β also editable from the packet form and shown as badges on the
stash cards), last year's seed spend grouped by vendor from your invoices,
and a winter wishlist. Each wishlist row shows the last vendor and date the
variety was ordered (resolved from past invoices) and has a checkbox; ticked
items roll up into an order list. Ratings are stored on the packet; the
wishlist is its own list with add/edit/delete.
- **2.24.0** β **βοΈ Weight in Quick Log harvest.** The Quick Log harvest
stepper now has a weight input + unit selector (prefilled from your
Preferences, like the Plants page form), so the fastest way to log a
harvest β including the NFC tap flow β captures the weight the scorecard's
by-weight board and the yield heatmap need. Weight stays optional; logging
by count alone works exactly as before. Also on the Planner page: when
GARDEN_LAT/GARDEN_LON aren't set, the weather strip now shows a small
dismissible hint explaining why instead of silently staying hidden.
- **2.23.0** β **π± Multi seed-packet tagging for invoices + expenses.** The
invoice packet picker is now a true multi-select (checkbox panel β link as
many packets as an order had), and the import preview links multiple packets
per invoice too (strong suggestions pre-checked). Expenses ("transactions")
get their own packet tagging: π± chips with one-tap suggestions and the same
multi picker on every expense row, backed by a new `expense_seed_packets`
table. Invoice and expense tags are independent β tagging one never touches
the other.
- **2.22.0** β **βοΈ Expense editing + smarter invoice categories.** Expense
rows on the Costs page now have edit and delete buttons β editing loads the
row into the log-a-purchase form (Save changes / Cancel), and deleting an
expense cleanly unlinks any invoices that pointed at it. Auto-created
expenses are also categorized by vendor now (247Garden β Supplies, seed
vendors β Seeds) with keyword fallback on the items text, instead of
everything landing in Seeds.
- **2.21.0** β **πΈ Expenses from invoices.** Creating an invoice now
auto-creates its expense row (amount, date, vendor, items as notes) so the
Costs page fills itself in β or link an existing expense by hand as before.
Older invoices get a "β Create expense" button, edits to an invoice sync
onto its auto-created expense, and deleting the invoice takes the
auto-created expense with it. CSV imports back expenses automatically too,
with re-runs still skipping cleanly.
- **2.20.0** β **π Invoice β seed packet links.** An invoice now knows which
seed packets it bought: link packets from a picker on each invoice row, or
tap a suggested match (the app guesses from the invoice's item text β you
confirm). Links are shown as π± chips and survive in backups; importing
invoices from CSV offers the best-guess packet per row right in the preview.
- **Unreleased** β Fixed: the π§Ύ Invoices section on the Costs page never
rendered its table (a script variable was used before its declaration),
so invoices now list correctly with PDF links and linked-expense badges.
The README tour gains a dedicated Invoices screenshot.
- **2.19.0** β **π§Ύ Invoices** on the Costs page. Keep the paper trail for
seed and supply orders: vendor, order date, order number, total, items,
and the PDF receipt, each optionally linked to the expense it documents.
Import them from CSV on the Import page (columns: `vendor`,
`order_number`, `order_date`, `total`, `items`, `notes` β re-runs skip
what's already there), and invoice PDFs ride along in backups.
- **2.18.0** β The visual refresh, plus a few fixes. Newsreader serif
headings and Source Sans body text, a warmer beige palette, and the classic
navy header. New β¨ Preferences card in Settings: which day the calendar
week starts on, default harvest weight unit, slideshow autoplay interval,
and a confirm-before-"Water all" guard for the Quick Log. The unguarded
bulk-delete posts endpoint is gone (`DELETE /api/posts` now returns 405;
deleting a single post still works). Fixed: `POST /api/plants/` no longer
500s when `date_planted` arrives as an ISO string β dates are coerced,
blanks become null, garbage gets a 422. Plus a pre-release audit pass over
the backend and frontend fixing assorted small glitches. The README tour
was re-shot in the new style with a new Tips & tricks section.
- **2.17.0** β Albums can now be deleted from the Photos page. Pick an album
and hit **ποΈ Delete album**: a confirm dialog names the album and its photo
count, then the album, its photo rows, and their downloaded files are all
removed. Your originals in Immich are never touched, so re-importing is
always an option; plant photo assignments from a deleted album are removed
with it. Handy when duplicates or stale metadata make a clean re-import
easier than surgery.
- **2.16.2** β Static assets (JS/CSS) now carry the app version in their URL,
so browsers fetch fresh code after every redeploy instead of running stale
cached scripts. Fixes the case where the footer showed a new version but
page behavior was still the old build's.
- **2.16.1** β The photo-library picker now works when *adding* a packet too:
pick a front/back photo on the add form and it's attached automatically when
the packet is saved (a β marks your pick until then). Previously the picker
only appeared when editing an existing packet.
- **2.16.0** β Seed packets meet the photo library. A packet's front/back
photo can now be picked from photos already in Verdant β e.g. an Immich
album of packet shots β instead of only uploading a file. New
`POST /api/seed-packets/{id}/photo-from-library` attaches a library photo
as a *copy*, so the original album import is never touched. The seed stash
gains a π· photo filter (all / with photos / without photos) for going
through which packs have pictures so far.
- **2.15.0** β Planner 2.0: the backyard builder grows up. A real **grid
system** (1 cell = 1 ft, resizable up to 60Γ60) with snap-to-grid dragging β
containers are drawn at their **true footprint**, so a 4Γ8 raised bed dwarfs
a 10-gal grow bag, and new containers land in the first free spot instead of
piling up. Containers now hold **many plants** (a bed lists everything in
it), and a **π rotation check** warns when the same plant family goes back
where it grew last season. New asset kinds: **cattle panel arches** (7 ft
tall by default, height editable per container for the 3D view) and
**pallets**. A **3D view** renders the whole plan in space β orbit around
the yard, see every bed, bag, arch, and plant, click any of them to edit.
Old free-form positions migrate to the grid automatically on first load.
Then came the **intelligence batch**: a **π€οΈ weather strip** (live conditions,
tonight's low, tomorrow's high, rain chance, wind gusts β powered by
Open-Meteo, set your coordinates in Settings), which went site-wide in
v2.27.0 as a slim ribbon under the header on every page, plus **garden
alerts** that read your actual data β frost warnings
naming your tender containers, heat alerts for the thirsty pots, rain-skip
nudges when you watered recently, spray wash-off warnings, wind alerts for
the arches, and a tomato blight watch. The **π₯ Yield heatmap** tints every
container by last season's harvest weight so you can see at a glance which
spots earned their keep, and the **π± companion hints** in each container's
modal suggest good (and bad) neighbors from a curated,
extension-service-sourced list.
- **2.14.0** β Tunnel-ready security, for the day Verdant meets the internet.
Set `TUNNEL_MODE=true` and the app flips its secure preset: HSTS header on,
`/docs` off, and the tunnel's proxy headers trusted. Always-on hardening
underneath: security headers on every response, per-IP rate limiting on the
API (1200/min, health checks exempt), and the container entrypoint honors
`HOST`/`PORT`. `docker-compose.yml` ships a commented-out `cloudflared`
service β pair it with a Cloudflare Access policy and there's no sign-on
inside Verdant at all. Zero behavior change on plain LAN use.
- **2.13.0** β Measurement cleanup, so units finally mean what they say.
Harvests carry a weight **unit** (oz/g/lb/kg) alongside the weight, and the
yield **scorecard converts everything to ounces** β no more adding grams to
ounces. The Review page leaderboard is now **split into two boards**: one
ranked by weight, one by piece count, never mixed. CSV re-imports now
**backfill weights** onto existing harvest rows, and watering/fertilization
amounts are parsed into number + unit when they look like one
(e.g. "2 gal"), with the original text always kept. New **Β°F/Β°C toggle** in
Settings (weather chips honor it), planner vessels get a proper
**volume (gal/qt/L)** instead of free-text size, and seed packets track
**seed count**. Backups are now **optionally photo-less**: uncheck the box
on the Backup page for a small database-only zip (restoring one leaves
your current photos untouched). (To backfill your existing harvests,
re-upload the harvest CSV on the Import page after updating.)
- **2.12.0** β New **π± Seedlings** tab: the indoor seed-starting workstation.
Track every batch from sow to transplant β tray, location, warming mat,
grow light, cells sown β with one-tap sprout logging, germination progress
bars (including days-to-sprout), a stage pipeline (sowing β germinating β
growing β hardening β transplanted), and a stale-batch nudge if nothing
sprouts after 3 weeks. Finished batches keep their stats so next year's
setup repeats what worked. Batches can link to a stash packet.
- **2.11.0** β Seed stash: packets now hold a **back-of-packet photo** too, for
all the growing info printed on the reverse β a β flip button on the card
swaps front/back (lightbox follows along). Also, **logo placeholders** are in
place in the header, footer, and favicon: drop the real logo file into
`app/static/img/logo-placeholder.svg` and it appears everywhere.
- **2.10.1** β Seed stash follow-up: the packet vendor is now a plain text
field with autocomplete (each vendor listed once β no more repeats), and a
**π¦ Move sources to stash** button copies every seed source into the stash
as a packet, pulling the vendor link and year out of the imported notes.
- **2.10.0** β The backyard update. **πΊοΈ Backyard Planner**: lay out grow
bags, raised beds, pots and planters on a drag-and-drop canvas per season,
assign plants to containers, copy last season's layout forward. **π·οΈ NFC
Tags**: `/t/` tap links with a tag manager page β stick tags on
fertilizer bottles, spray bottles, the harvest basket, the seed binder, or
plant containers and land on exactly the right pre-filled screen; taps are
counted. **π± Seed stash**: a real seed-packet catalog (photo, vendor link,
type, year bought, quantity) as a second tab on the Seeds page. **π§ͺ
Fertilizers** finally gets a management page (name, NPK, best-for); the
Garden Logs feeding form suggests from the shelf. **βοΈ Season scorecard**
on the Review page: yield vs. spending per variety, ranked β expenses can
now be tied to a plant on the Costs page.
- **2.9.0** β Immich album dedup: re-importing an album reuses the existing
Verdant album instead of duplicating it, and **π§Ή Merge duplicates** folds
older double-imports into one (deleting the re-downloaded duplicate files).
New **π― Match photos** page: flip through imported photos one by one and
assign each to a plant (arrow-key navigation, bulk-assign a whole day's
photos at once) β assigned photos appear in a gallery on the plant's
profile. XMP sidecar keywords from Immich are captured as photo tags on
import, backfilled for existing imports via the **π Sync metadata** button.
- **2.8.0** β Settings page: pick your USDA hardiness zone once and Verdant
derives your frost dates; morning digest controls (webhook, time, enabled)
moved out of env vars into the UI.
- **2.7.0** β Tidy-up: interface cleanups and polish across the app.
- **2.6.0** β Full-screen `/slideshow` page with date/camera EXIF captions
and 3/5/10/30s intervals; yield leaderboard on the Review page; `/quick`
mobile quick-log (one-tap watering, harvest +/β stepper, "Water all" per
location); expense tracking with per-category breakdown; seed-starting
calendar on the Plants page tuned to your last frost date; pest treatment
log.
- **2.5.1** β NULL-handling fixes: pre-existing rows with NULL camera/source
fields no longer 500 the Photos tab; CSV import copes with legacy NOT NULL
constraints in old databases.
- **2.5.0** β CSV import: bring your own data from another garden tracker.
Upload β preview (counts, warnings, sample rows) β import, for locations,
plants, fertilizers, seed sources, watering, fertilization, and harvests.
Idempotent: your exported IDs are preserved and re-runs skip instead of
duplicating. New `/import` page in the nav.
- **2.4.2** β Immich import failures are surfaced instead of swallowed: the
toast names the cause, and a 403 on photo download tells you to tick
`asset.download` on your API key. Fully-failing batches stop early.
- **2.4.1** β Automatic database column sync on startup (old v1.x databases
upgrade cleanly, no manual migrations) and Immich v3 album-asset fetching.
Batched Immich imports: the 200-photo cap is gone β albums import 50 photos
per request with an `Importing⦠n/total` progress readout, idempotent so
retries never duplicate.
- **2.4.0** β Seed Sources tab: vendor-grouped cards, vendor filter,
add/edit/delete, and linking sources to plants.
- **2.3.0** β Morning garden digest via Discord webhook (overdue / due today /
coming up), with preview and send-now endpoints for testing.
- **2.2.0** β Backup & restore: full database + uploads as a zip download,
new `/backup` page in the nav.
- **2.1.0** β Garden core: plant profiles with care cadence and reminders,
harvest tracking, per-plant timelines and timelapse, season review page,
Open-Meteo weather auto-stamp on observations.
- **2.0.0** β Multi-page app (blog, observations, calendar as standalone
routes), v2 data model: plants, locations, fertilizers, seed sources,
harvests, watering logs.
- **1.3.x** β Calendar heatmap fixes; Immich integration (browse + import
server albums); security hardening (Immich secrets via env only).
- **1.1.0** β Albums: create/upload/URL-import photo collections; "Pull from
album" picker; version shown in UI + API.
- **1.0.0** β Initial release: posts, fertilization + observation logs,
multi-image uploads, Docker.
---
*Built for one garden, shared with anyone who wants their own. If you grow
something worth bragging about, the blog tab is right there.*