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]>
103 lines
3.2 KiB
TypeScript
103 lines
3.2 KiB
TypeScript
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)
|
|
);
|
|
}
|