feat(app): detect new deploy and prompt refresh (+ lazy-chunk recovery)
When a new version is deployed, open tabs no longer silently run stale code until a manual F5. - Server stamps every response with X-App-Version (src/middleware/version.ts, onSend hook in server.ts). - Client bakes its build version via Vite define (__APP_VERSION__); apiFetch compares the header to it (no polling — piggybacks existing traffic) and latches a mismatch in a small store (appVersion.ts). - UpdateBanner: a persistent bottom Snackbar "Je k dispozici nová verze aplikace. — Obnovit" → location.reload(). User-initiated, so an in-progress form is never interrupted. - lazyWithReload: every React.lazy page reloads once if its hashed chunk 404s after a deploy (sessionStorage-guarded against loops), fixing the "old chunk gone" error screen. Approach chosen after researching current SPA practice (version header + banner beats polling / a service worker for a non-PWA admin app). Tests: +4 (server header, client store latch/notify). Full suite 669 pass, tsc -b clean, lint 0. Verified live via Playwright (header emitted; banner appears on a simulated mismatch). Detection applies to deploys from the NEXT release onward (current clients lack the detection code). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,65 +7,97 @@ import { queryClient } from "./lib/queryClient";
|
||||
import ErrorBoundary from "./components/ErrorBoundary";
|
||||
import AppShell from "./ui/AppShell";
|
||||
import AlertContainer from "./components/AlertContainer";
|
||||
import UpdateBanner from "./components/UpdateBanner";
|
||||
import { lazyWithReload } from "./utils/lazyWithReload";
|
||||
import MuiProvider from "./ui/MuiProvider";
|
||||
import { LoadingState } from "./ui";
|
||||
import Login from "./pages/Login";
|
||||
import Dashboard from "./pages/Dashboard";
|
||||
|
||||
const Odin = lazy(() => import("./pages/Odin"));
|
||||
const Users = lazy(() => import("./pages/Users"));
|
||||
const Attendance = lazy(() => import("./pages/Attendance"));
|
||||
const AttendanceHistory = lazy(() => import("./pages/AttendanceHistory"));
|
||||
const AttendanceAdmin = lazy(() => import("./pages/AttendanceAdmin"));
|
||||
const AttendanceBalances = lazy(() => import("./pages/AttendanceBalances"));
|
||||
const AttendanceCreate = lazy(() => import("./pages/AttendanceCreate"));
|
||||
const LeaveRequests = lazy(() => import("./pages/LeaveRequests"));
|
||||
const LeaveApproval = lazy(() => import("./pages/LeaveApproval"));
|
||||
const AttendanceLocation = lazy(() => import("./pages/AttendanceLocation"));
|
||||
const PlanWork = lazy(() => import("./pages/PlanWork"));
|
||||
const Trips = lazy(() => import("./pages/Trips"));
|
||||
const TripsHistory = lazy(() => import("./pages/TripsHistory"));
|
||||
const TripsAdmin = lazy(() => import("./pages/TripsAdmin"));
|
||||
const Vehicles = lazy(() => import("./pages/Vehicles"));
|
||||
const Offers = lazy(() => import("./pages/Offers"));
|
||||
const OfferDetail = lazy(() => import("./pages/OfferDetail"));
|
||||
const OffersCustomers = lazy(() => import("./pages/OffersCustomers"));
|
||||
const OffersTemplates = lazy(() => import("./pages/OffersTemplates"));
|
||||
const Orders = lazy(() => import("./pages/Orders"));
|
||||
const OrderDetail = lazy(() => import("./pages/OrderDetail"));
|
||||
const IssuedOrderDetail = lazy(() => import("./pages/IssuedOrderDetail"));
|
||||
const Projects = lazy(() => import("./pages/Projects"));
|
||||
const ProjectDetail = lazy(() => import("./pages/ProjectDetail"));
|
||||
const Invoices = lazy(() => import("./pages/Invoices"));
|
||||
const InvoiceDetail = lazy(() => import("./pages/InvoiceDetail"));
|
||||
const Settings = lazy(() => import("./pages/Settings"));
|
||||
const AuditLog = lazy(() => import("./pages/AuditLog"));
|
||||
const NotFound = lazy(() => import("./pages/NotFound"));
|
||||
const Warehouse = lazy(() => import("./pages/Warehouse"));
|
||||
const WarehouseItems = lazy(() => import("./pages/WarehouseItems"));
|
||||
const WarehouseItemDetail = lazy(() => import("./pages/WarehouseItemDetail"));
|
||||
const WarehouseReceipts = lazy(() => import("./pages/WarehouseReceipts"));
|
||||
const WarehouseReceiptDetail = lazy(
|
||||
const Odin = lazyWithReload(() => import("./pages/Odin"));
|
||||
const Users = lazyWithReload(() => import("./pages/Users"));
|
||||
const Attendance = lazyWithReload(() => import("./pages/Attendance"));
|
||||
const AttendanceHistory = lazyWithReload(
|
||||
() => import("./pages/AttendanceHistory"),
|
||||
);
|
||||
const AttendanceAdmin = lazyWithReload(() => import("./pages/AttendanceAdmin"));
|
||||
const AttendanceBalances = lazyWithReload(
|
||||
() => import("./pages/AttendanceBalances"),
|
||||
);
|
||||
const AttendanceCreate = lazyWithReload(
|
||||
() => import("./pages/AttendanceCreate"),
|
||||
);
|
||||
const LeaveRequests = lazyWithReload(() => import("./pages/LeaveRequests"));
|
||||
const LeaveApproval = lazyWithReload(() => import("./pages/LeaveApproval"));
|
||||
const AttendanceLocation = lazyWithReload(
|
||||
() => import("./pages/AttendanceLocation"),
|
||||
);
|
||||
const PlanWork = lazyWithReload(() => import("./pages/PlanWork"));
|
||||
const Trips = lazyWithReload(() => import("./pages/Trips"));
|
||||
const TripsHistory = lazyWithReload(() => import("./pages/TripsHistory"));
|
||||
const TripsAdmin = lazyWithReload(() => import("./pages/TripsAdmin"));
|
||||
const Vehicles = lazyWithReload(() => import("./pages/Vehicles"));
|
||||
const Offers = lazyWithReload(() => import("./pages/Offers"));
|
||||
const OfferDetail = lazyWithReload(() => import("./pages/OfferDetail"));
|
||||
const OffersCustomers = lazyWithReload(() => import("./pages/OffersCustomers"));
|
||||
const OffersTemplates = lazyWithReload(() => import("./pages/OffersTemplates"));
|
||||
const Orders = lazyWithReload(() => import("./pages/Orders"));
|
||||
const OrderDetail = lazyWithReload(() => import("./pages/OrderDetail"));
|
||||
const IssuedOrderDetail = lazyWithReload(
|
||||
() => import("./pages/IssuedOrderDetail"),
|
||||
);
|
||||
const Projects = lazyWithReload(() => import("./pages/Projects"));
|
||||
const ProjectDetail = lazyWithReload(() => import("./pages/ProjectDetail"));
|
||||
const Invoices = lazyWithReload(() => import("./pages/Invoices"));
|
||||
const InvoiceDetail = lazyWithReload(() => import("./pages/InvoiceDetail"));
|
||||
const Settings = lazyWithReload(() => import("./pages/Settings"));
|
||||
const AuditLog = lazyWithReload(() => import("./pages/AuditLog"));
|
||||
const NotFound = lazyWithReload(() => import("./pages/NotFound"));
|
||||
const Warehouse = lazyWithReload(() => import("./pages/Warehouse"));
|
||||
const WarehouseItems = lazyWithReload(() => import("./pages/WarehouseItems"));
|
||||
const WarehouseItemDetail = lazyWithReload(
|
||||
() => import("./pages/WarehouseItemDetail"),
|
||||
);
|
||||
const WarehouseReceipts = lazyWithReload(
|
||||
() => import("./pages/WarehouseReceipts"),
|
||||
);
|
||||
const WarehouseReceiptDetail = lazyWithReload(
|
||||
() => import("./pages/WarehouseReceiptDetail"),
|
||||
);
|
||||
const WarehouseReceiptForm = lazy(() => import("./pages/WarehouseReceiptForm"));
|
||||
const WarehouseIssues = lazy(() => import("./pages/WarehouseIssues"));
|
||||
const WarehouseIssueDetail = lazy(() => import("./pages/WarehouseIssueDetail"));
|
||||
const WarehouseIssueForm = lazy(() => import("./pages/WarehouseIssueForm"));
|
||||
const WarehouseReservations = lazy(
|
||||
const WarehouseReceiptForm = lazyWithReload(
|
||||
() => import("./pages/WarehouseReceiptForm"),
|
||||
);
|
||||
const WarehouseIssues = lazyWithReload(() => import("./pages/WarehouseIssues"));
|
||||
const WarehouseIssueDetail = lazyWithReload(
|
||||
() => import("./pages/WarehouseIssueDetail"),
|
||||
);
|
||||
const WarehouseIssueForm = lazyWithReload(
|
||||
() => import("./pages/WarehouseIssueForm"),
|
||||
);
|
||||
const WarehouseReservations = lazyWithReload(
|
||||
() => import("./pages/WarehouseReservations"),
|
||||
);
|
||||
const WarehouseInventory = lazy(() => import("./pages/WarehouseInventory"));
|
||||
const WarehouseInventoryDetail = lazy(
|
||||
const WarehouseInventory = lazyWithReload(
|
||||
() => import("./pages/WarehouseInventory"),
|
||||
);
|
||||
const WarehouseInventoryDetail = lazyWithReload(
|
||||
() => import("./pages/WarehouseInventoryDetail"),
|
||||
);
|
||||
const WarehouseInventoryForm = lazy(
|
||||
const WarehouseInventoryForm = lazyWithReload(
|
||||
() => import("./pages/WarehouseInventoryForm"),
|
||||
);
|
||||
const WarehouseReports = lazy(() => import("./pages/WarehouseReports"));
|
||||
const WarehouseSuppliers = lazy(() => import("./pages/WarehouseSuppliers"));
|
||||
const WarehouseLocations = lazy(() => import("./pages/WarehouseLocations"));
|
||||
const WarehouseCategories = lazy(() => import("./pages/WarehouseCategories"));
|
||||
const WarehouseReports = lazyWithReload(
|
||||
() => import("./pages/WarehouseReports"),
|
||||
);
|
||||
const WarehouseSuppliers = lazyWithReload(
|
||||
() => import("./pages/WarehouseSuppliers"),
|
||||
);
|
||||
const WarehouseLocations = lazyWithReload(
|
||||
() => import("./pages/WarehouseLocations"),
|
||||
);
|
||||
const WarehouseCategories = lazyWithReload(
|
||||
() => import("./pages/WarehouseCategories"),
|
||||
);
|
||||
const UiKit = import.meta.env.DEV ? lazy(() => import("./pages/UiKit")) : null;
|
||||
|
||||
export default function AdminApp() {
|
||||
@@ -75,6 +107,7 @@ export default function AdminApp() {
|
||||
<AlertProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AlertContainer />
|
||||
<UpdateBanner />
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={<LoadingState />}>
|
||||
<Routes>
|
||||
|
||||
Reference in New Issue
Block a user