import { Component, Suspense, type ReactNode, type ErrorInfo } from 'react'; import { BrowserRouter, Routes, Route, Navigate, useLocation } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { Layout } from './components/Layout'; import { useWebSocket } from './hooks/useWebSocket'; import { usePrintProgressTitle } from './hooks/usePrintProgressTitle'; import { useStreamTokenSync } from './hooks/useCameraStreamToken'; import { ThemeProvider } from './contexts/ThemeContext'; import { ToastProvider } from './contexts/ToastContext'; import { SliceJobTrackerProvider } from './contexts/SliceJobTrackerContext'; import { AuthProvider, useAuth } from './contexts/AuthContext'; import { ColorCatalogProvider } from './contexts/ColorCatalogContext'; import { SpoolBuddyLayout } from './components/spoolbuddy/SpoolBuddyLayout'; import { PageLoading } from './components/PageLoading'; import { lazyPage } from './utils/lazyPage'; // Every page loads as its own file when first opened (#3175). const PrintersPage = lazyPage(() => import('./pages/PrintersPage'), 'PrintersPage'); const ArchivesPage = lazyPage(() => import('./pages/ArchivesPage'), 'ArchivesPage'); const QueuePage = lazyPage(() => import('./pages/QueuePage'), 'QueuePage'); const StatsPage = lazyPage(() => import('./pages/StatsPage'), 'StatsPage'); const SettingsPage = lazyPage(() => import('./pages/SettingsPage'), 'SettingsPage'); const FinancePage = lazyPage(() => import('./pages/FinancePage'), 'FinancePage'); const ProfilesPage = lazyPage(() => import('./pages/ProfilesPage'), 'ProfilesPage'); const MaintenancePage = lazyPage(() => import('./pages/MaintenancePage'), 'MaintenancePage'); const ProjectsPage = lazyPage(() => import('./pages/ProjectsPage'), 'ProjectsPage'); const ProjectDetailPage = lazyPage(() => import('./pages/ProjectDetailPage'), 'ProjectDetailPage'); const FileManagerPage = lazyPage(() => import('./pages/FileManagerPage'), 'FileManagerPage'); const LibraryTrashPage = lazyPage(() => import('./pages/LibraryTrashPage'), 'LibraryTrashPage'); const CameraPage = lazyPage(() => import('./pages/CameraPage'), 'CameraPage'); const CamWallPage = lazyPage(() => import('./pages/CamWallPage'), 'CamWallPage'); const StreamOverlayPage = lazyPage(() => import('./pages/StreamOverlayPage'), 'StreamOverlayPage'); const ExternalLinkPage = lazyPage(() => import('./pages/ExternalLinkPage'), 'ExternalLinkPage'); const GroupEditPage = lazyPage(() => import('./pages/GroupEditPage'), 'GroupEditPage'); const PrinterLocationsPage = lazyPage(() => import('./pages/PrinterLocationsPage'), 'PrinterLocationsPage'); const InventoryPage = lazyPage(() => import('./pages/InventoryPage'), 'default'); const ModelSourcesPage = lazyPage(() => import('./pages/ModelSourcesPage'), 'ModelSourcesPage'); const SystemInfoPage = lazyPage(() => import('./pages/SystemInfoPage'), 'SystemInfoPage'); const LoginPage = lazyPage(() => import('./pages/LoginPage'), 'LoginPage'); const ConnectAuthorizePage = lazyPage(() => import('./pages/ConnectAuthorizePage'), 'ConnectAuthorizePage'); const SetupPage = lazyPage(() => import('./pages/SetupPage'), 'SetupPage'); const NotificationsPage = lazyPage(() => import('./pages/NotificationsPage'), 'NotificationsPage'); const GCodeViewerPage = lazyPage(() => import('./pages/GCodeViewerPage'), 'GCodeViewerPage'); const SpoolBuddyDashboard = lazyPage(() => import('./pages/spoolbuddy/SpoolBuddyDashboard'), 'SpoolBuddyDashboard'); const SpoolBuddyAmsPage = lazyPage(() => import('./pages/spoolbuddy/SpoolBuddyAmsPage'), 'SpoolBuddyAmsPage'); const SpoolBuddySettingsPage = lazyPage(() => import('./pages/spoolbuddy/SpoolBuddySettingsPage'), 'SpoolBuddySettingsPage'); const SpoolBuddyWriteTagPage = lazyPage(() => import('./pages/spoolbuddy/SpoolBuddyWriteTagPage'), 'SpoolBuddyWriteTagPage'); const SpoolBuddyInventoryPage = lazyPage(() => import('./pages/spoolbuddy/SpoolBuddyInventoryPage'), 'SpoolBuddyInventoryPage'); class ErrorBoundary extends Component<{ children: ReactNode }, { error: Error | null; errorInfo: ErrorInfo | null }> { state = { error: null as Error | null, errorInfo: null as ErrorInfo | null }; static getDerivedStateFromError(error: Error) { return { error }; } componentDidCatch(error: Error, errorInfo: ErrorInfo) { this.setState({ errorInfo }); console.error('React crash:', error, errorInfo); } render() { if (this.state.error) { return (

UI Crash

{this.state.error.message}
            {this.state.error.stack}
          
); } return this.props.children; } } const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60, retry: 1, }, }, }); function StreamTokenSync() { useStreamTokenSync(); return null; } function WebSocketProvider({ children }: { children: React.ReactNode }) { useWebSocket(); usePrintProgressTitle(); return <>{children}; } function ProtectedRoute({ children }: { children: React.ReactNode }) { const { authEnabled, loading, user } = useAuth(); const location = useLocation(); if (loading) { return
Loading...
; } if (authEnabled && !user) { return ; } return <>{children}; } function PermissionRoute({ permission, children }: { permission: string | string[]; children: React.ReactNode }) { // Permission-gated route: any user with the given permission can enter, not // just admins. Individual components below this guard apply their own // per-action permission checks. Used for pages where delegation is supported // (e.g. settings:read grants read-only access to Settings; specific tabs // require their own permissions like users:read, groups:update, etc.). const { authEnabled, loading, user, hasPermission } = useAuth(); const location = useLocation(); if (loading) { return
Loading...
; } // Auth disabled → open access (backward compatibility) if (!authEnabled) { return <>{children}; } if (!user) { return ; } // A list lets in anyone holding at least one of the permissions. const required = (Array.isArray(permission) ? permission : [permission]) as Parameters[0][]; if (!required.some((p) => hasPermission(p))) { return ; } return <>{children}; } function SetupRoute({ children }: { children: React.ReactNode }) { const { authEnabled, loading } = useAuth(); if (loading) { return
Loading...
; } // If auth is already enabled, redirect to login // Otherwise, allow access to setup page (even if setup was completed before) // This allows users to enable auth later if they skipped it during initial setup if (authEnabled) { return ; } return <>{children}; } function App() { return ( {/* ThemeProvider sits inside AuthProvider so its initial ``api.getSettings()`` fetch can wait for AuthContext to resolve — otherwise it fires unconditionally on every login page load and returns 401. ErrorBoundary uses inline styles, so a missing theme on a crash screen is not a regression. */} {/* Standalone pages (login, camera, overlay, ...) wait here; pages inside a layout wait in that layout, so its frame stays up. */} }> {/* Setup page - only accessible if auth not enabled */} } /> {/* Login page */} } /> {/* "Sign in with Bambuddy" for connected apps: standalone, no layout, so it also fits inside the sidebar iframe of the app asking. */} } /> {/* Camera page - standalone, no layout, no WebSocket (doesn't need real-time updates) */} } /> {/* Stream overlay page - standalone for OBS/streaming embeds, no auth required */} } /> {/* Cam Wall on its own URL (#2531). Outside ProtectedRoute because a ?token= kiosk has no session to protect; the page itself sends a tokenless visitor to /login, and the backend gates the feed. */} } /> {/* SpoolBuddy kiosk UI */} }> } /> } /> } /> } /> } /> {/* Calibration moved into the settings page; old bookmarks land there. */} } /> {/* Main app with WebSocket for real-time updates */} }> } /> } /> } /> {/* Slicer Pipelines (#1425) — Pipelines tab lives on the Print Queue page (Queue + History + Timeline + Pipelines). Old standalone URL redirects. */} } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> {/* The page was MakerWorld-only until Manyfold joined it (#1471); old links and bookmarks still land on that tab. */} } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> ); } export default App;