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
@@ -17,7 +17,8 @@ import { PendingQueueSheet } from "./pending-queue-sheet";
|
||||
const COUNT_INTERVAL_MS = 15_000;
|
||||
const EMPTY_QUEUE: QueueSummary = { own: 0, failed: 0, foreign: 0 };
|
||||
// Both auto-sync triggers can fire together; they share one sync run
|
||||
// (syncPendingEntries), and one toast id keeps it to one message.
|
||||
// (syncPendingEntries), even one already running - they change nothing that
|
||||
// is to be sent - and one toast id keeps it to one message.
|
||||
const SYNC_TOAST_ID = "offline-sync";
|
||||
// Wraps on narrow phones instead of overflowing; never below 44 px.
|
||||
const BAND_BUTTON_CLASS = "h-auto min-h-11 whitespace-normal text-left";
|
||||
@@ -26,6 +27,12 @@ interface OfflineBandProps {
|
||||
userId: string;
|
||||
/** Focus target when the queue sheet closes and the band shows nothing. */
|
||||
pageContentRef: RefObject<HTMLElement | null>;
|
||||
/**
|
||||
* False in an offline session (opened from the cached identity, no access
|
||||
* token): the band then shows what waits but neither opens the sync gate
|
||||
* nor sends - the server would only answer 401.
|
||||
*/
|
||||
syncEnabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,7 +40,7 @@ interface OfflineBandProps {
|
||||
* device, how much still waits to be sent and what the server rejected.
|
||||
* Sends by itself as soon as the connection is back.
|
||||
*/
|
||||
export function OfflineBand({ userId, pageContentRef }: OfflineBandProps) {
|
||||
export function OfflineBand({ userId, pageContentRef, syncEnabled = true }: OfflineBandProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const connectivity = useConnectivity();
|
||||
const statusRef = useRef<HTMLDivElement>(null);
|
||||
@@ -68,24 +75,31 @@ export function OfflineBand({ userId, pageContentRef }: OfflineBandProps) {
|
||||
|
||||
// The band only exists for a signed-in user, and the auth context sets that
|
||||
// user's access token before the user: the moment to reopen sync after a
|
||||
// logout's cancelSync(). Declared first, so it runs before the sync below.
|
||||
// logout's cancelSync(). Not without a token (offline session): the gate
|
||||
// stays closed until the session is online. Declared first, so it runs
|
||||
// before the sync below.
|
||||
useEffect(() => {
|
||||
resumeSync();
|
||||
}, [userId]);
|
||||
if (syncEnabled) {
|
||||
resumeSync();
|
||||
}
|
||||
}, [userId, syncEnabled]);
|
||||
|
||||
// Mount, and every switch back to online (a request reached the server again).
|
||||
useEffect(() => {
|
||||
if (connectivity === "online") {
|
||||
if (syncEnabled && connectivity === "online") {
|
||||
startSync();
|
||||
}
|
||||
}, [connectivity, startSync, userId]);
|
||||
}, [connectivity, startSync, userId, syncEnabled]);
|
||||
|
||||
// The browser's own `online` event: try even while the last request failed.
|
||||
useEffect(() => {
|
||||
if (!syncEnabled) {
|
||||
return;
|
||||
}
|
||||
const handleOnline = () => startSync();
|
||||
window.addEventListener("online", handleOnline);
|
||||
return () => window.removeEventListener("online", handleOnline);
|
||||
}, [startSync]);
|
||||
}, [startSync, syncEnabled]);
|
||||
|
||||
// The sheet stays open while the user still has entries in it (waiting
|
||||
// ones too, after the last rejected one was discarded), and closes once
|
||||
@@ -95,7 +109,8 @@ export function OfflineBand({ userId, pageContentRef }: OfflineBandProps) {
|
||||
setSheetOpen(false);
|
||||
}
|
||||
|
||||
const isOffline = connectivity === "offline";
|
||||
// An offline session cannot send, whatever the network says.
|
||||
const isOffline = connectivity === "offline" || !syncEnabled;
|
||||
const hasContent = isOffline || queue.own > 0 || hasRejected;
|
||||
|
||||
// Back to the trigger while it exists; otherwise to the band's message, or
|
||||
|
||||
Reference in new issue
Block a user