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
@@ -0,0 +1,102 @@
|
||||
import { ApiError, apiFetch } from "@/lib/auth/api-client";
|
||||
import { CreateOptions, idempotencyHeaders } from "@/lib/offline/idempotency";
|
||||
import { invalidColonyIdsOfBody } from "./bulk-input";
|
||||
import type { CreateTreatmentInput, Treatment } from "@/lib/treatments/types";
|
||||
import type { CreateFeedingInput, Feeding } from "@/lib/feedings/types";
|
||||
import type { CreateVarroaMeasurementInput, VarroaMeasurement } from "@/lib/varroa/types";
|
||||
|
||||
/** Mirrors ApiCore-Backend/src/common/bulk/bulk-limits.ts - keep both in step. */
|
||||
export const BULK_MAX_COLONIES = 100;
|
||||
|
||||
export interface BulkCreateResult<T> {
|
||||
bulkId: string;
|
||||
records: T[];
|
||||
}
|
||||
|
||||
export interface BulkCreateTreatmentInput extends Omit<CreateTreatmentInput, "colonyId"> {
|
||||
colonyIds: string[];
|
||||
}
|
||||
|
||||
export interface BulkCreateFeedingInput extends Omit<CreateFeedingInput, "colonyId"> {
|
||||
colonyIds: string[];
|
||||
}
|
||||
|
||||
/** What each colony has of its own in a bulk count. */
|
||||
export interface BulkVarroaEntry {
|
||||
colonyId: string;
|
||||
miteCount?: number | null;
|
||||
calculatedValue?: number | null;
|
||||
assessment?: string | null;
|
||||
}
|
||||
|
||||
export interface BulkCreateVarroaInput
|
||||
extends Omit<
|
||||
CreateVarroaMeasurementInput,
|
||||
"colonyId" | "miteCount" | "calculatedValue" | "assessment"
|
||||
> {
|
||||
entries: BulkVarroaEntry[];
|
||||
}
|
||||
|
||||
function postBulk<Record>(
|
||||
path: string,
|
||||
input: unknown,
|
||||
options: CreateOptions,
|
||||
): Promise<BulkCreateResult<Record>> {
|
||||
return apiFetch<BulkCreateResult<Record>>(path, {
|
||||
method: "POST",
|
||||
headers: idempotencyHeaders(options),
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
export function createTreatmentsBulk(
|
||||
input: BulkCreateTreatmentInput,
|
||||
options: CreateOptions = {},
|
||||
): Promise<BulkCreateResult<Treatment>> {
|
||||
return postBulk("/treatments/bulk", input, options);
|
||||
}
|
||||
|
||||
export function createFeedingsBulk(
|
||||
input: BulkCreateFeedingInput,
|
||||
options: CreateOptions = {},
|
||||
): Promise<BulkCreateResult<Feeding>> {
|
||||
return postBulk("/feedings/bulk", input, options);
|
||||
}
|
||||
|
||||
export function createVarroaBulk(
|
||||
input: BulkCreateVarroaInput,
|
||||
options: CreateOptions = {},
|
||||
): Promise<BulkCreateResult<VarroaMeasurement>> {
|
||||
return postBulk("/varroa-measurements/bulk", input, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* The colonies a refused bulk entry could not be recorded for (400
|
||||
* `invalidColonyIds`: unknown or ended since the pack was loaded). Empty for
|
||||
* every other error.
|
||||
*/
|
||||
export function invalidColonyIdsOf(error: unknown): string[] {
|
||||
return error instanceof ApiError ? invalidColonyIdsOfBody(error.body) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* The two 409 texts of the idempotency interceptor that mean „later“. The
|
||||
* server sends no error code for them; api.spec.ts pins both to its source.
|
||||
*/
|
||||
export const RETRY_LATER_MESSAGES = [
|
||||
"Diese Anfrage wird bereits verarbeitet - bitte kurz warten",
|
||||
"Diese Anfrage wurde gerade abgebrochen - bitte erneut senden",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* 409 „wird bereits verarbeitet“ (the same key is still running) and „gerade
|
||||
* abgebrochen“ (it was just released): nothing is wrong with the entry, the
|
||||
* answer is „später erneut“. Other 409s (key used for another action) are real.
|
||||
*/
|
||||
export function isRetryLaterError(error: unknown): boolean {
|
||||
return (
|
||||
error instanceof ApiError &&
|
||||
error.status === 409 &&
|
||||
RETRY_LATER_MESSAGES.some((message) => error.message === message)
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user