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
@@ -1,6 +1,8 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { JobPartialFailure } from '../system/job-run.service';
|
||||
import { DailyEvaluationJob } from './daily-evaluation.job';
|
||||
import type { AutopilotStep } from '../autopilot/autopilot.step';
|
||||
import type { SuggestionsExpiryStep } from '../autopilot/suggestions-expiry.step';
|
||||
import type { TimeEventsStep } from './steps/time-events.step';
|
||||
|
||||
describe('DailyEvaluationJob', () => {
|
||||
@@ -11,14 +13,29 @@ describe('DailyEvaluationJob', () => {
|
||||
VarroaMeasurementDue: 0,
|
||||
InspectionOverdue: 0,
|
||||
};
|
||||
const AUTOPILOT_COUNTS = {
|
||||
autopilotColonies: 4,
|
||||
suggestionsOpened: 2,
|
||||
suggestionsResolved: 1,
|
||||
};
|
||||
let timeEvents: { name: 'time-events'; run: jest.Mock };
|
||||
let autopilot: { name: 'autopilot'; run: jest.Mock };
|
||||
let expiry: { name: 'suggestions-expiry'; run: jest.Mock };
|
||||
let job: DailyEvaluationJob;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers({ now: new Date('2026-06-10T02:30:00.000Z') });
|
||||
timeEvents = { name: 'time-events', run: jest.fn() };
|
||||
timeEvents.run.mockResolvedValue(COUNTS);
|
||||
job = new DailyEvaluationJob(timeEvents as unknown as TimeEventsStep);
|
||||
autopilot = { name: 'autopilot', run: jest.fn() };
|
||||
autopilot.run.mockResolvedValue(AUTOPILOT_COUNTS);
|
||||
expiry = { name: 'suggestions-expiry', run: jest.fn() };
|
||||
expiry.run.mockResolvedValue({ suggestionsExpired: 3 });
|
||||
job = new DailyEvaluationJob(
|
||||
timeEvents as unknown as TimeEventsStep,
|
||||
autopilot as unknown as AutopilotStep,
|
||||
expiry as unknown as SuggestionsExpiryStep,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -31,8 +48,44 @@ describe('DailyEvaluationJob', () => {
|
||||
});
|
||||
|
||||
it('runs the steps for the Berlin day of the run and reports their counters', async () => {
|
||||
expect(await job.run()).toEqual(COUNTS);
|
||||
expect(await job.run()).toEqual({
|
||||
...COUNTS,
|
||||
...AUTOPILOT_COUNTS,
|
||||
suggestionsExpired: 3,
|
||||
});
|
||||
expect(timeEvents.run).toHaveBeenCalledWith('2026-06-10');
|
||||
expect(autopilot.run).toHaveBeenCalledWith('2026-06-10');
|
||||
expect(expiry.run).toHaveBeenCalledWith('2026-06-10');
|
||||
});
|
||||
|
||||
it('runs time-events, autopilot and suggestions-expiry in that order', async () => {
|
||||
await job.run();
|
||||
|
||||
expect(timeEvents.run.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
autopilot.run.mock.invocationCallOrder[0],
|
||||
);
|
||||
expect(autopilot.run.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
expiry.run.mock.invocationCallOrder[0],
|
||||
);
|
||||
});
|
||||
|
||||
it('a failing autopilot step does not stop the expiry step', async () => {
|
||||
jest.spyOn(Logger.prototype, 'error').mockImplementation(() => undefined);
|
||||
autopilot.run.mockRejectedValue(
|
||||
new JobPartialFailure({ autopilotColonies: 1 }, ['1 colony']),
|
||||
);
|
||||
|
||||
const failure = await job.run().then(
|
||||
() => null,
|
||||
(error: unknown) => error,
|
||||
);
|
||||
|
||||
expect(expiry.run).toHaveBeenCalled();
|
||||
expect(failure).toMatchObject({
|
||||
failedSteps: ['autopilot'],
|
||||
result: { autopilotColonies: 1, suggestionsExpired: 3 },
|
||||
message: 'Daily evaluation failed for: autopilot: 1 colony',
|
||||
});
|
||||
});
|
||||
|
||||
it('takes the Berlin day, not the UTC day, around midnight', async () => {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { AutopilotStep } from '../autopilot/autopilot.step';
|
||||
import { SuggestionsExpiryStep } from '../autopilot/suggestions-expiry.step';
|
||||
import { berlinToday } from '../common/dates';
|
||||
import { JobPartialFailure } from '../system/job-run.service';
|
||||
import {
|
||||
@@ -21,11 +23,19 @@ export class DailyEvaluationJob implements Job {
|
||||
readonly name: JobName = 'daily-evaluation';
|
||||
private readonly logger = new Logger(DailyEvaluationJob.name);
|
||||
|
||||
constructor(private readonly timeEvents: TimeEventsStep) {}
|
||||
constructor(
|
||||
private readonly timeEvents: TimeEventsStep,
|
||||
private readonly autopilot: AutopilotStep,
|
||||
private readonly suggestionsExpiry: SuggestionsExpiryStep,
|
||||
) {}
|
||||
|
||||
/** Evaluates the current Berlin day. */
|
||||
async run(): Promise<JobResult> {
|
||||
const steps: DailyEvaluationStep[] = [this.timeEvents];
|
||||
const steps: DailyEvaluationStep[] = [
|
||||
this.timeEvents,
|
||||
this.autopilot,
|
||||
this.suggestionsExpiry,
|
||||
];
|
||||
const { result, failed, failures } = await runDailyEvaluationSteps(
|
||||
steps,
|
||||
berlinToday(),
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { AuditModule } from '../audit/audit.module';
|
||||
import { JobPermissionsModule } from '../auth/permissions/permission-resolver.module';
|
||||
import { AutopilotModule } from '../autopilot/autopilot.module';
|
||||
import { BackupModule } from '../backup/backup.module';
|
||||
import { IdempotencyCleanupService } from '../common/idempotency/idempotency-cleanup.service';
|
||||
import { DomainEventsModule } from '../domain-events/domain-events.module';
|
||||
@@ -47,6 +48,7 @@ const JOB_CLASSES = [
|
||||
BackupModule,
|
||||
SystemModule,
|
||||
SeasonModule,
|
||||
AutopilotModule,
|
||||
],
|
||||
providers: [
|
||||
...JOB_CLASSES,
|
||||
|
||||
Reference in new issue
Block a user