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]>
This commit is contained in:
1 parent
64a2b7dad9
commit
02eb16924e
142 files changed
+11847
-1251
No files matched your search
@@ -0,0 +1,108 @@
|
||||
import type { EvidenceLookup } from '../evidence-lookup';
|
||||
import { renderSuggestion } from '../suggestion-texts';
|
||||
import type { SuggestionEvidence } from '../suggestion.types';
|
||||
import { RULE_TEXTS } from './rule-texts';
|
||||
|
||||
const lookup: EvidenceLookup = {
|
||||
colonyCode: (id) => (id === 'gone' ? null : 'LWG'),
|
||||
queenCode: () => null,
|
||||
treatmentProduct: (id) => (id === 'gone' ? null : 'Ameisensäure'),
|
||||
apiaryName: () => null,
|
||||
stockItemName: () => 'Gläser 500 g',
|
||||
};
|
||||
|
||||
const TODAY = '2026-10-14';
|
||||
|
||||
function render(
|
||||
reasonCode: string,
|
||||
evidence: SuggestionEvidence,
|
||||
intent: 'VARROA_TREAT' | 'FOLLOW_UP' | 'STOCK_REORDER' = 'FOLLOW_UP',
|
||||
) {
|
||||
return renderSuggestion(
|
||||
{ reasonCode, evidence: evidence as never, intent },
|
||||
lookup,
|
||||
TODAY,
|
||||
);
|
||||
}
|
||||
|
||||
describe('rule texts', () => {
|
||||
it('has a template for every reason code of the rule suggestions', () => {
|
||||
expect(Object.keys(RULE_TEXTS).sort()).toEqual(
|
||||
[
|
||||
'COLONY_HEALTH_CRITICAL',
|
||||
'FEEDING_FOLLOW_UP',
|
||||
'FEED_STATUS_CRITICAL',
|
||||
'QUEENLESS_NO_EGGS',
|
||||
'RAW_MATERIAL_LOW',
|
||||
'STOCK_ITEM_LOW',
|
||||
'SWARM_MOOD_HIGH',
|
||||
'TREATMENT_FOLLOW_UP',
|
||||
'VARROA_RED',
|
||||
].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
it('VARROA_RED names rate, day and limit with the colony code', () => {
|
||||
expect(render('VARROA_RED', RULE_TEXTS.VARROA_RED.example)).toEqual({
|
||||
title: 'Behandlung prüfen',
|
||||
reasons: ['12,4 Milben pro Tag am 10.05.', 'Grenze: 10 (LWG).'],
|
||||
});
|
||||
});
|
||||
|
||||
it('VARROA_RED words a percentage method in percent', () => {
|
||||
const { reasons } = render('VARROA_RED', {
|
||||
colonyId: 'c',
|
||||
measuredOn: '2026-07-10',
|
||||
value: 6,
|
||||
redAbove: 5,
|
||||
unit: 'PERCENT',
|
||||
});
|
||||
|
||||
expect(reasons).toEqual(['6 % Befall am 10.07.', 'Grenze: 5 % (LWG).']);
|
||||
});
|
||||
|
||||
it('QUEENLESS_NO_EGGS and the inspection rules name the day and the colony', () => {
|
||||
expect(
|
||||
render('QUEENLESS_NO_EGGS', RULE_TEXTS.QUEENLESS_NO_EGGS.example).reasons,
|
||||
).toEqual(['Königin nicht gesehen, keine Eier am 05.05.']);
|
||||
expect(
|
||||
render('FEED_STATUS_CRITICAL', RULE_TEXTS.FEED_STATUS_CRITICAL.example)
|
||||
.reasons,
|
||||
).toEqual(['Futterstatus kritisch am 05.05.']);
|
||||
});
|
||||
|
||||
it('STOCK_ITEM_LOW names the item and the minimum', () => {
|
||||
expect(
|
||||
render('STOCK_ITEM_LOW', RULE_TEXTS.STOCK_ITEM_LOW.example).reasons,
|
||||
).toEqual(['Gläser 500 g: noch 2 auf Lager, Mindestbestand 5.']);
|
||||
});
|
||||
|
||||
it('RAW_MATERIAL_LOW names the kind in German', () => {
|
||||
expect(
|
||||
render('RAW_MATERIAL_LOW', RULE_TEXTS.RAW_MATERIAL_LOW.example).reasons,
|
||||
).toEqual(['Bienenwachs: noch 400 g auf Lager, Mindestmenge 1.000 g.']);
|
||||
});
|
||||
|
||||
it('TREATMENT_FOLLOW_UP names the product, or none when the treatment is gone', () => {
|
||||
expect(
|
||||
render('TREATMENT_FOLLOW_UP', RULE_TEXTS.TREATMENT_FOLLOW_UP.example)
|
||||
.reasons,
|
||||
).toEqual(['Nach Behandlung mit Ameisensäure am 10.08.']);
|
||||
expect(
|
||||
render('TREATMENT_FOLLOW_UP', {
|
||||
colonyId: 'gone',
|
||||
treatmentId: 'gone',
|
||||
endedOn: '2026-08-10',
|
||||
}).reasons,
|
||||
).toEqual(['Nach der Behandlung am 10.08.']);
|
||||
});
|
||||
|
||||
it('survives evidence without its optional parts', () => {
|
||||
for (const [reasonCode, template] of Object.entries(RULE_TEXTS)) {
|
||||
const { title, reasons } = render(reasonCode, {});
|
||||
expect(title).not.toBe('');
|
||||
expect(reasons.length).toBeLessThanOrEqual(2);
|
||||
expect(template.ruleKey).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user