v0.14.0 — Animals & Pets

Livestock and household pets as leaves on the Location tree, mirroring Garden.

- Animal model (species/breed/sex/birthdate/location, soft-delete) + AnimalLog
  (feeding, vet, weight, medication, breeding, acquired, died); Task.animalId for
  recurring care reminders
- API-first /api/v1/animals (CRUD) + logs, animals:read/write scopes; animals
  service shared with the coming MCP
- Animals pages: list grouped by species with auto-computed age, detail with
  facts + latest weight + care log; add/edit/remove + log entry. Nav entry.
  Backup/restore cover the new tables
- Pure age.ts (newborn/days/months/years) with tests; verified end-to-end against
  a running server (create, vet + weigh-in logs, read-back)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-16 00:23:53 -05:00
parent 777569a21a
commit 39be24c2fc
21 changed files with 979 additions and 5 deletions

View File

@@ -0,0 +1,66 @@
-- CreateEnum
CREATE TYPE "AnimalLogType" AS ENUM ('NOTE', 'FEEDING', 'VET', 'WEIGHT', 'MEDICATION', 'BREEDING', 'ACQUIRED', 'DIED');
-- AlterTable
ALTER TABLE "Task" ADD COLUMN "animalId" TEXT;
-- CreateTable
CREATE TABLE "Animal" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"species" TEXT,
"breed" TEXT,
"sex" TEXT,
"locationId" TEXT,
"birthdate" TIMESTAMP(3),
"acquiredAt" TIMESTAMP(3),
"source" TEXT,
"notes" TEXT,
"imageUrl" TEXT,
"active" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Animal_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AnimalLog" (
"id" TEXT NOT NULL,
"animalId" TEXT NOT NULL,
"type" "AnimalLogType" NOT NULL,
"date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"notes" TEXT,
"weight" DECIMAL(10,2),
"actorId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AnimalLog_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "Animal_locationId_idx" ON "Animal"("locationId");
-- CreateIndex
CREATE INDEX "Animal_species_idx" ON "Animal"("species");
-- CreateIndex
CREATE INDEX "Animal_active_idx" ON "Animal"("active");
-- CreateIndex
CREATE INDEX "AnimalLog_animalId_idx" ON "AnimalLog"("animalId");
-- CreateIndex
CREATE INDEX "AnimalLog_date_idx" ON "AnimalLog"("date");
-- CreateIndex
CREATE INDEX "Task_animalId_idx" ON "Task"("animalId");
-- AddForeignKey
ALTER TABLE "Task" ADD CONSTRAINT "Task_animalId_fkey" FOREIGN KEY ("animalId") REFERENCES "Animal"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Animal" ADD CONSTRAINT "Animal_locationId_fkey" FOREIGN KEY ("locationId") REFERENCES "Location"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AnimalLog" ADD CONSTRAINT "AnimalLog_animalId_fkey" FOREIGN KEY ("animalId") REFERENCES "Animal"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -101,6 +101,7 @@ model Location {
updatedAt DateTime @updatedAt
plants Plant[]
items Item[]
animals Animal[]
@@index([parentId])
@@index([kind])
@@ -239,6 +240,8 @@ model Task {
plant Plant? @relation(fields: [plantId], references: [id])
itemId String?
item Item? @relation(fields: [itemId], references: [id], onDelete: SetNull)
animalId String?
animal Animal? @relation(fields: [animalId], references: [id], onDelete: SetNull)
recurrence RecurrenceType @default(YEARLY)
intervalDays Int? // for CUSTOM: every N days
month Int? // 112: for YEARLY, which month
@@ -252,6 +255,7 @@ model Task {
@@index([nextDue])
@@index([plantId])
@@index([itemId])
@@index([animalId])
@@index([active])
}
@@ -395,3 +399,58 @@ model ItemLog {
@@index([itemId])
@@index([date])
}
// ---------------------------------------------------------------------------
// Animals & pets — livestock and household pets, leaves in a Location with
// their own care log (vet, feeding, weight, meds) and recurring reminders.
// ---------------------------------------------------------------------------
enum AnimalLogType {
NOTE
FEEDING
VET
WEIGHT
MEDICATION
BREEDING
ACQUIRED
DIED
}
model Animal {
id String @id @default(cuid())
name String
species String? // "Dog", "Cat", "Chicken", "Goat"
breed String?
sex String? // freeform: "Female", "Male", "Wether"
locationId String?
location Location? @relation(fields: [locationId], references: [id], onDelete: SetNull)
birthdate DateTime?
acquiredAt DateTime?
source String?
notes String?
imageUrl String?
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
logs AnimalLog[]
tasks Task[]
@@index([locationId])
@@index([species])
@@index([active])
}
model AnimalLog {
id String @id @default(cuid())
animalId String
animal Animal @relation(fields: [animalId], references: [id], onDelete: Cascade)
type AnimalLogType
date DateTime @default(now())
notes String?
weight Decimal? @db.Decimal(10, 2) // for WEIGHT entries
actorId String?
createdAt DateTime @default(now())
@@index([animalId])
@@index([date])
}