feat: extend offline queueing to all quick-capture forms
Only Durchsicht had an offline fallback (queue to IndexedDB, sync on reconnect); Behandlung, Fütterung, Varroamessung, Schwarmkontrolle and Ernte just failed outright with no signal - exactly the situation a beekeeper is in at the actual hive. Generalizes lib/offline/db.ts and sync.ts to six queues (version-bumped IndexedDB schema, existing queued inspections preserved), wires the same "server rejected it vs. no network" fallback into all five remaining */new pages, and updates the sync badge to count/drain all six. Also: doc-hygiene pass (ARCHITECTURE.md/TASKS.md) to replace the stale pg_dump/CronJob backup plan with the in-app BackupModule that actually shipped earlier this session. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
ac45514356
commit
1f67cd5548
11 files changed
+297
-46
No files matched your search
@@ -1,52 +1,187 @@
|
||||
import { openDB, DBSchema, IDBPDatabase } from "idb";
|
||||
import { CreateInspectionInput } from "@/lib/inspections/types";
|
||||
import { CreateTreatmentInput } from "@/lib/treatments/types";
|
||||
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";
|
||||
|
||||
interface OfflineDB extends DBSchema {
|
||||
"pending-inspections": {
|
||||
key: number;
|
||||
value: PendingInspection;
|
||||
};
|
||||
}
|
||||
type PendingStoreName =
|
||||
| "pending-inspections"
|
||||
| "pending-treatments"
|
||||
| "pending-feedings"
|
||||
| "pending-varroa-measurements"
|
||||
| "pending-swarm-control-checks"
|
||||
| "pending-harvests";
|
||||
|
||||
export interface PendingInspection {
|
||||
interface PendingEntry<T> {
|
||||
id: number;
|
||||
input: CreateInspectionInput;
|
||||
input: T;
|
||||
queuedAt: string;
|
||||
}
|
||||
|
||||
interface OfflineDB extends DBSchema {
|
||||
"pending-inspections": { key: number; value: PendingEntry<CreateInspectionInput> };
|
||||
"pending-treatments": { key: number; value: PendingEntry<CreateTreatmentInput> };
|
||||
"pending-feedings": { key: number; value: PendingEntry<CreateFeedingInput> };
|
||||
"pending-varroa-measurements": {
|
||||
key: number;
|
||||
value: PendingEntry<CreateVarroaMeasurementInput>;
|
||||
};
|
||||
"pending-swarm-control-checks": {
|
||||
key: number;
|
||||
value: PendingEntry<CreateSwarmControlCheckInput>;
|
||||
};
|
||||
"pending-harvests": { key: number; value: PendingEntry<CreateHarvestInput> };
|
||||
}
|
||||
|
||||
const DB_NAME = "apicore-offline";
|
||||
const DB_VERSION = 1;
|
||||
const STORE_NAME = "pending-inspections";
|
||||
// v1 only had "pending-inspections". v2 adds the other five quick-capture
|
||||
// types - existing users' already-queued inspections must survive the
|
||||
// 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 = 2;
|
||||
|
||||
let dbPromise: Promise<IDBPDatabase<OfflineDB>> | null = null;
|
||||
|
||||
function getDb(): Promise<IDBPDatabase<OfflineDB>> {
|
||||
if (!dbPromise) {
|
||||
dbPromise = openDB<OfflineDB>(DB_NAME, DB_VERSION, {
|
||||
upgrade(db) {
|
||||
db.createObjectStore(STORE_NAME, { keyPath: "id", autoIncrement: true });
|
||||
upgrade(db, oldVersion) {
|
||||
if (oldVersion < 1) {
|
||||
db.createObjectStore("pending-inspections", { keyPath: "id", autoIncrement: true });
|
||||
}
|
||||
if (oldVersion < 2) {
|
||||
db.createObjectStore("pending-treatments", { keyPath: "id", autoIncrement: true });
|
||||
db.createObjectStore("pending-feedings", { keyPath: "id", autoIncrement: true });
|
||||
db.createObjectStore("pending-varroa-measurements", {
|
||||
keyPath: "id",
|
||||
autoIncrement: true,
|
||||
});
|
||||
db.createObjectStore("pending-swarm-control-checks", {
|
||||
keyPath: "id",
|
||||
autoIncrement: true,
|
||||
});
|
||||
db.createObjectStore("pending-harvests", { keyPath: "id", autoIncrement: true });
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
return dbPromise;
|
||||
}
|
||||
|
||||
export async function queueInspection(input: CreateInspectionInput): Promise<void> {
|
||||
async function queue<T>(store: PendingStoreName, input: T): Promise<void> {
|
||||
const db = await getDb();
|
||||
await db.add(STORE_NAME, { input, queuedAt: new Date().toISOString() } as PendingInspection);
|
||||
await db.add(store, { input, queuedAt: new Date().toISOString() } as never);
|
||||
}
|
||||
|
||||
export async function getPendingInspections(): Promise<PendingInspection[]> {
|
||||
async function getPending<T>(store: PendingStoreName): Promise<PendingEntry<T>[]> {
|
||||
const db = await getDb();
|
||||
return db.getAll(STORE_NAME);
|
||||
return db.getAll(store) as Promise<PendingEntry<T>[]>;
|
||||
}
|
||||
|
||||
export async function removePendingInspection(id: number): Promise<void> {
|
||||
async function removePending(store: PendingStoreName, id: number): Promise<void> {
|
||||
const db = await getDb();
|
||||
await db.delete(STORE_NAME, id);
|
||||
await db.delete(store, id);
|
||||
}
|
||||
|
||||
export async function countPendingInspections(): Promise<number> {
|
||||
async function countPending(store: PendingStoreName): Promise<number> {
|
||||
const db = await getDb();
|
||||
return db.count(STORE_NAME);
|
||||
return db.count(store);
|
||||
}
|
||||
|
||||
export type PendingInspection = PendingEntry<CreateInspectionInput>;
|
||||
export function queueInspection(input: CreateInspectionInput): Promise<void> {
|
||||
return queue("pending-inspections", input);
|
||||
}
|
||||
export function getPendingInspections(): Promise<PendingInspection[]> {
|
||||
return getPending("pending-inspections");
|
||||
}
|
||||
export function removePendingInspection(id: number): Promise<void> {
|
||||
return removePending("pending-inspections", id);
|
||||
}
|
||||
export function countPendingInspections(): Promise<number> {
|
||||
return countPending("pending-inspections");
|
||||
}
|
||||
|
||||
export type PendingTreatment = PendingEntry<CreateTreatmentInput>;
|
||||
export function queueTreatment(input: CreateTreatmentInput): Promise<void> {
|
||||
return queue("pending-treatments", input);
|
||||
}
|
||||
export function getPendingTreatments(): Promise<PendingTreatment[]> {
|
||||
return getPending("pending-treatments");
|
||||
}
|
||||
export function removePendingTreatment(id: number): Promise<void> {
|
||||
return removePending("pending-treatments", id);
|
||||
}
|
||||
export function countPendingTreatments(): Promise<number> {
|
||||
return countPending("pending-treatments");
|
||||
}
|
||||
|
||||
export type PendingFeeding = PendingEntry<CreateFeedingInput>;
|
||||
export function queueFeeding(input: CreateFeedingInput): Promise<void> {
|
||||
return queue("pending-feedings", input);
|
||||
}
|
||||
export function getPendingFeedings(): Promise<PendingFeeding[]> {
|
||||
return getPending("pending-feedings");
|
||||
}
|
||||
export function removePendingFeeding(id: number): Promise<void> {
|
||||
return removePending("pending-feedings", id);
|
||||
}
|
||||
export function countPendingFeedings(): Promise<number> {
|
||||
return countPending("pending-feedings");
|
||||
}
|
||||
|
||||
export type PendingVarroaMeasurement = PendingEntry<CreateVarroaMeasurementInput>;
|
||||
export function queueVarroaMeasurement(input: CreateVarroaMeasurementInput): Promise<void> {
|
||||
return queue("pending-varroa-measurements", input);
|
||||
}
|
||||
export function getPendingVarroaMeasurements(): Promise<PendingVarroaMeasurement[]> {
|
||||
return getPending("pending-varroa-measurements");
|
||||
}
|
||||
export function removePendingVarroaMeasurement(id: number): Promise<void> {
|
||||
return removePending("pending-varroa-measurements", id);
|
||||
}
|
||||
export function countPendingVarroaMeasurements(): Promise<number> {
|
||||
return countPending("pending-varroa-measurements");
|
||||
}
|
||||
|
||||
export type PendingSwarmControlCheck = PendingEntry<CreateSwarmControlCheckInput>;
|
||||
export function queueSwarmControlCheck(input: CreateSwarmControlCheckInput): Promise<void> {
|
||||
return queue("pending-swarm-control-checks", input);
|
||||
}
|
||||
export function getPendingSwarmControlChecks(): Promise<PendingSwarmControlCheck[]> {
|
||||
return getPending("pending-swarm-control-checks");
|
||||
}
|
||||
export function removePendingSwarmControlCheck(id: number): Promise<void> {
|
||||
return removePending("pending-swarm-control-checks", id);
|
||||
}
|
||||
export function countPendingSwarmControlChecks(): Promise<number> {
|
||||
return countPending("pending-swarm-control-checks");
|
||||
}
|
||||
|
||||
export type PendingHarvest = PendingEntry<CreateHarvestInput>;
|
||||
export function queueHarvest(input: CreateHarvestInput): Promise<void> {
|
||||
return queue("pending-harvests", input);
|
||||
}
|
||||
export function getPendingHarvests(): Promise<PendingHarvest[]> {
|
||||
return getPending("pending-harvests");
|
||||
}
|
||||
export function removePendingHarvest(id: number): Promise<void> {
|
||||
return removePending("pending-harvests", id);
|
||||
}
|
||||
export function countPendingHarvests(): Promise<number> {
|
||||
return countPending("pending-harvests");
|
||||
}
|
||||
|
||||
export async function countAllPending(): Promise<number> {
|
||||
const counts = await Promise.all([
|
||||
countPendingInspections(),
|
||||
countPendingTreatments(),
|
||||
countPendingFeedings(),
|
||||
countPendingVarroaMeasurements(),
|
||||
countPendingSwarmControlChecks(),
|
||||
countPendingHarvests(),
|
||||
]);
|
||||
return counts.reduce((sum, count) => sum + count, 0);
|
||||
}
|
||||
Reference in new issue
Block a user