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

@@ -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])
}