feat: T4 waves 1-2 - rule configs, schema, suggestions core, season, rule forms
- Rule configs with typed parsers and registry; Berlin day helpers - Schema: T4 enums, Suggestion table, feed vocabulary (feedStatus as ColonyCondition with legacy text mapping), split queen source, varroa sample size; migration harness for data migrations - SuggestionsService: propose/refresh/resolve/obsolete/expire, accept/dismiss/reopen incl. bulk, lock order, concurrency e2e - SeasonService: phenology-based segments, inspection/varroa due dates, queen age; TimeEventsStep with JobPartialFailure - Frontend: per-rule settings forms with visible value sources Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
7097fcd97c
commit
d99a697739
116 files changed
+11239
-801
No files matched your search
@@ -0,0 +1,19 @@
|
||||
-- T4 migration 1/2: new enum values only. PostgreSQL cannot use a value in
|
||||
-- the transaction that adds it, so everything that uses them (the Suggestion
|
||||
-- table, rule rows) comes in later migrations.
|
||||
-- AlterEnum
|
||||
ALTER TYPE "AutomationTriggerType" ADD VALUE 'INSPECTION_INTERVAL';
|
||||
ALTER TYPE "AutomationTriggerType" ADD VALUE 'QUEEN_CELLS_SWARM_CHECK';
|
||||
ALTER TYPE "AutomationTriggerType" ADD VALUE 'QUEEN_ACCEPTANCE_CHECK';
|
||||
ALTER TYPE "AutomationTriggerType" ADD VALUE 'SPLIT_SCHEDULE';
|
||||
ALTER TYPE "AutomationTriggerType" ADD VALUE 'COLONY_ENDED_CLEANUP';
|
||||
ALTER TYPE "AutomationTriggerType" ADD VALUE 'WITHDRAWAL_ENDED';
|
||||
ALTER TYPE "AutomationTriggerType" ADD VALUE 'HONEY_WATER_CONTENT';
|
||||
ALTER TYPE "AutomationTriggerType" ADD VALUE 'SEASON_AUTOPILOT';
|
||||
ALTER TYPE "AutomationTriggerType" ADD VALUE 'PHENOLOGY';
|
||||
|
||||
-- AlterEnum
|
||||
ALTER TYPE "BatchMovementType" ADD VALUE 'BOTTLING';
|
||||
|
||||
-- AlterEnum
|
||||
ALTER TYPE "CalendarSyncSourceType" ADD VALUE 'WITHDRAWAL';
|
||||
@@ -0,0 +1,71 @@
|
||||
-- T4 migration 2/2: suggestions (rule, chain and autopilot proposals the
|
||||
-- beekeeper decides on). dedupeKey is nullable and unique: Postgres allows many
|
||||
-- NULLs, so EXPIRED/RESOLVED rows release their key.
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SuggestionStatus" AS ENUM ('OPEN', 'ACCEPTED', 'DISMISSED', 'RESOLVED', 'EXPIRED', 'OBSOLETE');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SuggestionMode" AS ENUM ('PLAN', 'DO');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SuggestionSource" AS ENUM ('RULE', 'CHAIN', 'AUTOPILOT');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SuggestionIntent" AS ENUM ('INSPECT', 'SWARM_CHECK', 'QUEEN_CHECK', 'QUEEN_ACCEPTANCE', 'QUEEN_REPLACE', 'SPLIT_PLAN', 'VARROA_MEASURE', 'VARROA_RECHECK', 'VARROA_TREAT', 'SUMMER_TREATMENT', 'FEED', 'HONEY_SUPER', 'WINTER_CHECK', 'HEALTH_CHECK', 'FOLLOW_UP', 'STOCK_REORDER', 'CLEANUP');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Task" ADD COLUMN "sourceSuggestionId" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Suggestion" (
|
||||
"id" TEXT NOT NULL,
|
||||
"ruleKey" "AutomationTriggerType" NOT NULL,
|
||||
"source" "SuggestionSource" NOT NULL,
|
||||
"mode" "SuggestionMode" NOT NULL,
|
||||
"intent" "SuggestionIntent" NOT NULL,
|
||||
"status" "SuggestionStatus" NOT NULL DEFAULT 'OPEN',
|
||||
"priority" "TaskPriority" NOT NULL DEFAULT 'NORMAL',
|
||||
"relatedType" "TaskRelatedType" NOT NULL,
|
||||
"relatedId" TEXT,
|
||||
"colonyId" TEXT,
|
||||
"reasonCode" TEXT NOT NULL,
|
||||
"evidence" JSONB NOT NULL,
|
||||
"requiredPermission" TEXT,
|
||||
"dueDate" TIMESTAMP(3),
|
||||
"expiresAt" TIMESTAMP(3),
|
||||
"plan" JSONB,
|
||||
"dedupeKey" TEXT,
|
||||
"sourceEventId" TEXT,
|
||||
"decidedAt" TIMESTAMP(3),
|
||||
"decidedByUserId" TEXT,
|
||||
"decisionNote" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Suggestion_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Suggestion_dedupeKey_key" ON "Suggestion"("dedupeKey");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Suggestion_status_colonyId_idx" ON "Suggestion"("status", "colonyId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Suggestion_status_dueDate_idx" ON "Suggestion"("status", "dueDate");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Suggestion_ruleKey_relatedType_relatedId_status_idx" ON "Suggestion"("ruleKey", "relatedType", "relatedId", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Task_sourceSuggestionId_idx" ON "Task"("sourceSuggestionId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Task" ADD CONSTRAINT "Task_sourceSuggestionId_fkey" FOREIGN KEY ("sourceSuggestionId") REFERENCES "Suggestion"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Suggestion" ADD CONSTRAINT "Suggestion_colonyId_fkey" FOREIGN KEY ("colonyId") REFERENCES "Colony"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Suggestion" ADD CONSTRAINT "Suggestion_decidedByUserId_fkey" FOREIGN KEY ("decidedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
-- T4 migration 3/n: one vocabulary for the feed condition. Inspection.feedStatus
|
||||
-- was free text; it becomes ColonyCondition, like the colony card. Known words
|
||||
-- (ausreichend, niedrig, kritisch; trimmed of spaces, tabs and line breaks like
|
||||
-- JS trim, any case) map to the enum, any other text moves to the end of
|
||||
-- "notes" as 'Futter (alt): <text>'. Nothing is lost.
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Inspection" ADD COLUMN "feedCondition" "ColonyCondition";
|
||||
|
||||
-- Known words map to the enum.
|
||||
UPDATE "Inspection"
|
||||
SET "feedCondition" = CASE lower(btrim("feedStatus", E' \t\r\n'))
|
||||
WHEN 'ausreichend' THEN 'GOOD'::"ColonyCondition"
|
||||
WHEN 'niedrig' THEN 'WEAK'::"ColonyCondition"
|
||||
WHEN 'kritisch' THEN 'CRITICAL'::"ColonyCondition"
|
||||
END
|
||||
WHERE "feedStatus" IS NOT NULL;
|
||||
|
||||
-- Every other non-empty text goes to the notes.
|
||||
DO $$
|
||||
DECLARE
|
||||
moved integer;
|
||||
BEGIN
|
||||
UPDATE "Inspection"
|
||||
SET "notes" = concat_ws(E'\n', NULLIF("notes", ''), 'Futter (alt): ' || btrim("feedStatus", E' \t\r\n'))
|
||||
WHERE "feedCondition" IS NULL
|
||||
AND btrim("feedStatus", E' \t\r\n') <> '';
|
||||
GET DIAGNOSTICS moved = ROW_COUNT;
|
||||
RAISE NOTICE 't4_feed_vocabulary: % free-text feed values moved into notes', moved;
|
||||
END $$;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Inspection" DROP COLUMN "feedStatus";
|
||||
ALTER TABLE "Inspection" RENAME COLUMN "feedCondition" TO "feedStatus";
|
||||
@@ -0,0 +1,8 @@
|
||||
-- T4 migration 5/n: where the queen of a split colony comes from. Optional:
|
||||
-- an observation is never filled in for old rows.
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SplitQueenSource" AS ENUM ('RAISE_OWN', 'QUEEN_CELL', 'QUEEN');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "ColonyLineage" ADD COLUMN "queenSource" "SplitQueenSource";
|
||||
@@ -0,0 +1,5 @@
|
||||
-- T4 migration 7/n (part sampleBees): number of bees in a powdered-sugar or
|
||||
-- washing sample, so that the infestation percentage can be computed.
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "VarroaMeasurement" ADD COLUMN "sampleBees" INTEGER;
|
||||
@@ -115,6 +115,7 @@ model User {
|
||||
stockMovements StockMovement[]
|
||||
assignedTasks Task[] @relation("TaskAssignee")
|
||||
createdTasks Task[] @relation("TaskCreator")
|
||||
decidedSuggestions Suggestion[] @relation("SuggestionDecider")
|
||||
assignedTaskTemplates TaskTemplate[] @relation("TaskTemplateAssignee")
|
||||
createdTaskTemplates TaskTemplate[] @relation("TaskTemplateCreator")
|
||||
colonyMoves ColonyApiaryHistory[]
|
||||
@@ -312,6 +313,7 @@ model Colony {
|
||||
feedings Feeding[]
|
||||
harvests HarvestColony[]
|
||||
queenEvaluations QueenEvaluation[]
|
||||
suggestions Suggestion[]
|
||||
|
||||
@@index([apiaryId])
|
||||
@@index([status])
|
||||
@@ -340,16 +342,23 @@ enum LineageType {
|
||||
|
||||
// Ableger bilden (SPLIT: from=Muttervolk, to=Ableger) und Völker vereinigen
|
||||
// (MERGE: mehrere from-Zeilen auf denselben to=Zielvolk) – Abschnitt 4.4.
|
||||
enum SplitQueenSource {
|
||||
RAISE_OWN
|
||||
QUEEN_CELL
|
||||
QUEEN
|
||||
}
|
||||
|
||||
model ColonyLineage {
|
||||
id String @id @default(cuid())
|
||||
id String @id @default(cuid())
|
||||
type LineageType
|
||||
fromColonyId String
|
||||
fromColony Colony @relation("LineageFrom", fields: [fromColonyId], references: [id])
|
||||
fromColony Colony @relation("LineageFrom", fields: [fromColonyId], references: [id])
|
||||
toColonyId String
|
||||
toColony Colony @relation("LineageTo", fields: [toColonyId], references: [id])
|
||||
toColony Colony @relation("LineageTo", fields: [toColonyId], references: [id])
|
||||
date DateTime
|
||||
queenSource SplitQueenSource?
|
||||
notes String?
|
||||
createdAt DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([fromColonyId])
|
||||
@@index([toColonyId])
|
||||
@@ -447,11 +456,11 @@ model QueenColonyHistory {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
model Inspection {
|
||||
id String @id @default(cuid())
|
||||
id String @id @default(cuid())
|
||||
colonyId String
|
||||
colony Colony @relation(fields: [colonyId], references: [id])
|
||||
colony Colony @relation(fields: [colonyId], references: [id])
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
date DateTime
|
||||
weather String?
|
||||
temperature Float?
|
||||
@@ -467,13 +476,13 @@ model Inspection {
|
||||
swarmMood Int?
|
||||
gentleness Int?
|
||||
combBehavior Int?
|
||||
feedStatus String?
|
||||
feedStatus ColonyCondition?
|
||||
honeySuperStatus String?
|
||||
healthIssues String?
|
||||
varroaIssues String?
|
||||
measures String?
|
||||
notes String?
|
||||
createdAt DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
tasks Task[]
|
||||
|
||||
@@ -529,12 +538,99 @@ model Task {
|
||||
// rule see that it already left an open task for the same object instead
|
||||
// of piling up a duplicate on every further trigger.
|
||||
sourceAutomationTrigger AutomationTriggerType?
|
||||
// Set when the task was created by accepting a suggestion.
|
||||
sourceSuggestionId String?
|
||||
sourceSuggestion Suggestion? @relation(fields: [sourceSuggestionId], references: [id], onDelete: SetNull)
|
||||
createdAt DateTime @default(now())
|
||||
completedAt DateTime?
|
||||
|
||||
@@index([status, dueDate])
|
||||
@@index([relatedType, relatedId])
|
||||
@@index([sourceAutomationTrigger, relatedType, relatedId, status])
|
||||
@@index([sourceSuggestionId])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Vorschlaege (T4): Regeln, Ketten und der Saison-Autopilot schlagen vor, der
|
||||
// Imker entscheidet. Uebernehmen legt eine Aufgabe an (Task.sourceSuggestionId).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum SuggestionStatus {
|
||||
OPEN
|
||||
ACCEPTED
|
||||
DISMISSED
|
||||
RESOLVED
|
||||
EXPIRED
|
||||
OBSOLETE
|
||||
}
|
||||
|
||||
enum SuggestionMode {
|
||||
PLAN
|
||||
DO
|
||||
}
|
||||
|
||||
enum SuggestionSource {
|
||||
RULE
|
||||
CHAIN
|
||||
AUTOPILOT
|
||||
}
|
||||
|
||||
enum SuggestionIntent {
|
||||
INSPECT
|
||||
SWARM_CHECK
|
||||
QUEEN_CHECK
|
||||
QUEEN_ACCEPTANCE
|
||||
QUEEN_REPLACE
|
||||
SPLIT_PLAN
|
||||
VARROA_MEASURE
|
||||
VARROA_RECHECK
|
||||
VARROA_TREAT
|
||||
SUMMER_TREATMENT
|
||||
FEED
|
||||
HONEY_SUPER
|
||||
WINTER_CHECK
|
||||
HEALTH_CHECK
|
||||
FOLLOW_UP
|
||||
STOCK_REORDER
|
||||
CLEANUP
|
||||
}
|
||||
|
||||
model Suggestion {
|
||||
id String @id @default(cuid())
|
||||
ruleKey AutomationTriggerType
|
||||
source SuggestionSource
|
||||
mode SuggestionMode
|
||||
intent SuggestionIntent
|
||||
status SuggestionStatus @default(OPEN)
|
||||
priority TaskPriority @default(NORMAL)
|
||||
relatedType TaskRelatedType
|
||||
relatedId String?
|
||||
colonyId String?
|
||||
colony Colony? @relation(fields: [colonyId], references: [id])
|
||||
reasonCode String
|
||||
// Only IDs, numbers and YYYY-MM-DD.
|
||||
evidence Json
|
||||
// e.g. 'varroa:read'; without that permission the suggestion is invisible.
|
||||
requiredPermission String?
|
||||
dueDate DateTime?
|
||||
expiresAt DateTime?
|
||||
// PLAN with several dates: [{ step, offsetDays, windowDays? }]
|
||||
plan Json?
|
||||
// '<ruleKey>:<relatedId|none>:<anchor>'; EXPIRED/RESOLVED clear it.
|
||||
dedupeKey String? @unique
|
||||
sourceEventId String?
|
||||
decidedAt DateTime?
|
||||
decidedByUserId String?
|
||||
decidedByUser User? @relation("SuggestionDecider", fields: [decidedByUserId], references: [id])
|
||||
decisionNote String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
tasks Task[]
|
||||
|
||||
@@index([status, colonyId])
|
||||
@@index([status, dueDate])
|
||||
@@index([ruleKey, relatedType, relatedId, status])
|
||||
}
|
||||
|
||||
enum TaskTemplateRecurrenceType {
|
||||
@@ -589,6 +685,7 @@ model VarroaMeasurement {
|
||||
method VarroaMethod
|
||||
durationMinutes Int?
|
||||
miteCount Int?
|
||||
sampleBees Int?
|
||||
calculatedValue Float?
|
||||
assessment String?
|
||||
notes String?
|
||||
@@ -1172,6 +1269,8 @@ enum BatchMovementType {
|
||||
// Rohstoff in einer Herstellungscharge verbraucht (minus) bzw. beim
|
||||
// Loeschen einer irrtuemlich angelegten Charge zurueckgegeben (plus).
|
||||
CONSUMPTION
|
||||
// Abfuellung entnimmt Honig aus der Charge (Task 23: Bewegungsbuch)
|
||||
BOTTLING
|
||||
}
|
||||
|
||||
enum WithdrawalReason {
|
||||
@@ -1358,6 +1457,15 @@ enum AutomationTriggerType {
|
||||
RAW_MATERIAL_LOT_NUMBER_FORMAT
|
||||
PRODUCTION_BATCH_NUMBER_FORMAT
|
||||
RAW_MATERIAL_LOW
|
||||
INSPECTION_INTERVAL
|
||||
QUEEN_CELLS_SWARM_CHECK
|
||||
QUEEN_ACCEPTANCE_CHECK
|
||||
SPLIT_SCHEDULE
|
||||
COLONY_ENDED_CLEANUP
|
||||
WITHDRAWAL_ENDED
|
||||
HONEY_WATER_CONTENT
|
||||
SEASON_AUTOPILOT
|
||||
PHENOLOGY
|
||||
}
|
||||
|
||||
model AutomationRule {
|
||||
@@ -1446,6 +1554,7 @@ enum CalendarSyncSourceType {
|
||||
FEEDING
|
||||
HARVEST
|
||||
QUEEN_ASSIGNMENT
|
||||
WITHDRAWAL
|
||||
}
|
||||
|
||||
enum CalendarSyncStatus {
|
||||
|
||||
Reference in new issue
Block a user