feat: platform backbone and field capture, first part (checkpoint)

Platform backbone (plan 2026-10-13-plattform-rueckgrat), reviewed tasks:
- run roles api/worker/job/all with shared startup checks (Task 1)
- schema and migration for DomainEvent, deliveries, JobRun, ThrottlerHit (Task 2)
- audit, notifications and domain events written in the same transaction (Task 3)
- dispatcher with SKIP LOCKED claims, retries and stale release (Task 4)
- worker building blocks: tick, LISTEN/NOTIFY wake-up, health (Task 5)
- worker process and entry points (Task 6)
- throttler counters in Postgres (Task 19)
- separate keys per purpose with key ring (Task 20)
- public origin page without exact location (Task 22)
- shop keys split into read and write (Task 23)
- Kustomize base, components, overlay encry, CI validation (Task 26)

Field capture T3 (plan 2026-10-13-t3-erfassung-am-stand), reviewed tasks:
- brood frames, read endpoints, apiary overview and offline pack
- network basics, offline queue v3, navigation sheets, form building blocks
- new inspection with last-time hints and weather, varroa per day,
  treatment prefill, apiary page with round
- inspection photo (Task 10) is implemented, review pending

Not rolled out. Remaining tasks follow in later commits.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-13 17:57:54 +02:00
1 parent 10cbe9bf2d
commit c01a38835a
340 files changed
+25695 -1808

No files matched your search

@@ -0,0 +1,54 @@
"use client";
import { useEffect, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { useConnectivity } from "@/lib/offline/connectivity";
import { fetchTreatmentPrefill } from "./api";
import type { TreatmentPrefill } from "./types";
const DEBOUNCE_MS = 500;
const MIN_PRODUCT_LENGTH = 2;
const MAX_PRODUCT_LENGTH = 200;
/**
* The beekeeper's own last values for the product typed so far. Waits until
* typing pauses, needs at least two characters and asks only while online -
* a prefill is a convenience, never a reason to block or queue anything.
* While a request for the same product is pending or the device is offline,
* the last answer for that product stays; for any other product it is dropped.
*/
export function useTreatmentPrefill(product: string): TreatmentPrefill | null {
const trimmed = product.trim();
const [debounced, setDebounced] = useState("");
const connectivity = useConnectivity();
useEffect(() => {
const timer = setTimeout(() => setDebounced(trimmed), DEBOUNCE_MS);
return () => clearTimeout(timer);
}, [trimmed]);
const isSettled = debounced === trimmed;
const isAskable =
trimmed.length >= MIN_PRODUCT_LENGTH && trimmed.length <= MAX_PRODUCT_LENGTH;
const enabled = connectivity === "online" && isSettled && isAskable;
const { data } = useQuery({
queryKey: ["treatments", "prefill", debounced],
queryFn: () => fetchTreatmentPrefill(debounced),
enabled,
});
// The last settled answer is kept only for the product it was fetched for
// (pending request, offline). Once the product differs it is dropped at
// once, so a failed request can never leave another product's values.
const [held, setHeld] = useState<{ product: string; prefill: TreatmentPrefill | null } | null>(
null,
);
let settled: TreatmentPrefill | null | undefined;
if (isSettled && !isAskable) settled = null;
else if (enabled) settled = data;
if (settled !== undefined && (held?.product !== trimmed || held.prefill !== settled)) {
setHeld({ product: trimmed, prefill: settled });
}
if (settled !== undefined) return settled;
return held?.product === trimmed ? held.prefill : null;
}