feat: bump client-services (accumulated feature work + deploy fixes)
Snapshot of in-progress work across local_backend, manager_dashboard, and waiter_pwa (pricing, chat, fiscal, prep zones, recovery codes, CRM, inventory, permissions), plus the nginx/docker-compose deploy fixes for the Unraid + NPM reverse-proxy setup. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,57 +1,77 @@
|
||||
import { create } from 'zustand'
|
||||
|
||||
/**
|
||||
* Tracks the live connection state and emergency mode flag.
|
||||
* Tracks live connection state.
|
||||
*
|
||||
* States:
|
||||
* 'online' — server reachable, SSE connected, normal operation
|
||||
* 'reconnecting' — connection blip detected; 5-second grace before showing full modal
|
||||
* 'lost' — grace period expired, modal shown (Wait / Emergency)
|
||||
* 'emergency' — user chose emergency mode, working from IndexedDB snapshot
|
||||
* status:
|
||||
* 'online' — server reachable, SSE connected, normal operation
|
||||
* 'reconnecting' — blip detected, grace period (amber bar)
|
||||
* 'offline' — confirmed unreachable, working from local DB (red bar)
|
||||
*
|
||||
* The old 'emergency' and 'lost' states are gone. The app now enters offline
|
||||
* mode automatically — no user prompt needed.
|
||||
*
|
||||
* After 5 minutes offline the bar flashes to signal a serious problem.
|
||||
*/
|
||||
|
||||
const GRACE_MS = 5_000
|
||||
const GRACE_MS = 4_000 // how long to wait before declaring offline
|
||||
const FLASH_MS = 5 * 60 * 1000 // 5 minutes before bar starts flashing
|
||||
|
||||
const useConnectionStore = create((set, get) => ({
|
||||
status: 'online', // 'online' | 'reconnecting' | 'lost' | 'emergency'
|
||||
lostAt: null,
|
||||
status: 'online', // 'online' | 'reconnecting' | 'offline'
|
||||
sseAlive: false, // true when SSE stream is actively connected
|
||||
lostAt: null, // Date when connection was first lost
|
||||
flashing: false, // true when offline > 5 min
|
||||
_graceTimer: null,
|
||||
_flashTimer: null,
|
||||
|
||||
setSseAlive: (alive) => set({ sseAlive: alive }),
|
||||
|
||||
setLost: () => {
|
||||
const { status, _graceTimer } = get()
|
||||
// Already lost or in emergency — no-op
|
||||
if (status === 'lost' || status === 'emergency') return
|
||||
// Already in grace period — don't restart the timer
|
||||
if (status === 'offline') return
|
||||
if (status === 'reconnecting') return
|
||||
|
||||
// Start grace period
|
||||
const timer = setTimeout(() => {
|
||||
// Only escalate if we're still in reconnecting (not recovered in the meantime)
|
||||
if (get().status === 'reconnecting') {
|
||||
set({ status: 'lost', _graceTimer: null })
|
||||
}
|
||||
const graceTimer = setTimeout(() => {
|
||||
if (get().status !== 'reconnecting') return
|
||||
const lostAt = get().lostAt
|
||||
|
||||
// Start the 5-min flash timer
|
||||
const flashTimer = setTimeout(() => {
|
||||
if (get().status === 'offline') set({ flashing: true })
|
||||
}, FLASH_MS)
|
||||
|
||||
set({ status: 'offline', _graceTimer: null, _flashTimer: flashTimer })
|
||||
}, GRACE_MS)
|
||||
|
||||
set({ status: 'reconnecting', lostAt: new Date(), _graceTimer: timer })
|
||||
set({ status: 'reconnecting', lostAt: new Date(), _graceTimer: graceTimer })
|
||||
},
|
||||
|
||||
setOnline: () => {
|
||||
const { _graceTimer } = get()
|
||||
const { _graceTimer, _flashTimer } = get()
|
||||
if (_graceTimer) clearTimeout(_graceTimer)
|
||||
set({ status: 'online', lostAt: null, _graceTimer: null })
|
||||
if (_flashTimer) clearTimeout(_flashTimer)
|
||||
set({ status: 'online', lostAt: null, flashing: false, _graceTimer: null, _flashTimer: null })
|
||||
},
|
||||
|
||||
// Convenience accessors
|
||||
isOnline: () => get().status === 'online',
|
||||
isOffline: () => get().status === 'offline' || get().status === 'reconnecting',
|
||||
|
||||
// Legacy compat — SSEContext still calls these
|
||||
enterEmergency: () => {
|
||||
const { _graceTimer } = get()
|
||||
// treated as manual offline entry (immediate, no grace period)
|
||||
const { _graceTimer, _flashTimer } = get()
|
||||
if (_graceTimer) clearTimeout(_graceTimer)
|
||||
set({ status: 'emergency', _graceTimer: null })
|
||||
if (_flashTimer) clearTimeout(_flashTimer)
|
||||
const flashTimer = setTimeout(() => {
|
||||
if (get().status === 'offline') set({ flashing: true })
|
||||
}, FLASH_MS)
|
||||
set({ status: 'offline', lostAt: new Date(), flashing: false, _graceTimer: null, _flashTimer: flashTimer })
|
||||
},
|
||||
|
||||
exitEmergency: () => set({ status: 'online', lostAt: null, _graceTimer: null }),
|
||||
|
||||
isOnline: () => get().status === 'online',
|
||||
isLost: () => get().status === 'lost',
|
||||
isEmergency: () => get().status === 'emergency',
|
||||
exitEmergency: () => get().setOnline(),
|
||||
isEmergency: () => get().status === 'offline',
|
||||
isLost: () => get().status === 'offline',
|
||||
}))
|
||||
|
||||
export default useConnectionStore
|
||||
|
||||
Reference in New Issue
Block a user