7680d9a0ed
Accidental admin deletes of users/roles/subscriptions/plans/tariffs were hard and unrecoverable. Now they soft-delete into a recycle bin. Schema (migration 0012): nullable deleted_at + deleted_by on users, roles, subscriptions, subscription_plans, tariffs. Additive ADD COLUMN; verified against a copy of the live DB. Backend: each resource's DELETE route STAMPS instead of removing; every catalog list filters deleted_at IS NULL. New recycle-bin module + routes (GET /api/recycle-bin, POST .../restore, DELETE .../:id purge) gated on a new recyclebin:read/update/delete permission. A 6-hourly + startup sweep auto-purges items older than RECYCLE_BIN_RETENTION_DAYS (default 30; 0 = forever). Invariants: soft-deleted users can't log in (login rejects deleted_at; no-lockout counts live admins only); a soft-deleted subscription doesn't open the barrier; plans are versioned so a delete stamps all versions of the plan_id (bin shows one item); username/role-name UNIQUE spans deleted rows so reuse returns a clear 409 pointing at the bin; restore doesn't auto-cascade a dangling role (guard resolves missing role to empty perms). The signed append-only ledger is OUT of scope (no delete path). Web: a Recycle bin tab under Setup (RecycleBin.tsx) with Restore/Purge + purge confirm; api client + i18n (sq + en parity). Tests: recycle-bin.test.ts (9 unit) + recycle-bin-routes.test.ts (4 integration: delete -> can't-login -> restore -> login, purge, gating, 409 reuse). server 103/103; build+lint+test 19/19. Wiki: new concepts/soft-delete.md; local-jwt-auth + index + log updated. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
34 lines
1.3 KiB
TypeScript
34 lines
1.3 KiB
TypeScript
import { QueryClient } from "@tanstack/react-query";
|
|
|
|
// Single QueryClient for the app. TanStack Query owns SERVER state (fetch, cache,
|
|
// refetch, loading/error) — wrapping the existing thin api.ts fetchers. Client/UI
|
|
// state (live feed, WS status) lives in Zustand, not here. The WS layer invalidates
|
|
// these caches on live events so Query stays the source of truth for server data.
|
|
//
|
|
// Defaults tuned for a single-appliance booth: no window-focus refetch (it's a
|
|
// kiosk, not a tab someone switches to), and a short staleTime since the WS is the
|
|
// real freshness mechanism — queries are the fallback/initial load.
|
|
export const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
refetchOnWindowFocus: false,
|
|
staleTime: 5_000,
|
|
retry: 1,
|
|
},
|
|
},
|
|
});
|
|
|
|
/** Stable query keys — referenced by both the screens and the WS invalidator. */
|
|
export const qk = {
|
|
me: ["me"] as const,
|
|
occupancy: ["occupancy"] as const,
|
|
events: ["events"] as const,
|
|
activeSessions: ["active-sessions"] as const,
|
|
siteConfig: ["site-config"] as const,
|
|
shift: ["shift"] as const,
|
|
deviceStatus: ["device-status"] as const,
|
|
report: (from: string, to: string, bucket: string) =>
|
|
["report", from, to, bucket] as const,
|
|
recycleBin: ["recycle-bin"] as const,
|
|
} as const;
|