feat: T4 wave 10 – bottling quality, phenology and honey chain UI, Telegram link/receive and ops

- Bottling quality: water content above the legal limit (strict >) answers
  409 CONFIRMATION_REQUIRED bound to the measured value; prefill for jar
  size, best-before (shelfLifeMonths, month-end clamping) and water content;
  impossible dates give 400; water-content checks in src/common/quality.
- Frontend phenology: "Entwicklung melden" sheet, IndexedDB v4 queue,
  season header on the apiary page, confirmed revoke for all apiaries.
- Frontend honey chain: "Vorschlag aus …" hints with explicit take-over,
  prefill status note, movement ledger with withdraw/correct dialogs.
- Telegram: link tokens (hashed, single use), private-chat receiver with
  lease and offset CAS, bounded poison-update skip, Retry-After, pending
  deliveries skipped on disconnect; system API for deliveries and retry;
  worker-only secret wiring and manifest policy tests.
- ARCHITECTURE.md §7, §9, §10, §11.11–11.13, Annahmen 51–55.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-14 22:47:17 +02:00
1 parent 16cc557462
commit fab7f7e12f
91 files changed
+8463 -104

No files matched your search

+21 -2
View File
@@ -7,6 +7,7 @@ import { CreateFeedingInput } from "@/lib/feedings/types";
import { CreateVarroaMeasurementInput } from "@/lib/varroa/types";
import { CreateSwarmControlCheckInput } from "@/lib/swarm-control/types";
import { CreateHarvestInput } from "@/lib/harvests/types";
import type { CreatePhenologyReportInput } from "@/lib/phenology/types";
import {
colonyIdsOfBulkInput,
invalidColonyIdsOfBody,
@@ -21,6 +22,7 @@ export interface RecordQueueInputs {
"pending-varroa-measurements": CreateVarroaMeasurementInput;
"pending-swarm-control-checks": CreateSwarmControlCheckInput;
"pending-harvests": CreateHarvestInput;
"pending-phenology-reports": CreatePhenologyReportInput;
}
export type RecordStoreName = keyof RecordQueueInputs;
/** Every store whose entries wait to be sent and are shown in the queue sheet. */
@@ -107,11 +109,12 @@ export interface OfflineDB extends DBSchema, RecordStores {
const DB_NAME = "apicore-offline";
// v1 only had "pending-inspections", v2 added the other five quick-capture
// types, v3 adds bulk entries, photos, offline packs and meta. Entries already
// types, v3 adds bulk entries, photos, offline packs and meta, v4 adds plant
// reports (Pflanzenentwicklung). Entries already
// queued on a phone must survive every upgrade, so each store is only created
// if it doesn't exist yet (guarded by oldVersion, per idb's upgrade()
// convention), never recreated.
const DB_VERSION = 3;
const DB_VERSION = 4;
const RECORD_STORES: readonly RecordStoreName[] = [
"pending-inspections",
@@ -120,6 +123,7 @@ const RECORD_STORES: readonly RecordStoreName[] = [
"pending-varroa-measurements",
"pending-swarm-control-checks",
"pending-harvests",
"pending-phenology-reports",
];
const QUEUE_STORES: readonly QueueStoreName[] = [...RECORD_STORES, "pending-bulk"];
@@ -130,6 +134,7 @@ const RECORD_LABELS: Record<RecordStoreName, string> = {
"pending-varroa-measurements": "Varroamessung",
"pending-swarm-control-checks": "Schwarmkontrolle",
"pending-harvests": "Ernte",
"pending-phenology-reports": "Pflanzenmeldung",
};
const BULK_LABELS: Record<BulkKind, string> = {
treatments: "Behandlung für mehrere Völker",
@@ -172,6 +177,12 @@ export function getDb(): Promise<IDBPDatabase<OfflineDB>> {
db.createObjectStore("offline-packs", { keyPath: "apiaryId" });
db.createObjectStore("meta");
}
if (oldVersion < 4) {
db.createObjectStore("pending-phenology-reports", {
keyPath: "id",
autoIncrement: true,
});
}
},
// A newer app version in another tab wants to upgrade: step aside
// instead of blocking it. The next call here opens the new version.
@@ -263,6 +274,14 @@ export function queueHarvest(
return queue("pending-harvests", input, idempotencyKey, ownerUserId);
}
export function queuePhenologyReport(
input: CreatePhenologyReportInput,
idempotencyKey: string,
ownerUserId: string,
): Promise<void> {
return queue("pending-phenology-reports", input, idempotencyKey, ownerUserId);
}
/**
* One entry for several colonies. The key belongs to this exact selection of
* colonies: it is stored with the entry and sent again unchanged.