feat(autopilot): /autopilot/today liefert Saison-Zusätze für das Bienenjahr-Rad (T5b B5)

- seasons[] bekommt segmentSince, reportedOn und note
  (NO_COORDINATES | WEATHER_GAPS | WEATHER_STALE).
- Neues seasonCalendar: calendarFallback (8 Phasen, MM-DD), swarmSeason und
  winterVarroaWindow aus den Regel-Configs, auch bei inaktiver Regel.
- Nur Zusätze, keine Migration, Recht bleibt colonies:read;
  SEASON_AUTOPILOT wird je Anfrage einmal gelesen.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick.RosenloecherandClaude Opus 5.5 committed 2026-10-19 15:55:24 +02:00
1 parent 90d1c5244f
commit bcc7cbfd13
4 files changed
+260 -11

No files matched your search

+73 -1
View File
@@ -38,7 +38,18 @@ interface TodayBody {
nextStep: { colonyId: string } | null;
inspectionDue: { apiaryId: string }[];
suggestionsOpen: number;
seasons: { apiaryId: string; segment: string }[];
seasons: {
apiaryId: string;
segment: string;
segmentSince: string;
reportedOn: string | null;
note: string | null;
}[];
seasonCalendar?: {
calendarFallback: Record<string, string>;
swarmSeason: { from: string; until: string };
winterVarroaWindow: { from: string; to: string };
};
tasksVisible: boolean;
}
@@ -454,4 +465,65 @@ describe('Next steps (e2e)', () => {
expect(body.suggestionsOpen).toBeGreaterThanOrEqual(4);
expect(body.nextStep).not.toBeNull();
});
it('GET /autopilot/today carries seasonCalendar and segmentSince per apiary', async () => {
const example = await nextSteps.today(users.full.id, EXAMPLE_DAY);
expect(example.seasons).toContainEqual({
apiaryId,
apiaryName: `Next steps ${suffix}`,
segment: 'SCHWARMZEIT',
phase: 'APFEL',
phaseSource: 'CALENDAR',
segmentSince: '2026-04-15',
reportedOn: null,
note: null,
});
const response = await get(users.noTasks.token, '/autopilot/today').expect(
200,
);
const body = response.body as TodayBody;
const own = body.seasons.find((season) => season.apiaryId === apiaryId);
expect(own?.segmentSince).toMatch(/^\d{4}-\d{2}-\d{2}$/);
expect(own).toHaveProperty('reportedOn');
expect(own).toHaveProperty('note');
expect(body.seasonCalendar).toEqual({
calendarFallback: expect.objectContaining({
SALWEIDE: expect.stringMatching(/^\d{2}-\d{2}$/) as string,
WINTER: expect.stringMatching(/^\d{2}-\d{2}$/) as string,
}) as Record<string, string>,
swarmSeason: {
from: expect.any(String) as string,
until: expect.any(String) as string,
},
winterVarroaWindow: {
from: expect.stringMatching(/^\d{2}-\d{2}$/) as string,
to: expect.stringMatching(/^\d{2}-\d{2}$/) as string,
},
});
expect(Object.keys(body.seasonCalendar?.calendarFallback ?? {})).toEqual([
'SALWEIDE',
'KIRSCHE',
'APFEL',
'HOLUNDER',
'LINDE',
'TRACHTENDE',
'HERBST',
'WINTER',
]);
});
// A changed PHENOLOGY config is covered by the unit test: mutating the shared
// rule row here would race the other e2e suites that read it in parallel.
it('a user without colonies:read still gets 403 and no calendar', async () => {
const noColonies = await createUser('nocalendar', ['tasks:read']);
const response = await get(noColonies.token, '/autopilot/today').expect(
403,
);
expect(response.body).not.toHaveProperty('seasonCalendar');
expect(JSON.stringify(response.body)).not.toMatch(/calendarFallback/);
});
});