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:
1 parent
c01a38835a
commit
cdd30cf3ee
288 files changed
+39165
-2853
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user