feat: platform backbone and field capture complete (checkpoint)

Platform backbone (plan 2026-10-13-plattform-rueckgrat), all tasks reviewed:
- webhooks with delivery table, retries and stable delivery id
- calendar handlers; a finished task removes its Outlook event
- tasks, automations, inspections, varroa, treatments, feedings,
  harvests, colonies, wintering, stock and bottling record domain events
  in the same transaction; legacy post-commit calls removed
- row locks through one helper (lockRowForUpdate, lockRowsForShare,
  lockRowsForUpdate)
- job framework with JobRun, cli exit codes, cleanup, task templates,
  daily evaluation, hourly app-backup and the rekey job
- system API and page "Verwaltung > System" with retry
- @nestjs/schedule removed, guard tests against in-process schedulers

Field capture T3 (plan 2026-10-13-t3-erfassung-am-stand), all tasks reviewed:
- inspection photo offline without metadata
- bulk capture for treatments, feedings and varroa (backend + frontend)
- offline stand mode with service worker, offline pages and packs
- offline session, cold start and logout on shared devices

Docs: ARCHITECTURE.md assumptions and sections updated, TASKS.md.
Gates: backend lint, typecheck, 1770 unit, 420 e2e, build, no schema
drift; frontend lint, typecheck, 913 tests, build. Not rolled out.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-14 11:36:38 +02:00
1 parent c01a38835a
commit cdd30cf3ee
288 files changed
+39165 -2853

No files matched your search

@@ -34,11 +34,11 @@ function setOnLine(value: boolean) {
}
/** The band as the layout places it: above the page content wrapper. */
function BandOnPage() {
function BandOnPage({ syncEnabled }: { syncEnabled?: boolean }) {
const pageContentRef = useRef<HTMLDivElement>(null);
return (
<>
<OfflineBand userId="user-1" pageContentRef={pageContentRef} />
<OfflineBand userId="user-1" pageContentRef={pageContentRef} syncEnabled={syncEnabled} />
<div ref={pageContentRef} tabIndex={-1} data-testid="page-content">
Seiteninhalt
</div>
@@ -46,11 +46,11 @@ function BandOnPage() {
);
}
function renderBand() {
function renderBand(props: { syncEnabled?: boolean } = {}) {
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
return render(
<QueryClientProvider client={client}>
<BandOnPage />
<BandOnPage {...props} />
</QueryClientProvider>,
);
}
@@ -260,4 +260,39 @@ describe("OfflineBand", () => {
await waitFor(() => expect(syncPendingEntries).toHaveBeenCalledWith("user-1"));
});
describe("in an offline session (no access token)", () => {
it("neither opens the sync gate nor sends, even when the connection is back", async () => {
setOnLine(true);
vi.mocked(summarizeQueue).mockResolvedValue({ own: 2, failed: 0, foreign: 0 });
renderBand({ syncEnabled: false });
await screen.findByText("Offline – 2 Einträge warten");
act(() => {
window.dispatchEvent(new Event("online"));
});
expect(resumeSync).not.toHaveBeenCalled();
expect(syncPendingEntries).not.toHaveBeenCalled();
expect(screen.queryByRole("button", { name: /Jetzt senden/ })).not.toBeInTheDocument();
});
it("opens the gate and sends as soon as the session is online again", async () => {
setOnLine(true);
vi.mocked(summarizeQueue).mockResolvedValue({ own: 1, failed: 0, foreign: 0 });
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const view = (syncEnabled: boolean) => (
<QueryClientProvider client={client}>
<BandOnPage syncEnabled={syncEnabled} />
</QueryClientProvider>
);
const { rerender } = render(view(false));
await screen.findByText("Offline – 1 Eintrag wartet");
rerender(view(true));
await waitFor(() => expect(syncPendingEntries).toHaveBeenCalledWith("user-1"));
expect(resumeSync).toHaveBeenCalled();
});
});
});