fix: resend offline-queued entries with an idempotency key
When the server saved an inspection, harvest, treatment, feeding, varroa measurement or swarm check but the answer was lost (dead zone at the apiary), the form queued the entry and the sync later created it a second time - including a second automation task. Each opened form now creates one Idempotency-Key, sends it with the request and stores it with the queued entry, so the retry returns the first record. Entries queued before this change still sync without a key. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
5d11756a87
commit
5c2ea7bf1e
16 files changed
+139
-33
No files matched your search
@@ -11,6 +11,7 @@ import { FeedType } from "@/lib/feedings/types";
|
||||
import { FEED_TYPES, FEED_TYPE_LABELS } from "@/lib/feedings/labels";
|
||||
import { ApiError } from "@/lib/auth/api-client";
|
||||
import { queueFeeding } from "@/lib/offline/db";
|
||||
import { newIdempotencyKey } from "@/lib/offline/idempotency";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -50,8 +51,12 @@ export default function NewFeedingPage() {
|
||||
const [notes, setNotes] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// One key per opened form: a double tap or an offline retry of the
|
||||
// same entry is recognised by the server instead of saved twice.
|
||||
const [idempotencyKey] = useState(newIdempotencyKey);
|
||||
const mutation = useMutation({
|
||||
mutationFn: createFeeding,
|
||||
mutationFn: (input: Parameters<typeof createFeeding>[0]) =>
|
||||
createFeeding(input, { idempotencyKey }),
|
||||
onSuccess: (feeding) => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["colonies", feeding.colonyId, "feedings"],
|
||||
@@ -67,7 +72,7 @@ export default function NewFeedingPage() {
|
||||
setError("Fütterung konnte nicht gespeichert werden.");
|
||||
return;
|
||||
}
|
||||
void queueFeeding(variables).then(() => {
|
||||
void queueFeeding(variables, idempotencyKey).then(() => {
|
||||
toast.info("Kein Netzwerk – Fütterung wurde offline gespeichert und wird synchronisiert.");
|
||||
router.push(`/colonies/${variables.colonyId}`);
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import { createHarvest } from "@/lib/harvests/api";
|
||||
import { fetchWithdrawalStatus } from "@/lib/treatments/api";
|
||||
import { ApiError } from "@/lib/auth/api-client";
|
||||
import { queueHarvest } from "@/lib/offline/db";
|
||||
import { newIdempotencyKey } from "@/lib/offline/idempotency";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -60,8 +61,12 @@ export default function NewHarvestPage() {
|
||||
(withdrawalStatuses ?? []).map((status) => [status.colonyId, status]),
|
||||
);
|
||||
|
||||
// One key per opened form: a double tap or an offline retry of the
|
||||
// same entry is recognised by the server instead of saved twice.
|
||||
const [idempotencyKey] = useState(newIdempotencyKey);
|
||||
const mutation = useMutation({
|
||||
mutationFn: createHarvest,
|
||||
mutationFn: (input: Parameters<typeof createHarvest>[0]) =>
|
||||
createHarvest(input, { idempotencyKey }),
|
||||
onSuccess: (harvest) => {
|
||||
void queryClient.invalidateQueries({ queryKey: ["harvests"] });
|
||||
toast.success("Ernte gespeichert");
|
||||
@@ -72,7 +77,7 @@ export default function NewHarvestPage() {
|
||||
setError("Ernte konnte nicht gespeichert werden.");
|
||||
return;
|
||||
}
|
||||
void queueHarvest(variables).then(() => {
|
||||
void queueHarvest(variables, idempotencyKey).then(() => {
|
||||
toast.info("Kein Netzwerk – Ernte wurde offline gespeichert und wird synchronisiert.");
|
||||
router.push("/harvests");
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import { fetchColonies } from "@/lib/colonies/api";
|
||||
import { createInspection } from "@/lib/inspections/api";
|
||||
import { ApiError } from "@/lib/auth/api-client";
|
||||
import { queueInspection } from "@/lib/offline/db";
|
||||
import { newIdempotencyKey } from "@/lib/offline/idempotency";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -69,8 +70,12 @@ export default function NewInspectionPage() {
|
||||
const [notes, setNotes] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// One key per opened form: a double tap or an offline retry of the
|
||||
// same entry is recognised by the server instead of saved twice.
|
||||
const [idempotencyKey] = useState(newIdempotencyKey);
|
||||
const mutation = useMutation({
|
||||
mutationFn: createInspection,
|
||||
mutationFn: (input: Parameters<typeof createInspection>[0]) =>
|
||||
createInspection(input, { idempotencyKey }),
|
||||
onSuccess: (inspection) => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["colonies", inspection.colonyId, "inspections"],
|
||||
@@ -83,7 +88,7 @@ export default function NewInspectionPage() {
|
||||
setError("Durchsicht konnte nicht gespeichert werden.");
|
||||
return;
|
||||
}
|
||||
void queueInspection(variables).then(() => {
|
||||
void queueInspection(variables, idempotencyKey).then(() => {
|
||||
toast.info("Kein Netzwerk – Durchsicht wurde offline gespeichert und wird synchronisiert.");
|
||||
router.push(`/colonies/${variables.colonyId}`);
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import { SWARM_ACTION_LABELS, SWARM_MOOD_LABELS } from "@/lib/swarm-control/labe
|
||||
import { SwarmControlAction, SwarmMood } from "@/lib/swarm-control/types";
|
||||
import { ApiError } from "@/lib/auth/api-client";
|
||||
import { queueSwarmControlCheck } from "@/lib/offline/db";
|
||||
import { newIdempotencyKey } from "@/lib/offline/idempotency";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -50,8 +51,12 @@ export default function NewSwarmControlCheckPage() {
|
||||
const [notes, setNotes] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// One key per opened form: a double tap or an offline retry of the
|
||||
// same entry is recognised by the server instead of saved twice.
|
||||
const [idempotencyKey] = useState(newIdempotencyKey);
|
||||
const mutation = useMutation({
|
||||
mutationFn: createSwarmControlCheck,
|
||||
mutationFn: (input: Parameters<typeof createSwarmControlCheck>[0]) =>
|
||||
createSwarmControlCheck(input, { idempotencyKey }),
|
||||
onSuccess: (check) => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["colonies", check.colonyId, "swarm-control-checks"],
|
||||
@@ -64,7 +69,7 @@ export default function NewSwarmControlCheckPage() {
|
||||
setError("Schwarmkontrolle konnte nicht gespeichert werden.");
|
||||
return;
|
||||
}
|
||||
void queueSwarmControlCheck(variables).then(() => {
|
||||
void queueSwarmControlCheck(variables, idempotencyKey).then(() => {
|
||||
toast.info("Kein Netzwerk – Schwarmkontrolle wurde offline gespeichert und wird synchronisiert.");
|
||||
router.push(`/colonies/${variables.colonyId}`);
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import { fetchColonies } from "@/lib/colonies/api";
|
||||
import { createTreatment } from "@/lib/treatments/api";
|
||||
import { ApiError } from "@/lib/auth/api-client";
|
||||
import { queueTreatment } from "@/lib/offline/db";
|
||||
import { newIdempotencyKey } from "@/lib/offline/idempotency";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -52,8 +53,12 @@ export default function NewTreatmentPage() {
|
||||
const [withdrawalPeriodDays, setWithdrawalPeriodDays] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// One key per opened form: a double tap or an offline retry of the
|
||||
// same entry is recognised by the server instead of saved twice.
|
||||
const [idempotencyKey] = useState(newIdempotencyKey);
|
||||
const mutation = useMutation({
|
||||
mutationFn: createTreatment,
|
||||
mutationFn: (input: Parameters<typeof createTreatment>[0]) =>
|
||||
createTreatment(input, { idempotencyKey }),
|
||||
onSuccess: (treatment) => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["colonies", treatment.colonyId, "treatments"],
|
||||
@@ -66,7 +71,7 @@ export default function NewTreatmentPage() {
|
||||
setError("Behandlung konnte nicht gespeichert werden.");
|
||||
return;
|
||||
}
|
||||
void queueTreatment(variables).then(() => {
|
||||
void queueTreatment(variables, idempotencyKey).then(() => {
|
||||
toast.info("Kein Netzwerk – Behandlung wurde offline gespeichert und wird synchronisiert.");
|
||||
router.push(`/colonies/${variables.colonyId}`);
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import { VarroaMethod } from "@/lib/varroa/types";
|
||||
import { VARROA_METHODS, VARROA_METHOD_LABELS } from "@/lib/varroa/labels";
|
||||
import { ApiError } from "@/lib/auth/api-client";
|
||||
import { queueVarroaMeasurement } from "@/lib/offline/db";
|
||||
import { newIdempotencyKey } from "@/lib/offline/idempotency";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -49,8 +50,12 @@ export default function NewVarroaMeasurementPage() {
|
||||
const [notes, setNotes] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// One key per opened form: a double tap or an offline retry of the
|
||||
// same entry is recognised by the server instead of saved twice.
|
||||
const [idempotencyKey] = useState(newIdempotencyKey);
|
||||
const mutation = useMutation({
|
||||
mutationFn: createVarroaMeasurement,
|
||||
mutationFn: (input: Parameters<typeof createVarroaMeasurement>[0]) =>
|
||||
createVarroaMeasurement(input, { idempotencyKey }),
|
||||
onSuccess: (measurement) => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["colonies", measurement.colonyId, "varroa-measurements"],
|
||||
@@ -63,7 +68,7 @@ export default function NewVarroaMeasurementPage() {
|
||||
setError("Varroamessung konnte nicht gespeichert werden.");
|
||||
return;
|
||||
}
|
||||
void queueVarroaMeasurement(variables).then(() => {
|
||||
void queueVarroaMeasurement(variables, idempotencyKey).then(() => {
|
||||
toast.info("Kein Netzwerk – Varroamessung wurde offline gespeichert und wird synchronisiert.");
|
||||
router.push(`/colonies/${variables.colonyId}`);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user