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:
1 parent
16cc557462
commit
fab7f7e12f
91 files changed
+8463
-104
No files matched your search
@@ -63,12 +63,114 @@ async function createVersion2Queue(entry: object): Promise<void> {
|
||||
db.close();
|
||||
}
|
||||
|
||||
/** A device that last opened the app with DB v3 (before plant reports). */
|
||||
async function createVersion3Queue(inspection: object): Promise<void> {
|
||||
const db = await openDB(DB_NAME, 3, {
|
||||
upgrade(database) {
|
||||
for (const store of [...VERSION_2_STORES, "pending-bulk"]) {
|
||||
database.createObjectStore(store, {
|
||||
keyPath: "id",
|
||||
autoIncrement: true,
|
||||
});
|
||||
}
|
||||
const photos = database.createObjectStore("pending-photos", {
|
||||
keyPath: "id",
|
||||
});
|
||||
photos.createIndex("sessionId", "sessionId");
|
||||
photos.createIndex("inspectionIdempotencyKey", "inspectionIdempotencyKey");
|
||||
photos.createIndex("ownerUserId", "ownerUserId");
|
||||
database.createObjectStore("offline-packs", { keyPath: "apiaryId" });
|
||||
database.createObjectStore("meta");
|
||||
},
|
||||
});
|
||||
await db.add("pending-inspections", inspection);
|
||||
db.close();
|
||||
}
|
||||
|
||||
describe("offline queue database", () => {
|
||||
beforeEach(() => {
|
||||
globalThis.indexedDB = new IDBFactory();
|
||||
});
|
||||
|
||||
it("upgrading v2 → v3 keeps already queued inspections", async () => {
|
||||
it.each([
|
||||
["a fresh install (0)", 0],
|
||||
["version 1", 1],
|
||||
])("opening from %s creates the plant report store at version 4", async (_name, from) => {
|
||||
if (from === 1) {
|
||||
const old = await openDB(DB_NAME, 1, {
|
||||
upgrade(database) {
|
||||
database.createObjectStore("pending-inspections", {
|
||||
keyPath: "id",
|
||||
autoIncrement: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
await old.add("pending-inspections", {
|
||||
input: INSPECTION,
|
||||
queuedAt: "2026-10-01T08:00:00.000Z",
|
||||
});
|
||||
old.close();
|
||||
}
|
||||
const db = await loadDb();
|
||||
|
||||
await db.queuePhenologyReport(
|
||||
{ phase: "APFEL", date: "2026-04-20" },
|
||||
"plant-key-0000000003",
|
||||
"user-1",
|
||||
);
|
||||
|
||||
const upgraded = await openDB(DB_NAME);
|
||||
expect(upgraded.version).toBe(4);
|
||||
expect([...upgraded.objectStoreNames]).toContain("pending-phenology-reports");
|
||||
expect(await upgraded.count("pending-phenology-reports")).toBe(1);
|
||||
expect(await upgraded.count("pending-inspections")).toBe(from === 1 ? 1 : 0);
|
||||
upgraded.close();
|
||||
});
|
||||
|
||||
it("db upgrade 3 -> 4 keeps queued inspections and adds the plant report queue", async () => {
|
||||
await createVersion3Queue({
|
||||
input: INSPECTION,
|
||||
idempotencyKey: "key-from-v3",
|
||||
ownerUserId: "user-1",
|
||||
queuedAt: "2026-10-01T08:00:00.000Z",
|
||||
});
|
||||
const db = await loadDb();
|
||||
|
||||
const entries = await db.getPendingEntries("pending-inspections");
|
||||
expect(entries).toEqual([
|
||||
{
|
||||
id: 1,
|
||||
input: INSPECTION,
|
||||
idempotencyKey: "key-from-v3",
|
||||
ownerUserId: "user-1",
|
||||
queuedAt: "2026-10-01T08:00:00.000Z",
|
||||
},
|
||||
]);
|
||||
await db.queuePhenologyReport(
|
||||
{ phase: "SALWEIDE", date: "2026-03-20", apiaryId: null },
|
||||
"key-plant-0000000001",
|
||||
"user-1",
|
||||
);
|
||||
expect(await db.getPendingEntries("pending-phenology-reports")).toEqual([
|
||||
expect.objectContaining({
|
||||
input: { phase: "SALWEIDE", date: "2026-03-20", apiaryId: null },
|
||||
idempotencyKey: "key-plant-0000000001",
|
||||
ownerUserId: "user-1",
|
||||
}),
|
||||
]);
|
||||
expect(await db.summarizeQueue("user-1")).toEqual({
|
||||
own: 2,
|
||||
failed: 0,
|
||||
foreign: 0,
|
||||
});
|
||||
expect((await db.listQueue("user-1")).map((item) => item.label)).toContain("Pflanzenmeldung");
|
||||
|
||||
const upgraded = await openDB(DB_NAME);
|
||||
expect(upgraded.version).toBe(4);
|
||||
upgraded.close();
|
||||
});
|
||||
|
||||
it("upgrading v2 → v4 keeps already queued inspections", async () => {
|
||||
await createVersion2Queue({
|
||||
input: INSPECTION,
|
||||
idempotencyKey: "key-from-v2",
|
||||
@@ -90,7 +192,7 @@ describe("offline queue database", () => {
|
||||
expect(await db.summarizeQueue("user-1")).toEqual({ own: 1, failed: 0, foreign: 0 });
|
||||
|
||||
const upgraded = await openDB(DB_NAME);
|
||||
expect(upgraded.version).toBe(3);
|
||||
expect(upgraded.version).toBe(4);
|
||||
expect([...upgraded.objectStoreNames]).toEqual(
|
||||
expect.arrayContaining([
|
||||
...VERSION_2_STORES,
|
||||
@@ -98,6 +200,7 @@ describe("offline queue database", () => {
|
||||
"pending-photos",
|
||||
"offline-packs",
|
||||
"meta",
|
||||
"pending-phenology-reports",
|
||||
]),
|
||||
);
|
||||
expect([...upgraded.transaction("pending-photos").store.indexNames].sort()).toEqual([
|
||||
|
||||
Reference in new issue
Block a user