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:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-14 13:42:32 +02:00
1 parent 7097fcd97c
commit d99a697739
116 files changed
+11239 -801

No files matched your search

@@ -11,6 +11,7 @@ interface InspectionBody {
id: string;
colonyId: string;
broodFrames?: number | null;
feedStatus?: string | null;
}
interface TaskBody {
@@ -268,4 +269,37 @@ describe('Inspections (e2e)', () => {
.send({ broodFrames: 'x' })
.expect(400);
});
it('maps legacy feed words to the enum, accepts enum names and rejects other text', async () => {
const post = (feedStatus: unknown) =>
request(app.getHttpServer())
.post('/api/v1/inspections')
.set('Authorization', `Bearer ${staffToken}`)
.send({ colonyId, date: new Date().toISOString(), feedStatus });
await post('halb voll').expect(400);
await post(5).expect(400);
const blank = await post(' ').expect(201);
expect((blank.body as InspectionBody).feedStatus).toBeNull();
const legacy = await post(' Niedrig ').expect(201);
expect((legacy.body as InspectionBody).feedStatus).toBe('WEAK');
const created = await post('WEAK').expect(201);
const body = created.body as InspectionBody;
expect(body.feedStatus).toBe('WEAK');
const patch = (feedStatus: unknown) =>
request(app.getHttpServer())
.patch(`/api/v1/inspections/${body.id}`)
.set('Authorization', `Bearer ${staffToken}`)
.send({ feedStatus });
await patch('irgendwas').expect(400);
const mapped = await patch('Kritisch').expect(200);
expect((mapped.body as InspectionBody).feedStatus).toBe('CRITICAL');
const blankPatch = await patch(' ').expect(200);
expect((blankPatch.body as InspectionBody).feedStatus).toBeNull();
await patch('WEAK').expect(200);
const cleared = await patch(null).expect(200);
expect((cleared.body as InspectionBody).feedStatus).toBeNull();
});
});