-
diff --git a/web/src/routes/FlocksPage.tsx b/web/src/routes/FlocksPage.tsx
index dde657029..acde182db 100644
--- a/web/src/routes/FlocksPage.tsx
+++ b/web/src/routes/FlocksPage.tsx
@@ -268,13 +268,20 @@ export function FlocksPage() {
onClick={() => startEdit(f)}>edit
{f.status === "Active" && (
)}
{f.status !== "Archived" && (
)}
diff --git a/web/src/routes/SalesPage.tsx b/web/src/routes/SalesPage.tsx
index f2408eff5..c8ce1784d 100644
--- a/web/src/routes/SalesPage.tsx
+++ b/web/src/routes/SalesPage.tsx
@@ -156,6 +156,9 @@ export function SalesPage() {
const onConfirm = () => run(async () => {
if (!active) return;
+ // One-way action (#59): allocation cannot be undone yet (void is #60).
+ if (!window.confirm(
+ "Confirm this order? Stock is allocated from inventory (FIFO) and cannot be undone.")) return;
const scope = `confirm:${active.id}`;
await confirmOrder(active.id, keyFor(scope));
const refreshed = await getOrder(active.id);
@@ -167,6 +170,7 @@ export function SalesPage() {
const onCancel = () => run(async () => {
if (!active) return;
+ if (!window.confirm("Cancel this draft? Its line items are discarded.")) return;
const scope = `cancel:${active.id}`;
await cancelOrder(active.id, keyFor(scope));
setActive(null);
From 9f906e2df1a7034e3c4b4d284e165d162ef266d7 Mon Sep 17 00:00:00 2001
From: mforce <>
Date: Thu, 16 Jul 2026 16:33:44 -0700
Subject: [PATCH 2/2] Address codex + pi review feedback on destructive-action
guards
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Prefill retry no longer clears the blocked state optimistically:
saves stay disabled from the moment a prefill is in flight until it
succeeds (new prefillPending state) — the optimistic clear reopened
the exact overwrite window the guard exists to close. (codex P1,
pi High)
- A retry that recovers for the same flock+date no longer zeroes the
form: values typed while the banner was up survive (there is no
server entry to overwrite); a normal date/flock change still resets.
(pi Medium)
- Sales confirms hoisted above run(): buttons no longer flash disabled
while the user is deciding, matching the Flocks pattern. (pi Medium)
- Cancel-draft dialog reworded: cancellation is a status change — the
order keeps its lines and becomes read-only; "lines are discarded"
was wrong. (codex P2)
Re-verified in chromium: retry-while-broken stays blocked, typed
values survive a retry recovery, normal date change still resets,
recovery unblocks saving.
---
web/src/routes/DailyEntryPage.tsx | 32 ++++++++++++++-----
web/src/routes/SalesPage.tsx | 51 ++++++++++++++++++-------------
2 files changed, 55 insertions(+), 28 deletions(-)
diff --git a/web/src/routes/DailyEntryPage.tsx b/web/src/routes/DailyEntryPage.tsx
index 27273c5eb..412238af4 100644
--- a/web/src/routes/DailyEntryPage.tsx
+++ b/web/src/routes/DailyEntryPage.tsx
@@ -41,9 +41,12 @@ export function DailyEntryPage() {
// so an untouched re-save must not wipe an existing entry's grading.
const [gradesTouched, setGradesTouched] = useState(false);
const [existingStatus, setExistingStatus] = useState(null);
- // Prefill failure blocks saving (silent-overwrite guard, #59).
+ // Prefill failure OR in-flight prefill blocks saving (silent-overwrite
+ // guard, #59); failedTarget marks which flock+date the failure was for.
const [prefillFailed, setPrefillFailed] = useState(false);
+ const [prefillPending, setPrefillPending] = useState(false);
const [prefillRetry, setPrefillRetry] = useState(0);
+ const failedTarget = useRef(null);
const [busy, setBusy] = useState(false);
const inFlight = useRef(false);
@@ -81,11 +84,19 @@ export function DailyEntryPage() {
useEffect(() => {
if (!flockId || !date) return;
let cancelled = false;
- setPrefillFailed(false);
+ // Saves stay blocked from the moment a prefill is in flight until it
+ // SUCCEEDS — clearing the flag optimistically would reopen the exact
+ // overwrite window this guard closes (#61 review).
+ setPrefillPending(true);
+ const target = `${flockId}|${date}`;
listDailyEntries({ flockId, from: date, to: date, limit: 1 })
.then((entries) => {
if (cancelled) return;
const existing = entries.find((e) => e.date === date);
+ // A retry that recovers for the SAME flock+date must not zero the form:
+ // the user may have typed while the banner was up, and with no server
+ // entry there is nothing to overwrite.
+ const isRetryRecovery = failedTarget.current === target;
if (existing) {
setTotalEggs(existing.totalEggs);
setCracked(existing.crackedEggs);
@@ -95,18 +106,25 @@ export function DailyEntryPage() {
setGradeQty(Object.fromEntries(existing.grades.map((g) => [g.eggGradeId, g.quantity])));
setGradesTouched(existing.grades.length > 0);
setExistingStatus(existing.status);
- } else {
+ } else if (!isRetryRecovery) {
setTotalEggs(0); setCracked(0); setDirty(0); setDiscarded(0); setMortality(0);
setGradeQty({});
setGradesTouched(false);
setExistingStatus(null);
}
+ failedTarget.current = null;
+ setPrefillFailed(false);
+ setPrefillPending(false);
})
.catch(() => {
// Not best-effort (#59): without the prefill we can't know whether this
// day already has data — saving would overwrite it with zeros. Block
// saving until a retry succeeds.
- if (!cancelled) setPrefillFailed(true);
+ if (!cancelled) {
+ failedTarget.current = target;
+ setPrefillFailed(true);
+ setPrefillPending(false);
+ }
});
return () => { cancelled = true; };
}, [flockId, date, prefillRetry]);
@@ -152,7 +170,7 @@ export function DailyEntryPage() {
}
async function onSave(submit: boolean) {
- if (inFlight.current || !selectedFlock || prefillFailed) return; // sync re-entry guard
+ if (inFlight.current || !selectedFlock || prefillFailed || prefillPending) return; // sync re-entry guard
// One-way action (#59): submit freezes the day and creates egg lots.
if (submit && !window.confirm(
"Submit this day? Egg lots are created and the entry can no longer be edited — corrections need a manager adjustment.")) return;
@@ -295,10 +313,10 @@ export function DailyEntryPage() {
{message &&
{message}
}
-
diff --git a/web/src/routes/SalesPage.tsx b/web/src/routes/SalesPage.tsx
index c8ce1784d..36955acc2 100644
--- a/web/src/routes/SalesPage.tsx
+++ b/web/src/routes/SalesPage.tsx
@@ -154,30 +154,39 @@ export function SalesPage() {
clearKey(scope);
});
- const onConfirm = () => run(async () => {
- if (!active) return;
- // One-way action (#59): allocation cannot be undone yet (void is #60).
+ // One-way actions (#59). Confirm BEFORE run() so buttons don't flash
+ // disabled while the user decides.
+ const onConfirm = () => {
+ // Allocation cannot be undone yet (void is #60).
if (!window.confirm(
"Confirm this order? Stock is allocated from inventory (FIFO) and cannot be undone.")) return;
- const scope = `confirm:${active.id}`;
- await confirmOrder(active.id, keyFor(scope));
- const refreshed = await getOrder(active.id);
- setActive(refreshed);
- setMessage(`Order ${refreshed.referenceNumber} confirmed — stock allocated (FIFO).`);
- await loadOrders();
- clearKey(scope);
- });
+ void run(async () => {
+ if (!active) return;
+ const scope = `confirm:${active.id}`;
+ await confirmOrder(active.id, keyFor(scope));
+ const refreshed = await getOrder(active.id);
+ setActive(refreshed);
+ setMessage(`Order ${refreshed.referenceNumber} confirmed — stock allocated (FIFO).`);
+ await loadOrders();
+ clearKey(scope);
+ });
+ };
- const onCancel = () => run(async () => {
- if (!active) return;
- if (!window.confirm("Cancel this draft? Its line items are discarded.")) return;
- const scope = `cancel:${active.id}`;
- await cancelOrder(active.id, keyFor(scope));
- setActive(null);
- setMessage("Draft order cancelled.");
- await loadOrders();
- clearKey(scope);
- });
+ const onCancel = () => {
+ // Cancel is a status change: the order keeps its lines but becomes
+ // read-only and can't be confirmed.
+ if (!window.confirm(
+ "Cancel this draft? The order becomes cancelled and can no longer be edited or confirmed.")) return;
+ void run(async () => {
+ if (!active) return;
+ const scope = `cancel:${active.id}`;
+ await cancelOrder(active.id, keyFor(scope));
+ setActive(null);
+ setMessage("Draft order cancelled.");
+ await loadOrders();
+ clearKey(scope);
+ });
+ };
// Always fetch fresh on open — the list row may be stale relative to
// mutations made through the panel since the list was loaded.