fix: T4 browser-check findings, interval on season state, T6 plan refresh
- Honey batch create omits an empty batch number (was 400); product form
labels the legacy total weight, detail shows jar weight and shelf life;
bottling hints next to their fields, values cleared on product change,
post-save water-content wording; withdraw reason placeholder not
selectable; honey batch actions offline-disabled with reason, 44 px.
- Offline signal: one slow request no longer flips the app offline; two
consecutive timeouts do, any HTTP answer clears it, aborts never count.
- Phenology sheet as a side card on desktop with focus return; confirm
dialogs as alertdialog; step-up texts in du-form ("Das Passwort stimmt
nicht."); backup import gives its disabled reason; missing event labels
plus a guard test; profile buttons 44 px.
- GET /phenology/state returns intervalDays via the shared
inspectionIntervalOf (autopilot, digest, season state use one source).
- T6 plan aligned with the code after T4, plus owner decisions of
2026-10-15 (Berlin day for restrictions, COMPLIANCE category, override
without step-up).
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
9760ce154f
commit
7a1480138d
63 files changed
+1076
-251
No files matched your search
@@ -66,6 +66,7 @@ describe('PhenologyService', () => {
|
||||
$transaction: jest.Mock;
|
||||
apiary: { findUnique: jest.Mock; findMany: jest.Mock };
|
||||
phenologyReport: { findMany: jest.Mock };
|
||||
automationRule: { findUnique: jest.Mock };
|
||||
};
|
||||
let audit: { record: jest.Mock };
|
||||
let events: { record: jest.Mock };
|
||||
@@ -100,6 +101,8 @@ describe('PhenologyService', () => {
|
||||
findMany: jest.fn().mockResolvedValue([]),
|
||||
},
|
||||
phenologyReport: { findMany: jest.fn().mockResolvedValue([]) },
|
||||
// No row: the built-in interval setting applies (SCHWARMZEIT 7 days).
|
||||
automationRule: { findUnique: jest.fn().mockResolvedValue(null) },
|
||||
};
|
||||
audit = { record: jest.fn() };
|
||||
events = { record: jest.fn() };
|
||||
@@ -436,11 +439,44 @@ describe('PhenologyService', () => {
|
||||
gts: 210.5,
|
||||
gtsThroughDay: '2026-03-31',
|
||||
note: null,
|
||||
intervalDays: 14,
|
||||
},
|
||||
]);
|
||||
expect(JSON.stringify(states)).not.toMatch(/latitude|longitude/);
|
||||
});
|
||||
|
||||
it('intervalDays is the interval of the apiary segment from the rule', async () => {
|
||||
prisma.apiary.findMany.mockResolvedValue([
|
||||
{ id: 'a1', name: 'Hang' },
|
||||
{ id: 'a2', name: 'Wald' },
|
||||
]);
|
||||
season.statesFor.mockResolvedValue(
|
||||
new Map([
|
||||
['a1', seasonState({ segment: 'SCHWARMZEIT' })],
|
||||
['a2', seasonState({ apiaryId: 'a2', segment: 'WINTERRUHE' })],
|
||||
]),
|
||||
);
|
||||
|
||||
const states = await service.state();
|
||||
|
||||
expect(states.map((state) => state.intervalDays)).toEqual([7, null]);
|
||||
});
|
||||
|
||||
it('intervalDays is null while the interval rule is inactive', async () => {
|
||||
prisma.apiary.findMany.mockResolvedValue([{ id: 'a1', name: 'Hang' }]);
|
||||
season.statesFor.mockResolvedValue(new Map([['a1', seasonState()]]));
|
||||
prisma.automationRule.findUnique.mockResolvedValue({
|
||||
id: 'rule-1',
|
||||
active: false,
|
||||
config: { intervalDays: { FRUEHJAHR: 10 } },
|
||||
actionTemplate: null,
|
||||
});
|
||||
|
||||
const [state] = await service.state();
|
||||
|
||||
expect(state.intervalDays).toBeNull();
|
||||
});
|
||||
|
||||
it('reportedOn is null unless the phase comes from a report', async () => {
|
||||
prisma.apiary.findMany.mockResolvedValue([{ id: 'a1', name: 'Hang' }]);
|
||||
season.statesFor.mockResolvedValue(
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
Prisma,
|
||||
SeasonSegment,
|
||||
} from '../../generated/prisma';
|
||||
import { readRule } from '../automation/rule-config';
|
||||
import { AuditLogService } from '../audit/audit-log.service';
|
||||
import { berlinToday, dayToDate } from '../common/dates';
|
||||
import { utcDay } from '../domain-events/domain-event-types';
|
||||
@@ -18,6 +19,7 @@ import { DomainEventsService } from '../domain-events/domain-events.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { lockRowForUpdate, lockRowsForShare } from '../prisma/row-lock';
|
||||
import type { SeasonNote } from '../season/season-types';
|
||||
import { inspectionIntervalOf } from '../season/inspection-due';
|
||||
import { SeasonService } from '../season/season.service';
|
||||
import type { CreatePhenologyReportDto } from './dto/create-phenology-report.dto';
|
||||
import type { UpdatePhenologyReportDto } from './dto/update-phenology-report.dto';
|
||||
@@ -52,6 +54,8 @@ export interface PhenologyApiaryState {
|
||||
gts: number | null;
|
||||
gtsThroughDay: string | null;
|
||||
note: SeasonNote;
|
||||
/** The inspection interval in force now (days); null in winter or without the rule. */
|
||||
intervalDays: number | null;
|
||||
}
|
||||
|
||||
/** A report day must exist in the calendar and must not lie after today. */
|
||||
@@ -267,6 +271,7 @@ export class PhenologyService {
|
||||
where: { revokedAt: null, date: yearRange(Number(today.slice(0, 4))) },
|
||||
select: { apiaryId: true, phase: true, date: true },
|
||||
});
|
||||
const interval = await readRule(this.prisma, 'INSPECTION_INTERVAL');
|
||||
return apiaries.flatMap((apiary) => {
|
||||
const state = states.get(apiary.id);
|
||||
if (state === undefined) {
|
||||
@@ -294,6 +299,11 @@ export class PhenologyService {
|
||||
gts: state.gts,
|
||||
gtsThroughDay: state.gtsThroughDay,
|
||||
note: state.note,
|
||||
intervalDays: inspectionIntervalOf(
|
||||
state.segment,
|
||||
interval.config.intervalDays,
|
||||
interval.active,
|
||||
),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user