Show Offline when the server is unreachable, and document the pilot changes

The terminal's badge only looked at navigator.onLine, so it said Online
while the server was down. It now also reflects whether the last sync or
health check reached the server. The cart badge no longer covers the
product name.

docs/PILOT_READINESS.md records what changed against the PRD review, the
decisions taken (offline sales may take stock negative, transfers may
not, server price wins but differences are flagged), rollout notes, and
what is still open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 18:54:24 +03:00
parent 472573a1a8
commit 8fe50af341
4 changed files with 122 additions and 6 deletions
+9 -2
View File
@@ -41,6 +41,9 @@ export default function TerminalPage() {
const [mpesaEnabled, setMpesaEnabled] = useState(false);
const [bootError, setBootError] = useState<string | null>(null);
const [online, setOnline] = useState(true);
// navigator.onLine only knows about the phone's own connection; this
// also goes false when the server could not be reached.
const [serverReachable, setServerReachable] = useState(true);
const [counts, setCounts] = useState({ pending: 0, rejected: 0 });
const [showQueue, setShowQueue] = useState(false);
const [queueRows, setQueueRows] = useState<QueuedSale[]>([]);
@@ -98,6 +101,7 @@ export default function TerminalPage() {
return;
}
user = cached.user;
setServerReachable(false);
setMpesaEnabled(false); // M-Pesa needs a connection anyway
show("Offline - using this phone's saved session. Sales will sync when you're back online.", "info");
}
@@ -137,6 +141,7 @@ export default function TerminalPage() {
window.addEventListener("offline", goOffline);
const unregister = registerOfflineSync({ id: me.id, businessId: me.businessId, branchId: me.branchId }, (r) => {
setCounts((c) => ({ ...c, pending: r.pending }));
setServerReachable(!r.offline);
if (r.synced > 0) show(r.synced + " saved sale(s) synced", "success");
if (r.rejected > 0) {
show(r.rejected + " sale(s) could not be synced - tap the banner to see why", "error");
@@ -235,6 +240,7 @@ export default function TerminalPage() {
if (navigator.onLine) {
flushQueue(owner).then(async (r) => {
setCounts((c) => ({ ...c, pending: r.pending }));
setServerReachable(!r.offline);
const rows = await salesFor(me.id);
const updated = rows.find((row) => row.clientUuid === sale.clientUuid);
if (updated) setScreen((s) => (s.kind === "receipt" && s.sale.clientUuid === sale.clientUuid ? { ...s, sale: updated } : s));
@@ -374,6 +380,7 @@ export default function TerminalPage() {
}
const quantities = Object.fromEntries(cart.map((c) => [c.productId, c.quantity]));
const connected = online && serverReachable;
return (
<main className="min-h-screen bg-sand-50 pb-10">
@@ -387,10 +394,10 @@ export default function TerminalPage() {
<span
className={
"rounded-full px-2.5 py-1 text-xs font-medium " +
(online ? "bg-confirm-500/20 text-confirm-500" : "bg-alert-500/20 text-alert-500")
(connected ? "bg-confirm-500/20 text-confirm-500" : "bg-alert-500/20 text-alert-500")
}
>
{online ? "● Online" : "● Offline"}
{connected ? "● Online" : "● Offline"}
</span>
<button onClick={handleSignOut} className="text-xs text-sand-200/60 underline underline-offset-2">
Sign out