Files
ApiCore/ApiCore-Backend/src/automation/rule-config/split-schedule.config.ts
T
Nick SzaunigandClaude Opus 5.5 d99a697739 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]>
2026-10-14 13:42:32 +02:00

107 lines
3.0 KiB
TypeScript

import {
asRecord,
bool,
fieldPath,
has,
intIn,
makeParser,
optionalIntIn,
report,
stringIn,
type Scope,
} from './parse-helpers';
import type { PlanStepConfig, SplitScheduleConfig } from './rule-config.types';
type SplitVariant = 'RAISE_OWN' | 'QUEEN_CELL' | 'QUEEN';
const MAX_STEPS_PER_VARIANT = 10;
const MAX_OFFSET_DAYS = 120;
const MAX_WINDOW_DAYS = 30;
const STEP_NAME = /^[A-Z][A-Z_]{1,39}$/;
export const DEFAULT_SPLIT_SCHEDULE_CONFIG: SplitScheduleConfig = {
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,
};
function clonedSteps(variant: SplitVariant): PlanStepConfig[] {
return DEFAULT_SPLIT_SCHEDULE_CONFIG[variant].map((step) => ({ ...step }));
}
/** Scope of one list entry: its path is `VARIANT[index]`. */
function stepScope(
scope: Scope,
variant: SplitVariant,
index: number,
raw: unknown,
): Scope | null {
const record = asRecord(raw);
const path = `${fieldPath(scope, variant)}[${index}]`;
if (record === null) {
scope.issues.push({
field: path,
message: 'Bitte jeden Schritt als Gruppe senden.',
});
return null;
}
return { raw: record, path, issues: scope.issues };
}
function readSteps(scope: Scope, variant: SplitVariant): PlanStepConfig[] {
if (!has(scope, variant)) {
return clonedSteps(variant);
}
const list = scope.raw[variant];
if (!Array.isArray(list) || list.length > MAX_STEPS_PER_VARIANT) {
report(
scope,
variant,
`Bitte eine Liste mit höchstens ${MAX_STEPS_PER_VARIANT} Schritten senden.`,
);
return clonedSteps(variant);
}
const before = scope.issues.length;
const steps: PlanStepConfig[] = [];
list.forEach((entry: unknown, index) => {
const item = stepScope(scope, variant, index, entry);
if (item === null) {
return;
}
if (!has(item, 'step')) {
scope.issues.push({
field: `${item.path}.step`,
message: 'Bitte den Schritt angeben.',
});
return;
}
const step: PlanStepConfig = {
step: stringIn(item, 'step', '', 2, 40, STEP_NAME),
offsetDays: intIn(item, 'offsetDays', 0, 0, MAX_OFFSET_DAYS),
};
const windowDays = optionalIntIn(item, 'windowDays', 0, MAX_WINDOW_DAYS);
if (windowDays !== undefined) {
step.windowDays = windowDays;
}
steps.push(step);
});
// One bad entry makes the whole list unreliable: back to the default plan.
return scope.issues.length > before ? clonedSteps(variant) : steps;
}
export const splitScheduleParser = makeParser<SplitScheduleConfig>(
'SPLIT_SCHEDULE',
DEFAULT_SPLIT_SCHEDULE_CONFIG,
(scope) => ({
RAISE_OWN: readSteps(scope, 'RAISE_OWN'),
QUEEN_CELL: readSteps(scope, 'QUEEN_CELL'),
QUEEN: readSteps(scope, 'QUEEN'),
notify: bool(scope, 'notify', DEFAULT_SPLIT_SCHEDULE_CONFIG.notify),
}),
);