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

@@ -29,7 +29,7 @@ describe("StepUpDialog", () => {
const confirm = screen.getByRole("button", { name: "Verbinden" });
expect(confirm).toBeDisabled();
await userEvent.type(screen.getByLabelText("Ihr Passwort"), "geheim");
await userEvent.type(screen.getByLabelText("Dein Passwort"), "geheim");
await userEvent.click(confirm);
await waitFor(() => expect(onConfirm).toHaveBeenCalledWith({ password: "geheim" }));
@@ -39,7 +39,7 @@ describe("StepUpDialog", () => {
it("asks for the six-digit code with MFA and sends both", async () => {
const { onConfirm } = renderDialog({ needsMfa: true });
await userEvent.type(screen.getByLabelText("Ihr Passwort"), "geheim");
await userEvent.type(screen.getByLabelText("Dein Passwort"), "geheim");
const confirm = screen.getByRole("button", { name: "Verbinden" });
await userEvent.type(screen.getByLabelText("Code aus der Authenticator-App"), "12ab34");
expect(confirm).toBeDisabled();
@@ -56,21 +56,21 @@ describe("StepUpDialog", () => {
const onConfirm = vi.fn().mockRejectedValue(new ApiError(401, "Das Passwort ist falsch."));
const { onOpenChange } = renderDialog({ onConfirm });
await userEvent.type(screen.getByLabelText("Ihr Passwort"), "falsch");
await userEvent.type(screen.getByLabelText("Dein Passwort"), "falsch");
await userEvent.click(screen.getByRole("button", { name: "Verbinden" }));
await waitFor(() =>
expect(screen.getByRole("alert")).toHaveTextContent("Das Passwort ist falsch."),
);
expect(onOpenChange).not.toHaveBeenCalledWith(false);
expect(screen.getByLabelText("Ihr Passwort")).toHaveValue("");
expect(screen.getByLabelText("Ihr Passwort")).toHaveFocus();
expect(screen.getByLabelText("Dein Passwort")).toHaveValue("");
expect(screen.getByLabelText("Dein Passwort")).toHaveFocus();
});
it("shows the German too-many-attempts text on 429", async () => {
renderDialog({ onConfirm: vi.fn().mockRejectedValue(new ApiError(429, "ThrottlerException")) });
await userEvent.type(screen.getByLabelText("Ihr Passwort"), "x");
await userEvent.type(screen.getByLabelText("Dein Passwort"), "x");
await userEvent.click(screen.getByRole("button", { name: "Verbinden" }));
await waitFor(() =>
@@ -83,7 +83,7 @@ describe("StepUpDialog", () => {
it("says the session expired on a bare 401", async () => {
renderDialog({ onConfirm: vi.fn().mockRejectedValue(new ApiError(401, "Unauthorized")) });
await userEvent.type(screen.getByLabelText("Ihr Passwort"), "x");
await userEvent.type(screen.getByLabelText("Dein Passwort"), "x");
await userEvent.click(screen.getByRole("button", { name: "Verbinden" }));
await waitFor(() =>
@@ -96,17 +96,17 @@ describe("StepUpDialog", () => {
it("makes the inputs read-only while the action runs", async () => {
renderDialog({ onConfirm: vi.fn(() => new Promise<void>(() => {})) });
await userEvent.type(screen.getByLabelText("Ihr Passwort"), "geheim");
await userEvent.type(screen.getByLabelText("Dein Passwort"), "geheim");
await userEvent.click(screen.getByRole("button", { name: "Verbinden" }));
await waitFor(() => expect(screen.getByLabelText("Ihr Passwort")).toHaveAttribute("readonly"));
await waitFor(() => expect(screen.getByLabelText("Dein Passwort")).toHaveAttribute("readonly"));
});
it("cannot be closed with Escape while the action runs, and shows the pending label", async () => {
const onConfirm = vi.fn(() => new Promise<void>(() => {}));
const { onOpenChange } = renderDialog({ onConfirm, pendingLabel: "Wird eingespielt…" });
await userEvent.type(screen.getByLabelText("Ihr Passwort"), "geheim");
await userEvent.type(screen.getByLabelText("Dein Passwort"), "geheim");
await userEvent.click(screen.getByRole("button", { name: "Verbinden" }));
expect(await screen.findByRole("button", { name: "Wird eingespielt…" })).toBeDisabled();