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

+28 -8
View File
@@ -1,11 +1,12 @@
"use client";
import { ReactNode, useEffect, useRef } from "react";
import { ReactNode, useEffect, useRef, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Hexagon } from "lucide-react";
import { useAuth } from "@/lib/auth/auth-context";
import { purgeStaleDraftPhotos } from "@/lib/photos/store";
import { finishHandOvers, purgeStaleDraftPhotos } from "@/lib/photos/store";
import { LogoutConfirmDialog } from "@/components/auth/logout-confirm-dialog";
import { NotificationBell } from "@/components/notification-bell";
import { OfflineBand } from "@/components/offline/offline-band";
import { UserAvatar } from "@/components/user-avatar";
@@ -25,7 +26,8 @@ import { PageTransition } from "./page-transition";
export default function AppLayout({ children }: { children: ReactNode }) {
const router = useRouter();
const { user, isLoading, logout } = useAuth();
const { user, isLoading, sessionMode } = useAuth();
const [isLogoutOpen, setIsLogoutOpen] = useState(false);
// Focus lands here when the offline queue sheet closes and nothing else is left.
const pageContentRef = useRef<HTMLDivElement>(null);
@@ -41,6 +43,16 @@ export default function AppLayout({ children }: { children: ReactNode }) {
void purgeStaleDraftPhotos(Date.now()).catch(() => undefined);
}, []);
// Online saves whose photos could not be tied to their inspection at once:
// finished for the signed-in user, even offline - the photos then wait as
// entries of their own. Every sync run of that user tries again too.
const userId = user?.id;
useEffect(() => {
if (userId) {
void finishHandOvers(userId).catch(() => undefined);
}
}, [userId]);
if (isLoading || !user) {
return null;
}
@@ -69,7 +81,7 @@ export default function AppLayout({ children }: { children: ReactNode }) {
<QuickAddSheet user={user} variant="header" />
<NotificationBell />
<DropdownMenu>
<DropdownMenuTrigger className="rounded-full">
<DropdownMenuTrigger className="rounded-full" aria-label="Konto-Menü">
<UserAvatar name={user.name} hasAvatar={user.hasAvatar} size="sm" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
@@ -84,9 +96,7 @@ export default function AppLayout({ children }: { children: ReactNode }) {
</DropdownMenuItem>
<DropdownMenuItem
variant="destructive"
onClick={() => {
void logout().then(() => router.push("/login"));
}}
onClick={() => setIsLogoutOpen(true)}
>
Abmelden
</DropdownMenuItem>
@@ -94,7 +104,11 @@ export default function AppLayout({ children }: { children: ReactNode }) {
</DropdownMenu>
</div>
</header>
<OfflineBand userId={user.id} pageContentRef={pageContentRef} />
<OfflineBand
userId={user.id}
pageContentRef={pageContentRef}
syncEnabled={sessionMode === "online"}
/>
<div
ref={pageContentRef}
tabIndex={-1}
@@ -105,6 +119,12 @@ export default function AppLayout({ children }: { children: ReactNode }) {
</div>
<BottomNav user={user} />
<QuickAddSheet user={user} variant="fab" />
<LogoutConfirmDialog
open={isLogoutOpen}
onOpenChange={setIsLogoutOpen}
userId={user.id}
onLoggedOut={() => router.push("/login")}
/>
</div>
);
}