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,164 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ApiError } from "@/lib/auth/api-client";
|
||||
import { AUTOMATION_TRIGGER_TYPES } from "./types";
|
||||
import { RULE_FORM_DEFAULTS, RULE_LABELS, RULE_VALUE_SOURCES, issuesByField } from "./rule-configs";
|
||||
|
||||
describe("RULE_LABELS", () => {
|
||||
it("every trigger has a label", () => {
|
||||
for (const trigger of AUTOMATION_TRIGGER_TYPES) {
|
||||
expect(RULE_LABELS[trigger], trigger).toBeTruthy();
|
||||
}
|
||||
expect(RULE_LABELS.SEASON_AUTOPILOT).toBe("Saison-Autopilot");
|
||||
expect(RULE_LABELS.PHENOLOGY).toBe("Pflanzenentwicklung");
|
||||
});
|
||||
});
|
||||
|
||||
describe("RULE_VALUE_SOURCES", () => {
|
||||
const sources = RULE_VALUE_SOURCES;
|
||||
|
||||
it("follows the spec tables for the varroa and water values", () => {
|
||||
expect(sources.VARROA_THRESHOLD?.["naturalFall.redAbove"]).toMatchObject({ kind: "SOURCE" });
|
||||
expect(sources.VARROA_THRESHOLD?.["naturalFall.redAbove"].label).toContain("LWG Veitshöchheim");
|
||||
expect(sources.VARROA_THRESHOLD?.youngColonyFactor).toEqual({
|
||||
label: "Erfahrungswert",
|
||||
kind: "SOURCE",
|
||||
});
|
||||
expect(sources.HONEY_WATER_CONTENT?.warnAbovePercent.label).toContain("D.I.B.");
|
||||
expect(sources.HONEY_WATER_CONTENT?.confirmAbovePercent.label).toContain("HonigV Anlage 2");
|
||||
});
|
||||
|
||||
it("names Erfahrungswert for the inspection intervals and the acceptance check", () => {
|
||||
const intervals = sources.INSPECTION_INTERVAL!;
|
||||
expect(intervals["intervalDays.FRUEHJAHR"].label).toBe("Erfahrungswert");
|
||||
expect(intervals["intervalDays.HERBST"].label).toBe("Erfahrungswert");
|
||||
expect(intervals["intervalDays.SCHWARMZEIT"].label).toBe(
|
||||
"Bieneninstitute (LWG, LLH Kirchhain)",
|
||||
);
|
||||
expect(sources.QUEEN_ACCEPTANCE_CHECK?.dueInDays.label).toBe("Erfahrungswert");
|
||||
});
|
||||
|
||||
it("gives every split step a source from its variant", () => {
|
||||
const steps = sources.SPLIT_SCHEDULE!;
|
||||
expect(steps["RAISE_OWN[0].offsetDays"].label).toBe("Erfahrungswert");
|
||||
expect(steps["RAISE_OWN[1].windowDays"].label).toBe("Erfahrungswert");
|
||||
expect(steps["QUEEN_CELL[0].offsetDays"].kind).toBe("ASSUMPTION");
|
||||
expect(steps["QUEEN[0].offsetDays"].label).toBe("wie Königin zugesetzt");
|
||||
});
|
||||
|
||||
it("marks the autopilot window and phenology guesses as assumptions", () => {
|
||||
const autopilot = sources.SEASON_AUTOPILOT!;
|
||||
expect(autopilot["winterVarroaWindow.from"]).toEqual({
|
||||
label: "Annahme (LWG-Konzept)",
|
||||
kind: "ASSUMPTION",
|
||||
});
|
||||
expect(autopilot.varroaMeasureIntervalDays.label).toBe("Erfahrungswert (28 Tage)");
|
||||
expect(autopilot["rules.A9"].label).toBe("Annahme");
|
||||
expect(autopilot["rules.A10"].label).toBe("Annahme");
|
||||
for (let n = 1; n <= 10; n += 1) expect(autopilot[`rules.A${n}`], `A${n}`).toBeDefined();
|
||||
const phenology = sources.PHENOLOGY!;
|
||||
expect(phenology["swarmSeason.from"].label).toBe("Annahme nach Bieneninstitut-Praxis");
|
||||
expect(phenology["gtsThresholds.SALWEIDE"].label).toBe(
|
||||
"DWD; Gleichsetzung mit Salweidenblüte = Annahme",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Pinned to the table "Regeln" of the global constraints, so a changed default is a decision.
|
||||
describe("RULE_FORM_DEFAULTS", () => {
|
||||
it("matches the built-in settings of the backend", () => {
|
||||
expect(RULE_FORM_DEFAULTS).toEqual({
|
||||
VARROA_THRESHOLD: {
|
||||
naturalFall: {
|
||||
seasonMonths: [3, 4, 5, 6, 7, 8, 9, 10],
|
||||
yellowFrom: 5,
|
||||
redAbove: 10,
|
||||
winterRedAbove: 0.5,
|
||||
},
|
||||
infestationPercent: {
|
||||
summerMonths: [5, 6, 7],
|
||||
summer: { yellowFrom: 1, redAbove: 5 },
|
||||
autumnMonths: [8, 9, 10],
|
||||
autumn: { yellowFrom: 2, redAbove: 5 },
|
||||
},
|
||||
youngColonyFactor: 0.5,
|
||||
youngColonyTypes: ["YOUNG_COLONY", "NUCLEUS_COLONY", "MATING_NUCLEUS"],
|
||||
recheckAfterDays: 14,
|
||||
notify: true,
|
||||
},
|
||||
INSPECTION_INTERVAL: {
|
||||
intervalDays: { FRUEHJAHR: 14, SCHWARMZEIT: 7, SOMMER: 14, SPAETSOMMER: 21, HERBST: 21 },
|
||||
},
|
||||
QUEEN_CELLS_SWARM_CHECK: { dueInDays: 7, notify: false },
|
||||
QUEEN_ACCEPTANCE_CHECK: { dueInDays: 7, notify: false },
|
||||
SPLIT_SCHEDULE: {
|
||||
RAISE_OWN: [
|
||||
{ step: "RAISE_CELLS_CHECK", offsetDays: 9 },
|
||||
{ step: "MATING_CHECK", offsetDays: 21, windowDays: 3 },
|
||||
{ step: "EGGS_CHECK", offsetDays: 28 },
|
||||
],
|
||||
QUEEN_CELL: [{ step: "EGGS_CHECK", offsetDays: 21 }],
|
||||
QUEEN: [{ step: "ACCEPTANCE_CHECK", offsetDays: 7 }],
|
||||
notify: false,
|
||||
},
|
||||
WITHDRAWAL_ENDED: { notify: true },
|
||||
HONEY_WATER_CONTENT: { warnAbovePercent: 18, confirmAbovePercent: 20, byHoneyType: {} },
|
||||
SEASON_AUTOPILOT: {
|
||||
honeySuperMinFrames: 8,
|
||||
varroaMeasureIntervalDays: 28,
|
||||
winterVarroaWindow: { from: "11-15", to: "12-20" },
|
||||
queenMaxAgeYears: 2,
|
||||
suggestionTtlDays: 30,
|
||||
maxStepsPerColony: 3,
|
||||
rules: {
|
||||
A1: true, A2: true, A3: true, A4: true, A5: true,
|
||||
A6: true, A7: true, A8: true, A9: true, A10: true,
|
||||
},
|
||||
},
|
||||
PHENOLOGY: {
|
||||
useGts: true,
|
||||
gtsThresholds: { SALWEIDE: 200 },
|
||||
calibrateFromReports: true,
|
||||
calibratePhases: ["KIRSCHE", "APFEL"],
|
||||
calendarFallback: {
|
||||
SALWEIDE: "03-20", KIRSCHE: "04-15", APFEL: "04-28", HOLUNDER: "05-30",
|
||||
LINDE: "06-20", TRACHTENDE: "08-01", HERBST: "09-15", WINTER: "11-15",
|
||||
},
|
||||
swarmSeason: { from: "KIRSCHE", until: "LINDE" },
|
||||
maxGapShare: 0.1,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("issuesByField", () => {
|
||||
it("maps the issues of a 400 to their fields", () => {
|
||||
const error = new ApiError(400, "Die Einstellung ist ungültig.", {
|
||||
message: "Die Einstellung ist ungültig.",
|
||||
issues: [
|
||||
{ field: "naturalFall.redAbove", message: "Zu hoch." },
|
||||
{ field: "rules.A1", message: "Ungültig." },
|
||||
],
|
||||
});
|
||||
expect(issuesByField(error)).toEqual({
|
||||
"naturalFall.redAbove": "Zu hoch.",
|
||||
"rules.A1": "Ungültig.",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the first message of a field", () => {
|
||||
const error = new ApiError(400, "x", {
|
||||
issues: [
|
||||
{ field: "a", message: "erste" },
|
||||
{ field: "a", message: "zweite" },
|
||||
],
|
||||
});
|
||||
expect(issuesByField(error)).toEqual({ a: "erste" });
|
||||
});
|
||||
|
||||
it("returns nothing for other errors and malformed bodies", () => {
|
||||
expect(issuesByField(new Error("x"))).toEqual({});
|
||||
expect(issuesByField(new ApiError(500, "x", { issues: "nope" }))).toEqual({});
|
||||
expect(issuesByField(new ApiError(400, "x", { issues: [{ field: 3 }, null] }))).toEqual({});
|
||||
expect(issuesByField(undefined)).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,309 @@
|
||||
import { AutomationTriggerType } from "./types";
|
||||
|
||||
export const RULE_LABELS: Record<AutomationTriggerType, string> = {
|
||||
QUEENLESS_NO_EGGS: "Keine Königin gesehen und keine Stifte",
|
||||
VARROA_THRESHOLD: "Varroa-Schwellenwert überschritten",
|
||||
FEED_STATUS_CRITICAL: "Futterstatus kritisch",
|
||||
SWARM_MOOD_HIGH: "Hohe Schwarmstimmung",
|
||||
COLONY_HEALTH_CRITICAL: "Gesundheitsstatus kritisch",
|
||||
STOCK_ITEM_LOW: "Lagerartikel unter Mindestbestand",
|
||||
HONEY_BATCH_NUMBER_FORMAT: "Chargennummer-Format (Honigchargen)",
|
||||
BOTTLING_LOT_NUMBER_FORMAT: "Los-/Chargennummer-Format (Abfüllungen)",
|
||||
TREATMENT_FOLLOW_UP: "Kontrolle nach Behandlung",
|
||||
FEEDING_FOLLOW_UP: "Kontrolle nach Fütterung",
|
||||
RAW_MATERIAL_LOT_NUMBER_FORMAT: "Chargennummer-Format (Rohstoffe)",
|
||||
PRODUCTION_BATCH_NUMBER_FORMAT: "Chargennummer-Format (Herstellung)",
|
||||
RAW_MATERIAL_LOW: "Rohstoff unter Mindestmenge",
|
||||
INSPECTION_INTERVAL: "Durchsicht-Rhythmus",
|
||||
QUEEN_CELLS_SWARM_CHECK: "Weiselzellen: Schwarmkontrolle",
|
||||
QUEEN_ACCEPTANCE_CHECK: "Königin zugesetzt: Annahme prüfen",
|
||||
SPLIT_SCHEDULE: "Ableger-Fahrplan",
|
||||
COLONY_ENDED_CLEANUP: "Volk beendet: Aufräumen",
|
||||
WITHDRAWAL_ENDED: "Wartezeit zu Ende",
|
||||
HONEY_WATER_CONTENT: "Wassergehalt Honig",
|
||||
SEASON_AUTOPILOT: "Saison-Autopilot",
|
||||
PHENOLOGY: "Pflanzenentwicklung",
|
||||
};
|
||||
|
||||
export const RULE_DESCRIPTIONS: Record<AutomationTriggerType, string> = {
|
||||
QUEENLESS_NO_EGGS:
|
||||
"Bei einer Durchsicht: Königin nicht gesehen und keine Stifte vorhanden, dann Kontrollaufgabe.",
|
||||
VARROA_THRESHOLD:
|
||||
"Ampel und Vorschlag „Behandlung prüfen“ nach Messmethode, Jahreszeit und Volkstyp.",
|
||||
FEED_STATUS_CRITICAL:
|
||||
"Futterstatus bei einer Durchsicht auf kritisch gesetzt, dann Fütterungsaufgabe.",
|
||||
SWARM_MOOD_HIGH:
|
||||
"Bei einer Schwarmkontrolle Schwarmstimmung hoch erfasst, dann Kontrollaufgabe.",
|
||||
COLONY_HEALTH_CRITICAL:
|
||||
"Gesundheitsstatus eines Volkes wechselt auf kritisch, dann Kontrollaufgabe.",
|
||||
STOCK_ITEM_LOW:
|
||||
"Bestand eines Lagerartikels fällt nach einer Buchung unter den Mindestbestand, dann Nachbestellaufgabe.",
|
||||
HONEY_BATCH_NUMBER_FORMAT:
|
||||
"Wenn aktiv, wird die Chargennummer neuer Honigchargen automatisch nach diesem Format erzeugt, statt sie manuell einzugeben.",
|
||||
BOTTLING_LOT_NUMBER_FORMAT:
|
||||
"Wenn aktiv, wird die Los-/Chargennummer neuer Abfüllungen automatisch nach diesem Format erzeugt, statt sie manuell einzugeben.",
|
||||
TREATMENT_FOLLOW_UP:
|
||||
"Nach jeder erfassten Behandlung eine Kontrollaufgabe vorschlagen, fällig die eingestellten Tage nach Behandlungsende. Die passende Frist hängt vom Mittel ab (Packungsbeilage) – deshalb ab Werk aus und ohne voreingestellte Frist.",
|
||||
FEEDING_FOLLOW_UP:
|
||||
"Nach jeder erfassten Fütterung eine Kontrollaufgabe vorschlagen, fällig die eingestellten Tage nach der Fütterung. Ab Werk aus und ohne voreingestellte Frist.",
|
||||
RAW_MATERIAL_LOT_NUMBER_FORMAT:
|
||||
"Wenn aktiv, wird die Chargennummer neuer Rohstoff-Chargen (Wachs, Propolis) automatisch nach diesem Format erzeugt, statt sie manuell einzugeben.",
|
||||
PRODUCTION_BATCH_NUMBER_FORMAT:
|
||||
"Wenn aktiv, wird die Chargennummer neuer Herstellungschargen (Kerzen, Tinkturen, …) automatisch nach diesem Format erzeugt, statt sie manuell einzugeben.",
|
||||
RAW_MATERIAL_LOW:
|
||||
"Fällt der Gesamtbestand einer Rohstoff-Art nach Verbrauch, Entnahme oder Inventur unter die Mindestmenge, dann Aufgabe zum Nachschub. Leere Mindestmenge = diese Art wird nicht überwacht.",
|
||||
INSPECTION_INTERVAL:
|
||||
"Nach wie vielen Tagen eine Durchsicht fällig ist. Der Wert hängt vom Abschnitt der Saison ab.",
|
||||
QUEEN_CELLS_SWARM_CHECK:
|
||||
"Weiselzellen gesehen: Schwarmkontrolle vorschlagen, fällig nach den eingestellten Tagen.",
|
||||
QUEEN_ACCEPTANCE_CHECK:
|
||||
"Königin zugesetzt: Prüfung der Annahme vorschlagen, fällig nach den eingestellten Tagen.",
|
||||
SPLIT_SCHEDULE:
|
||||
"Nach dem Ableger die Kontrollen vorschlagen. Die Tage zählen ab dem Tag der Bildung.",
|
||||
COLONY_ENDED_CLEANUP:
|
||||
"Wird ein Volk beendet, schlägt die App vor, offene Aufgaben und Vorschläge dieses Volkes aufzuräumen. Es gibt nichts einzustellen.",
|
||||
WITHDRAWAL_ENDED:
|
||||
"Die Wartezeit einer Behandlung ist vorbei: Ernte ist wieder erlaubt.",
|
||||
HONEY_WATER_CONTENT:
|
||||
"Ab welchem Wassergehalt die App beim Honig warnt und ab wann beim Abfüllen eine Bestätigung nötig ist.",
|
||||
SEASON_AUTOPILOT:
|
||||
"Die App schlägt je Volk die nächsten Schritte der Saison vor. Jede Regel A1 bis A10 lässt sich einzeln ausschalten.",
|
||||
PHENOLOGY:
|
||||
"Wie die App die Saison erkennt: aus deiner Meldung, aus der Wärmesumme (GTS) oder nach dem Kalender.",
|
||||
};
|
||||
|
||||
export interface ValueSource {
|
||||
label: string;
|
||||
kind: "SOURCE" | "ASSUMPTION";
|
||||
}
|
||||
|
||||
/** Built-in settings the forms start from when the stored config is partial. */
|
||||
export const RULE_FORM_DEFAULTS = {
|
||||
VARROA_THRESHOLD: {
|
||||
naturalFall: {
|
||||
seasonMonths: [3, 4, 5, 6, 7, 8, 9, 10],
|
||||
yellowFrom: 5,
|
||||
redAbove: 10,
|
||||
winterRedAbove: 0.5,
|
||||
},
|
||||
infestationPercent: {
|
||||
summerMonths: [5, 6, 7],
|
||||
summer: { yellowFrom: 1, redAbove: 5 },
|
||||
autumnMonths: [8, 9, 10],
|
||||
autumn: { yellowFrom: 2, redAbove: 5 },
|
||||
},
|
||||
youngColonyFactor: 0.5,
|
||||
youngColonyTypes: ["YOUNG_COLONY", "NUCLEUS_COLONY", "MATING_NUCLEUS"],
|
||||
recheckAfterDays: 14,
|
||||
notify: true,
|
||||
},
|
||||
INSPECTION_INTERVAL: {
|
||||
intervalDays: {
|
||||
FRUEHJAHR: 14,
|
||||
SCHWARMZEIT: 7,
|
||||
SOMMER: 14,
|
||||
SPAETSOMMER: 21,
|
||||
HERBST: 21,
|
||||
},
|
||||
},
|
||||
QUEEN_CELLS_SWARM_CHECK: { dueInDays: 7, notify: false },
|
||||
QUEEN_ACCEPTANCE_CHECK: { dueInDays: 7, notify: false },
|
||||
SPLIT_SCHEDULE: {
|
||||
RAISE_OWN: [
|
||||
{ step: "RAISE_CELLS_CHECK", offsetDays: 9 },
|
||||
{ step: "MATING_CHECK", offsetDays: 21, windowDays: 3 },
|
||||
{ step: "EGGS_CHECK", offsetDays: 28 },
|
||||
],
|
||||
QUEEN_CELL: [{ step: "EGGS_CHECK", offsetDays: 21 }],
|
||||
QUEEN: [{ step: "ACCEPTANCE_CHECK", offsetDays: 7 }],
|
||||
notify: false,
|
||||
},
|
||||
WITHDRAWAL_ENDED: { notify: true },
|
||||
HONEY_WATER_CONTENT: {
|
||||
warnAbovePercent: 18,
|
||||
confirmAbovePercent: 20,
|
||||
byHoneyType: {},
|
||||
},
|
||||
SEASON_AUTOPILOT: {
|
||||
honeySuperMinFrames: 8,
|
||||
varroaMeasureIntervalDays: 28,
|
||||
winterVarroaWindow: { from: "11-15", to: "12-20" },
|
||||
queenMaxAgeYears: 2,
|
||||
suggestionTtlDays: 30,
|
||||
maxStepsPerColony: 3,
|
||||
rules: {
|
||||
A1: true,
|
||||
A2: true,
|
||||
A3: true,
|
||||
A4: true,
|
||||
A5: true,
|
||||
A6: true,
|
||||
A7: true,
|
||||
A8: true,
|
||||
A9: true,
|
||||
A10: true,
|
||||
},
|
||||
},
|
||||
PHENOLOGY: {
|
||||
useGts: true,
|
||||
gtsThresholds: { SALWEIDE: 200 },
|
||||
calibrateFromReports: true,
|
||||
calibratePhases: ["KIRSCHE", "APFEL"],
|
||||
calendarFallback: {
|
||||
SALWEIDE: "03-20",
|
||||
KIRSCHE: "04-15",
|
||||
APFEL: "04-28",
|
||||
HOLUNDER: "05-30",
|
||||
LINDE: "06-20",
|
||||
TRACHTENDE: "08-01",
|
||||
HERBST: "09-15",
|
||||
WINTER: "11-15",
|
||||
},
|
||||
swarmSeason: { from: "KIRSCHE", until: "LINDE" },
|
||||
maxGapShare: 0.1,
|
||||
},
|
||||
} as const satisfies Partial<Record<AutomationTriggerType, object>>;
|
||||
|
||||
// Spec rows citing "Fahrplan §10" without an institute show "Erfahrungswert".
|
||||
const FAHRPLAN: ValueSource = { label: "Erfahrungswert", kind: "SOURCE" };
|
||||
const ASSUMPTION: ValueSource = { label: "Annahme", kind: "ASSUMPTION" };
|
||||
|
||||
function source(label: string): ValueSource {
|
||||
return { label, kind: "SOURCE" };
|
||||
}
|
||||
|
||||
function assumption(label: string): ValueSource {
|
||||
return { label, kind: "ASSUMPTION" };
|
||||
}
|
||||
|
||||
const LWG_NATURAL_FALL = source(
|
||||
"LWG Veitshöchheim; Gültigkeit ab März = Annahme", // table from Fahrplan §10
|
||||
);
|
||||
const LWG_WINTER = source(
|
||||
"LWG Veitshöchheim („ab November Behandlung schon über 0,5“); Ende Februar = Annahme",
|
||||
);
|
||||
const LWG_SUMMER = source("LWG Veitshöchheim („Juli“); Mai und Juni = Annahme");
|
||||
const LWG_AUTUMN = source("LWG Veitshöchheim („September“); August und Oktober = Annahme");
|
||||
const SWARM_CHECK = source(
|
||||
"Bieneninstitute (LWG, LLH Kirchhain): Verdeckelung nach 8–9 Tagen",
|
||||
);
|
||||
|
||||
/** Plan steps per split variant: offsets and windows with their source (spec 7.4). */
|
||||
function splitScheduleSources(): Record<string, ValueSource> {
|
||||
const variantSource: Record<string, ValueSource> = {
|
||||
RAISE_OWN: FAHRPLAN,
|
||||
QUEEN_CELL: ASSUMPTION,
|
||||
QUEEN: source("wie Königin zugesetzt"),
|
||||
};
|
||||
const result: Record<string, ValueSource> = {};
|
||||
const defaults = RULE_FORM_DEFAULTS.SPLIT_SCHEDULE as unknown as Record<
|
||||
string,
|
||||
{ offsetDays: number; windowDays?: number }[]
|
||||
>;
|
||||
for (const [variant, steps] of Object.entries(defaults)) {
|
||||
if (!Array.isArray(steps)) continue;
|
||||
steps.forEach((step, index) => {
|
||||
result[`${variant}[${index}].offsetDays`] = variantSource[variant];
|
||||
if (step.windowDays !== undefined) {
|
||||
result[`${variant}[${index}].windowDays`] = variantSource[variant];
|
||||
}
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Where the built-in values come from; paths follow the config keys. */
|
||||
export const RULE_VALUE_SOURCES: Partial<
|
||||
Record<AutomationTriggerType, Record<string, ValueSource>>
|
||||
> = {
|
||||
VARROA_THRESHOLD: {
|
||||
"naturalFall.yellowFrom": LWG_NATURAL_FALL,
|
||||
"naturalFall.redAbove": LWG_NATURAL_FALL,
|
||||
"naturalFall.winterRedAbove": LWG_WINTER,
|
||||
"infestationPercent.summer.yellowFrom": LWG_SUMMER,
|
||||
"infestationPercent.summer.redAbove": LWG_SUMMER,
|
||||
"infestationPercent.autumn.yellowFrom": LWG_AUTUMN,
|
||||
"infestationPercent.autumn.redAbove": LWG_AUTUMN,
|
||||
youngColonyFactor: FAHRPLAN,
|
||||
recheckAfterDays: FAHRPLAN,
|
||||
},
|
||||
INSPECTION_INTERVAL: {
|
||||
"intervalDays.FRUEHJAHR": FAHRPLAN,
|
||||
"intervalDays.SCHWARMZEIT": source(
|
||||
"Bieneninstitute (LWG, LLH Kirchhain)",
|
||||
),
|
||||
"intervalDays.SOMMER": FAHRPLAN,
|
||||
"intervalDays.SPAETSOMMER": FAHRPLAN,
|
||||
"intervalDays.HERBST": FAHRPLAN,
|
||||
},
|
||||
QUEEN_CELLS_SWARM_CHECK: { dueInDays: SWARM_CHECK },
|
||||
QUEEN_ACCEPTANCE_CHECK: { dueInDays: FAHRPLAN },
|
||||
SPLIT_SCHEDULE: splitScheduleSources(),
|
||||
HONEY_WATER_CONTENT: {
|
||||
warnAbovePercent: source("D.I.B., Warenzeichen „Echter Deutscher Honig“"),
|
||||
confirmAbovePercent: source("HonigV Anlage 2 (RL 2001/110/EG Anhang II)"),
|
||||
},
|
||||
SEASON_AUTOPILOT: {
|
||||
honeySuperMinFrames: assumption("Annahme (Imkerpraxis, 8 Waben)"),
|
||||
varroaMeasureIntervalDays: source("Erfahrungswert (28 Tage)"),
|
||||
"winterVarroaWindow.from": assumption("Annahme (LWG-Konzept)"),
|
||||
"winterVarroaWindow.to": assumption("Annahme (LWG-Konzept)"),
|
||||
queenMaxAgeYears: assumption("Annahme (Imkerpraxis, 2 Jahre)"),
|
||||
suggestionTtlDays: ASSUMPTION,
|
||||
maxStepsPerColony: ASSUMPTION,
|
||||
"rules.A1": source("Fahrplan, Durchsicht-Rhythmus"), // spec 7.2
|
||||
"rules.A2": source("Erfahrungswert (28 Tage); Winterfenster = Annahme (LWG-Konzept)"),
|
||||
"rules.A3": source("LWG Veitshöchheim"),
|
||||
"rules.A4": source("LWG-Varroakonzept: Behandlung nach der letzten Ernte"),
|
||||
"rules.A5": source("Imkerpraxis; bewusst ohne Zielmenge"),
|
||||
"rules.A6": source("Erfahrungswert"), // Lastenheft §23
|
||||
"rules.A7": source("Imkerpraxis; 8 Waben = Annahme"),
|
||||
"rules.A8": source("Imkerpraxis; 2 Jahre = Annahme"),
|
||||
"rules.A9": assumption("Annahme"), // Annahme 16
|
||||
"rules.A10": assumption("Annahme"), // Annahme 16
|
||||
},
|
||||
PHENOLOGY: {
|
||||
"gtsThresholds.SALWEIDE": source(
|
||||
"DWD; Gleichsetzung mit Salweidenblüte = Annahme",
|
||||
),
|
||||
"calendarFallback.SALWEIDE": ASSUMPTION,
|
||||
"calendarFallback.KIRSCHE": ASSUMPTION,
|
||||
"calendarFallback.APFEL": ASSUMPTION,
|
||||
"calendarFallback.HOLUNDER": ASSUMPTION,
|
||||
"calendarFallback.LINDE": ASSUMPTION,
|
||||
"calendarFallback.TRACHTENDE": ASSUMPTION,
|
||||
"calendarFallback.HERBST": ASSUMPTION,
|
||||
"calendarFallback.WINTER": ASSUMPTION,
|
||||
"swarmSeason.from": assumption("Annahme nach Bieneninstitut-Praxis"),
|
||||
"swarmSeason.until": assumption("Annahme nach Bieneninstitut-Praxis"),
|
||||
maxGapShare: ASSUMPTION,
|
||||
},
|
||||
};
|
||||
|
||||
/** The server's field messages of a rejected config (400 with `issues`). */
|
||||
export function issuesByField(error: unknown): Record<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
const body = (error as { body?: unknown } | null | undefined)?.body;
|
||||
const issues = (body as { issues?: unknown } | null | undefined)?.issues;
|
||||
if (!Array.isArray(issues)) return result;
|
||||
for (const issue of issues) {
|
||||
if (typeof issue !== "object" || issue === null) continue;
|
||||
const { field, message } = issue as { field?: unknown; message?: unknown };
|
||||
if (typeof field !== "string" || typeof message !== "string") continue;
|
||||
if (!(field in result)) result[field] = message;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/** Default of the notify switch for the older rules (`DEFAULT_RULES` of the backend). */
|
||||
export const SIMPLE_RULE_NOTIFY_DEFAULTS: Partial<Record<AutomationTriggerType, boolean>> = {
|
||||
QUEENLESS_NO_EGGS: true,
|
||||
COLONY_HEALTH_CRITICAL: true,
|
||||
FEED_STATUS_CRITICAL: false,
|
||||
SWARM_MOOD_HIGH: false,
|
||||
STOCK_ITEM_LOW: false,
|
||||
RAW_MATERIAL_LOW: false,
|
||||
TREATMENT_FOLLOW_UP: false,
|
||||
FEEDING_FOLLOW_UP: false,
|
||||
};
|
||||
@@ -1,17 +1,29 @@
|
||||
export type AutomationTriggerType =
|
||||
| "QUEENLESS_NO_EGGS"
|
||||
| "VARROA_THRESHOLD"
|
||||
| "FEED_STATUS_CRITICAL"
|
||||
| "SWARM_MOOD_HIGH"
|
||||
| "COLONY_HEALTH_CRITICAL"
|
||||
| "STOCK_ITEM_LOW"
|
||||
| "HONEY_BATCH_NUMBER_FORMAT"
|
||||
| "BOTTLING_LOT_NUMBER_FORMAT"
|
||||
| "TREATMENT_FOLLOW_UP"
|
||||
| "FEEDING_FOLLOW_UP"
|
||||
| "RAW_MATERIAL_LOT_NUMBER_FORMAT"
|
||||
| "PRODUCTION_BATCH_NUMBER_FORMAT"
|
||||
| "RAW_MATERIAL_LOW";
|
||||
export const AUTOMATION_TRIGGER_TYPES = [
|
||||
"QUEENLESS_NO_EGGS",
|
||||
"VARROA_THRESHOLD",
|
||||
"FEED_STATUS_CRITICAL",
|
||||
"SWARM_MOOD_HIGH",
|
||||
"COLONY_HEALTH_CRITICAL",
|
||||
"STOCK_ITEM_LOW",
|
||||
"HONEY_BATCH_NUMBER_FORMAT",
|
||||
"BOTTLING_LOT_NUMBER_FORMAT",
|
||||
"TREATMENT_FOLLOW_UP",
|
||||
"FEEDING_FOLLOW_UP",
|
||||
"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",
|
||||
] as const;
|
||||
|
||||
export type AutomationTriggerType = (typeof AUTOMATION_TRIGGER_TYPES)[number];
|
||||
|
||||
export interface AutomationRule {
|
||||
id: string;
|
||||
|
||||
Reference in new issue
Block a user