v0.11.0 — Plant types: the reusable "kind" record

A rich-but-all-optional reference record per kind of plant (care, sun, water,
bloom & harvest windows, toxic-to-pets, edible parts, spacing), distinct from a
specific planting or a group.

- PlantType model + optional Plant.plantTypeId; migration + idempotent backfill
  that seeds 247 kinds from the built-in plant library and links plantings by
  species/common name (wired into the deploy entrypoint)
- /api/plant-types CRUD; new "Plant types" section (browse by category, edit a
  kind, see every spot you've planted it)
- New plants auto-link to their type on create; plant detail shows "More about…"
- PlantType added to backup/restore; bump APP_VERSION 0.11.0 + changelog

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 23:46:07 -05:00
parent 9c0312676a
commit cdb21ed58a
20 changed files with 984 additions and 8 deletions

View File

@@ -0,0 +1,42 @@
-- v0.11.0 — Plant types (the "kind" record)
-- A rich-but-all-optional reference record per kind of plant, plus an optional
-- link from each planting. Seeded/linked by prisma/scripts/backfill-plant-types.ts.
-- CreateTable
CREATE TABLE "PlantType" (
"id" TEXT NOT NULL,
"commonName" TEXT NOT NULL,
"species" TEXT,
"category" "PlantCategory" NOT NULL DEFAULT 'OTHER',
"careNotes" TEXT,
"sun" TEXT,
"water" TEXT,
"bloomStart" INTEGER,
"bloomEnd" INTEGER,
"harvestStart" INTEGER,
"harvestEnd" INTEGER,
"toxicToPets" BOOLEAN,
"edibleParts" TEXT,
"spacing" TEXT,
"imageUrl" TEXT,
"notes" TEXT,
"active" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "PlantType_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "PlantType_commonName_idx" ON "PlantType"("commonName");
CREATE INDEX "PlantType_category_idx" ON "PlantType"("category");
CREATE INDEX "PlantType_active_idx" ON "PlantType"("active");
-- AlterTable: optional link from a planting to its kind
ALTER TABLE "Plant" ADD COLUMN "plantTypeId" TEXT;
-- CreateIndex
CREATE INDEX "Plant_plantTypeId_idx" ON "Plant"("plantTypeId");
-- AddForeignKey
ALTER TABLE "Plant" ADD CONSTRAINT "Plant_plantTypeId_fkey" FOREIGN KEY ("plantTypeId") REFERENCES "PlantType"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -126,6 +126,8 @@ model Plant {
zone String? // legacy freeform location — kept during the Location migration (dropped in Release B)
locationId String? // shared Location spine
location Location? @relation(fields: [locationId], references: [id], onDelete: SetNull)
plantTypeId String? // optional link to the "kind" reference record
plantType PlantType? @relation(fields: [plantTypeId], references: [id], onDelete: SetNull)
plantedAt DateTime? // when it went in the ground
source String? // where it came from ("Jung's Nursery", "from seed", "division from Mom's")
notes String? // general notes about this plant
@@ -141,10 +143,42 @@ model Plant {
@@index([category])
@@index([zone])
@@index([locationId])
@@index([plantTypeId])
@@index([active])
@@index([groupId])
}
// PlantType — the "kind" of plant (e.g. "Lily of the valley"), holding general
// reusable reference info. Rich but every field optional; seeded from the
// built-in plant library (src/lib/garden/plant-db.ts). Individual Plant rows
// optionally point at their type.
model PlantType {
id String @id @default(cuid())
commonName String
species String? // scientific name
category PlantCategory @default(OTHER)
careNotes String? // general care: pruning, watering, feeding
sun String? // "Full sun", "Part shade", "Shade"
water String? // "Low", "Moderate", "High"
bloomStart Int? // month 112
bloomEnd Int? // month 112
harvestStart Int? // month 112
harvestEnd Int? // month 112
toxicToPets Boolean? // null = unknown
edibleParts String? // "Fruit, young leaves"
spacing String? // "1015 ft", "18 in"
imageUrl String?
notes String?
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
plants Plant[]
@@index([commonName])
@@index([category])
@@index([active])
}
enum PlantLogType {
OBSERVATION // general note — "looking healthy", "saw first flowers"
CARE // watering, mulching, pruning, fertilizing, chop-and-drop

View File

@@ -0,0 +1,23 @@
/**
* v0.11.0 Plant-type backfill. Seeds the PlantType library from the built-in
* plant DB and links existing plantings to their kind. Idempotent — runs on
* deploy (docker-entrypoint.sh) and can be run manually:
*
* npx tsx prisma/scripts/backfill-plant-types.ts
*/
import { PrismaClient } from "@prisma/client";
import { syncPlantTypesFromLibrary } from "../../src/lib/garden/plant-types";
const db = new PrismaClient();
async function main() {
const r = await syncPlantTypesFromLibrary(db);
console.log(`Plant types: library has ${r.types} kinds; linked ${r.linked} planting(s).`);
}
main()
.catch((e) => {
console.error("Plant-type backfill failed:", e);
process.exit(1);
})
.finally(() => db.$disconnect());

View File

@@ -1,5 +1,6 @@
import { PrismaClient, PlantCategory } from "@prisma/client";
import { hashSync } from "bcryptjs";
import { syncPlantTypesFromLibrary } from "../src/lib/garden/plant-types";
const db = new PrismaClient();
@@ -87,6 +88,10 @@ async function main() {
});
}
// Seed the PlantType library and link the sample plants to their kind.
const pt = await syncPlantTypesFromLibrary(db);
console.log(`Seeded ${pt.types} plant types; linked ${pt.linked} plantings.`);
console.log("Demo seed complete.");
}