Files
ApiCore/ApiCore-Backend/src/suggestions/texts/autopilot-texts.ts
T
Nick SzaunigandClaude Opus 5.5 02eb16924e feat: T4 wave 4 - rules suggest, takeover, chains, autopilot A1-A3, suggestions page, varroa bands UI
- Automation rules propose suggestions instead of creating tasks;
  notify per rule config; corrected varroa measurements refresh or
  resolve their suggestion
- Inspection values can be taken over into the colony card
  (occupiedFrames 0-40, strength deprecated); warning counts use the
  same visibility rule as the notification list
- Biotech chains: swarm check after queen cells, queen acceptance
  check, split schedules (raise own, queen cell, queen)
- Season autopilot core with A1 inspection due, A2 varroa measurement,
  A3 recheck after treatment; colony sync and daily step
- Frontend: suggestions page with groups, bulk accept, undo, paging,
  focus management and permission gating
- Frontend: varroa traffic light from seasonal bands, sample size,
  one range request for the colony chart
- Notifications: mark-read answers 404 for rows the user may no
  longer see

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-14 15:22:36 +02:00

176 lines
4.6 KiB
TypeScript

import { daysBetween } from '../../common/dates';
import type { SuggestionEvidence } from '../suggestion.types';
import {
formatDayDe,
type SuggestionTextRegister,
type SuggestionTextTemplate,
} from '../suggestion-texts';
const MAX_PRODUCT_LENGTH = 40;
const SEGMENT_PHRASES: Readonly<Record<string, string>> = {
FRUEHJAHR: 'Im Frühjahr',
SCHWARMZEIT: 'In der Schwarmzeit',
SOMMER: 'Im Sommer',
SPAETSOMMER: 'Im Spätsommer',
HERBST: 'Im Herbst',
};
function text(evidence: SuggestionEvidence, key: string): string | null {
const value = evidence[key];
return typeof value === 'string' ? value : null;
}
function count(evidence: SuggestionEvidence, key: string): number | null {
const value = evidence[key];
return typeof value === 'number' ? value : null;
}
/** 'vor 8 Tagen', 'vor 1 Tag', 'heute'. */
function agoDe(from: string, today: string): string {
const days = Math.max(0, daysBetween(from, today));
if (days === 0) {
return 'heute';
}
return `vor ${days} ${days === 1 ? 'Tag' : 'Tagen'}`;
}
function lastInspectionReason(
evidence: SuggestionEvidence,
today: string,
): string {
const last = text(evidence, 'lastInspectedOn');
return last === null
? 'Noch keine Durchsicht erfasst.'
: `Letzte Durchsicht ${agoDe(last, today)}.`;
}
function intervalReason(evidence: SuggestionEvidence): string[] {
const phrase = SEGMENT_PHRASES[text(evidence, 'segment') ?? ''];
const days = count(evidence, 'intervalDays');
return phrase !== undefined && days !== null
? [`${phrase} alle ${days} Tage.`]
: [];
}
const INSPECTION_DUE: SuggestionTextTemplate = {
ruleKey: 'INSPECTION_INTERVAL',
example: {
colonyId: 'c1',
inspectionId: 'i1',
lastInspectedOn: '2026-05-04',
dueOn: '2026-05-11',
intervalDays: 7,
segment: 'SCHWARMZEIT',
},
render: (evidence, _lookup, today) => ({
title: 'Durchsicht',
reasons: [
lastInspectionReason(evidence, today),
...intervalReason(evidence),
],
}),
};
const INSPECTION_FIRST_OF_YEAR: SuggestionTextTemplate = {
ruleKey: 'INSPECTION_INTERVAL',
example: {
colonyId: 'c1',
inspectionId: 'i1',
lastInspectedOn: '2025-09-20',
dueOn: '2026-03-20',
intervalDays: 14,
segment: 'FRUEHJAHR',
},
render: (evidence) => {
const last = text(evidence, 'lastInspectedOn');
return {
title: 'Erste Durchsicht im Jahr',
reasons: [
'Die Saison hat begonnen.',
last === null
? 'Noch keine Durchsicht erfasst.'
: `Letzte Durchsicht am ${formatDayDe(last, true)}.`,
],
};
},
};
const VARROA_MEASURE_DUE: SuggestionTextTemplate = {
ruleKey: 'SEASON_AUTOPILOT',
example: {
colonyId: 'c1',
measurementId: 'm1',
lastMeasuredOn: '2026-05-06',
dueOn: '2026-06-03',
intervalDays: 28,
},
render: (evidence, _lookup, today) => {
const last = text(evidence, 'lastMeasuredOn');
const days = count(evidence, 'intervalDays');
return {
title: 'Varroa messen',
reasons: [
last === null
? 'Noch keine Messung erfasst.'
: `Letzte Messung ${agoDe(last, today)}.`,
...(days === null ? [] : [`Alle ${days} Tage messen.`]),
],
};
},
};
const VARROA_WINTER_MEASURE: SuggestionTextTemplate = {
ruleKey: 'SEASON_AUTOPILOT',
example: {
colonyId: 'c1',
measurementId: 'm1',
lastMeasuredOn: '2026-09-01',
dueOn: '2026-11-15',
intervalDays: 28,
},
render: () => ({
title: 'Varroa messen',
reasons: ['Vor der Winterbehandlung den Milbenfall messen.'],
}),
};
const VARROA_RECHECK_DUE: SuggestionTextTemplate = {
ruleKey: 'SEASON_AUTOPILOT',
example: {
colonyId: 'c1',
treatmentId: 't1',
endedOn: '2026-08-02',
recheckOn: '2026-08-16',
},
render: (evidence, lookup) => {
const treatmentId = text(evidence, 'treatmentId');
const product =
(treatmentId === null ? null : lookup.treatmentProduct(treatmentId)) ??
'Behandlung';
const name = product.slice(0, MAX_PRODUCT_LENGTH);
const ended = text(evidence, 'endedOn');
const recheck = text(evidence, 'recheckOn');
return {
title: 'Behandlungserfolg messen',
reasons: [
ended === null
? `Behandlung mit ${name} beendet.`
: `Behandlung mit ${name} endete am ${formatDayDe(ended)}`,
...(recheck === null
? []
: [`Nachkontrolle ab ${formatDayDe(recheck)}`]),
],
};
},
};
/** Texts of the season autopilot suggestions, by reason code. */
export const AUTOPILOT_TEXTS: SuggestionTextRegister = {
INSPECTION_DUE,
INSPECTION_FIRST_OF_YEAR,
VARROA_MEASURE_DUE,
VARROA_WINTER_MEASURE,
VARROA_RECHECK_DUE,
};