import { Component, 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 { PrintersPage } from './pages/PrintersPage';
import { ArchivesPage } from './pages/ArchivesPage';
import { QueuePage } from './pages/QueuePage';
import { StatsPage } from './pages/StatsPage';
import { SettingsPage } from './pages/SettingsPage';
import { FinancePage } from './pages/FinancePage';
import { ProfilesPage } from './pages/ProfilesPage';
import { MaintenancePage } from './pages/MaintenancePage';
import { ProjectsPage } from './pages/ProjectsPage';
import { ProjectDetailPage } from './pages/ProjectDetailPage';
import { FileManagerPage } from './pages/FileManagerPage';
import { LibraryTrashPage } from './pages/LibraryTrashPage';
import { CameraPage } from './pages/CameraPage';
import { CamWallPage } from './pages/CamWallPage';
import { StreamOverlayPage } from './pages/StreamOverlayPage';
import { ExternalLinkPage } from './pages/ExternalLinkPage';
import { GroupEditPage } from './pages/GroupEditPage';
import InventoryPage from './pages/InventoryPage';
import { MakerworldPage } from './pages/MakerworldPage';
import { SystemInfoPage } from './pages/SystemInfoPage';
import { LoginPage } from './pages/LoginPage';
import { SetupPage } from './pages/SetupPage';
import { NotificationsPage } from './pages/NotificationsPage';
import { GCodeViewerPage } from './pages/GCodeViewerPage';
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 { SpoolBuddyDashboard } from './pages/spoolbuddy/SpoolBuddyDashboard';
import { SpoolBuddyAmsPage } from './pages/spoolbuddy/SpoolBuddyAmsPage';
import { SpoolBuddySettingsPage } from './pages/spoolbuddy/SpoolBuddySettingsPage';
import { SpoolBuddyCalibrationPage } from './pages/spoolbuddy/SpoolBuddyCalibrationPage';
import { SpoolBuddyWriteTagPage } from './pages/spoolbuddy/SpoolBuddyWriteTagPage';
import { SpoolBuddyInventoryPage } from './pages/spoolbuddy/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; 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 ;
}
if (!hasPermission(permission as Parameters[0])) {
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. */}
{/* Setup page - only accessible if auth not enabled */}
} />
{/* Login page */}
} />
{/* 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 */}
}>
} />
} />
} />
} />
} />
} />
{/* 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. */}
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
} />
);
}
export default App;