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:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-15 12:47:08 +02:00
1 parent 9760ce154f
commit 7a1480138d
63 files changed
+1076 -251

No files matched your search

@@ -0,0 +1,61 @@
import { UnauthorizedException } from '@nestjs/common';
import * as bcrypt from 'bcryptjs';
import { ReauthService } from './reauth.service';
describe('ReauthService', () => {
const baseUser = {
id: 'u1',
email: '[email protected]',
passwordHash: bcrypt.hashSync('richtig', 4),
mfaEnabled: false,
mfaSecret: null,
mfaLastTimeStep: null,
authLockedUntil: null,
isActive: true,
};
const accountGuard = {
isLocked: jest.fn(),
recordFailure: jest.fn(),
recordSuccess: jest.fn(),
consumeTotp: jest.fn(),
};
const prisma = { user: { findUnique: jest.fn() } };
const service = new ReauthService(prisma as never, accountGuard as never);
beforeEach(() => {
jest.resetAllMocks();
prisma.user.findUnique.mockResolvedValue(baseUser);
accountGuard.isLocked.mockReturnValue(false);
});
it('sagt bei falschem Passwort, dass es nicht stimmt', async () => {
await expect(service.verify('u1', { password: 'falsch' })).rejects.toThrow(
new UnauthorizedException('Das Passwort stimmt nicht.'),
);
expect(accountGuard.recordFailure).toHaveBeenCalledWith('u1');
});
it('sagt bei falschem Code, dass er nicht stimmt', async () => {
prisma.user.findUnique.mockResolvedValue({ ...baseUser, mfaEnabled: true });
accountGuard.consumeTotp.mockResolvedValue(false);
await expect(
service.verify('u1', { password: 'richtig', mfaCode: '000000' }),
).rejects.toThrow(new UnauthorizedException('Der Code stimmt nicht.'));
});
it('nennt bei gesperrtem Konto die Sperre', async () => {
accountGuard.isLocked.mockReturnValue(true);
await expect(service.verify('u1', { password: 'richtig' })).rejects.toThrow(
new UnauthorizedException(
'Zu viele Fehlversuche. Bestätigen ist für 15 Minuten gesperrt.',
),
);
});
it('lässt richtige Angaben durch', async () => {
await expect(
service.verify('u1', { password: 'richtig' }),
).resolves.toBeUndefined();
expect(accountGuard.recordSuccess).toHaveBeenCalledWith('u1');
});
});