App.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. import { Component, type ReactNode, type ErrorInfo } from 'react';
  2. import { BrowserRouter, Routes, Route, Navigate, useLocation } from 'react-router-dom';
  3. import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
  4. import { Layout } from './components/Layout';
  5. import { PrintersPage } from './pages/PrintersPage';
  6. import { ArchivesPage } from './pages/ArchivesPage';
  7. import { QueuePage } from './pages/QueuePage';
  8. import { StatsPage } from './pages/StatsPage';
  9. import { SettingsPage } from './pages/SettingsPage';
  10. import { FinancePage } from './pages/FinancePage';
  11. import { ProfilesPage } from './pages/ProfilesPage';
  12. import { MaintenancePage } from './pages/MaintenancePage';
  13. import { ProjectsPage } from './pages/ProjectsPage';
  14. import { ProjectDetailPage } from './pages/ProjectDetailPage';
  15. import { FileManagerPage } from './pages/FileManagerPage';
  16. import { LibraryTrashPage } from './pages/LibraryTrashPage';
  17. import { CameraPage } from './pages/CameraPage';
  18. import { CamWallPage } from './pages/CamWallPage';
  19. import { StreamOverlayPage } from './pages/StreamOverlayPage';
  20. import { ExternalLinkPage } from './pages/ExternalLinkPage';
  21. import { GroupEditPage } from './pages/GroupEditPage';
  22. import { PrinterLocationsPage } from './pages/PrinterLocationsPage';
  23. import InventoryPage from './pages/InventoryPage';
  24. import { MakerworldPage } from './pages/MakerworldPage';
  25. import { SystemInfoPage } from './pages/SystemInfoPage';
  26. import { LoginPage } from './pages/LoginPage';
  27. import { ConnectAuthorizePage } from './pages/ConnectAuthorizePage';
  28. import { SetupPage } from './pages/SetupPage';
  29. import { NotificationsPage } from './pages/NotificationsPage';
  30. import { GCodeViewerPage } from './pages/GCodeViewerPage';
  31. import { useWebSocket } from './hooks/useWebSocket';
  32. import { usePrintProgressTitle } from './hooks/usePrintProgressTitle';
  33. import { useStreamTokenSync } from './hooks/useCameraStreamToken';
  34. import { ThemeProvider } from './contexts/ThemeContext';
  35. import { ToastProvider } from './contexts/ToastContext';
  36. import { SliceJobTrackerProvider } from './contexts/SliceJobTrackerContext';
  37. import { AuthProvider, useAuth } from './contexts/AuthContext';
  38. import { ColorCatalogProvider } from './contexts/ColorCatalogContext';
  39. import { SpoolBuddyLayout } from './components/spoolbuddy/SpoolBuddyLayout';
  40. import { SpoolBuddyDashboard } from './pages/spoolbuddy/SpoolBuddyDashboard';
  41. import { SpoolBuddyAmsPage } from './pages/spoolbuddy/SpoolBuddyAmsPage';
  42. import { SpoolBuddySettingsPage } from './pages/spoolbuddy/SpoolBuddySettingsPage';
  43. import { SpoolBuddyCalibrationPage } from './pages/spoolbuddy/SpoolBuddyCalibrationPage';
  44. import { SpoolBuddyWriteTagPage } from './pages/spoolbuddy/SpoolBuddyWriteTagPage';
  45. import { SpoolBuddyInventoryPage } from './pages/spoolbuddy/SpoolBuddyInventoryPage';
  46. class ErrorBoundary extends Component<{ children: ReactNode }, { error: Error | null; errorInfo: ErrorInfo | null }> {
  47. state = { error: null as Error | null, errorInfo: null as ErrorInfo | null };
  48. static getDerivedStateFromError(error: Error) {
  49. return { error };
  50. }
  51. componentDidCatch(error: Error, errorInfo: ErrorInfo) {
  52. this.setState({ errorInfo });
  53. console.error('React crash:', error, errorInfo);
  54. }
  55. render() {
  56. if (this.state.error) {
  57. return (
  58. <div style={{ padding: 24, color: '#ef4444', backgroundColor: '#18181b', minHeight: '100vh', fontFamily: 'monospace' }}>
  59. <h1 style={{ fontSize: 20, marginBottom: 12 }}>UI Crash</h1>
  60. <pre style={{ whiteSpace: 'pre-wrap', fontSize: 14 }}>{this.state.error.message}</pre>
  61. <pre style={{ whiteSpace: 'pre-wrap', fontSize: 12, color: '#a1a1aa', marginTop: 12 }}>
  62. {this.state.error.stack}
  63. </pre>
  64. <button
  65. onClick={() => { this.setState({ error: null, errorInfo: null }); }}
  66. style={{ marginTop: 16, padding: '8px 16px', backgroundColor: '#3b82f6', color: '#fff', border: 'none', borderRadius: 8, cursor: 'pointer' }}
  67. >
  68. Retry
  69. </button>
  70. </div>
  71. );
  72. }
  73. return this.props.children;
  74. }
  75. }
  76. const queryClient = new QueryClient({
  77. defaultOptions: {
  78. queries: {
  79. staleTime: 1000 * 60,
  80. retry: 1,
  81. },
  82. },
  83. });
  84. function StreamTokenSync() {
  85. useStreamTokenSync();
  86. return null;
  87. }
  88. function WebSocketProvider({ children }: { children: React.ReactNode }) {
  89. useWebSocket();
  90. usePrintProgressTitle();
  91. return <>{children}</>;
  92. }
  93. function ProtectedRoute({ children }: { children: React.ReactNode }) {
  94. const { authEnabled, loading, user } = useAuth();
  95. const location = useLocation();
  96. if (loading) {
  97. return <div className="min-h-screen flex items-center justify-center">Loading...</div>;
  98. }
  99. if (authEnabled && !user) {
  100. return <Navigate to="/login" replace state={{ from: location }} />;
  101. }
  102. return <>{children}</>;
  103. }
  104. function PermissionRoute({ permission, children }: { permission: string; children: React.ReactNode }) {
  105. // Permission-gated route: any user with the given permission can enter, not
  106. // just admins. Individual components below this guard apply their own
  107. // per-action permission checks. Used for pages where delegation is supported
  108. // (e.g. settings:read grants read-only access to Settings; specific tabs
  109. // require their own permissions like users:read, groups:update, etc.).
  110. const { authEnabled, loading, user, hasPermission } = useAuth();
  111. const location = useLocation();
  112. if (loading) {
  113. return <div className="min-h-screen flex items-center justify-center">Loading...</div>;
  114. }
  115. // Auth disabled → open access (backward compatibility)
  116. if (!authEnabled) {
  117. return <>{children}</>;
  118. }
  119. if (!user) {
  120. return <Navigate to="/login" replace state={{ from: location }} />;
  121. }
  122. if (!hasPermission(permission as Parameters<typeof hasPermission>[0])) {
  123. return <Navigate to="/" replace />;
  124. }
  125. return <>{children}</>;
  126. }
  127. function SetupRoute({ children }: { children: React.ReactNode }) {
  128. const { authEnabled, loading } = useAuth();
  129. if (loading) {
  130. return <div className="min-h-screen flex items-center justify-center">Loading...</div>;
  131. }
  132. // If auth is already enabled, redirect to login
  133. // Otherwise, allow access to setup page (even if setup was completed before)
  134. // This allows users to enable auth later if they skipped it during initial setup
  135. if (authEnabled) {
  136. return <Navigate to="/login" replace />;
  137. }
  138. return <>{children}</>;
  139. }
  140. function App() {
  141. return (
  142. <ErrorBoundary>
  143. <ToastProvider>
  144. <QueryClientProvider client={queryClient}>
  145. <AuthProvider>
  146. {/* ThemeProvider sits inside AuthProvider so its initial
  147. ``api.getSettings()`` fetch can wait for AuthContext to
  148. resolve — otherwise it fires unconditionally on every
  149. login page load and returns 401. ErrorBoundary uses
  150. inline styles, so a missing theme on a crash screen is
  151. not a regression. */}
  152. <ThemeProvider>
  153. <ColorCatalogProvider>
  154. <SliceJobTrackerProvider>
  155. <StreamTokenSync />
  156. <BrowserRouter>
  157. <Routes>
  158. {/* Setup page - only accessible if auth not enabled */}
  159. <Route path="/setup" element={<SetupRoute><SetupPage /></SetupRoute>} />
  160. {/* Login page */}
  161. <Route path="/login" element={<LoginPage />} />
  162. {/* "Sign in with Bambuddy" for connected apps: standalone, no layout,
  163. so it also fits inside the sidebar iframe of the app asking. */}
  164. <Route path="/connect/authorize" element={<ProtectedRoute><ConnectAuthorizePage /></ProtectedRoute>} />
  165. {/* Camera page - standalone, no layout, no WebSocket (doesn't need real-time updates) */}
  166. <Route path="/camera/:printerId" element={<CameraPage />} />
  167. {/* Stream overlay page - standalone for OBS/streaming embeds, no auth required */}
  168. <Route path="/overlay/:printerId" element={<StreamOverlayPage />} />
  169. {/* Cam Wall on its own URL (#2531). Outside ProtectedRoute because a
  170. ?token= kiosk has no session to protect; the page itself sends a
  171. tokenless visitor to /login, and the backend gates the feed. */}
  172. <Route path="/camwall" element={<CamWallPage />} />
  173. {/* SpoolBuddy kiosk UI */}
  174. <Route element={<ProtectedRoute><WebSocketProvider><SpoolBuddyLayout /></WebSocketProvider></ProtectedRoute>}>
  175. <Route path="spoolbuddy" element={<SpoolBuddyDashboard />} />
  176. <Route path="spoolbuddy/ams" element={<SpoolBuddyAmsPage />} />
  177. <Route path="spoolbuddy/write-tag" element={<SpoolBuddyWriteTagPage />} />
  178. <Route path="spoolbuddy/inventory" element={<SpoolBuddyInventoryPage />} />
  179. <Route path="spoolbuddy/settings" element={<SpoolBuddySettingsPage />} />
  180. <Route path="spoolbuddy/calibration" element={<SpoolBuddyCalibrationPage />} />
  181. </Route>
  182. {/* Main app with WebSocket for real-time updates */}
  183. <Route element={<ProtectedRoute><WebSocketProvider><Layout /></WebSocketProvider></ProtectedRoute>}>
  184. <Route index element={<PrintersPage />} />
  185. <Route path="archives" element={<ArchivesPage />} />
  186. <Route path="queue" element={<QueuePage />} />
  187. {/* Slicer Pipelines (#1425) — Pipelines tab lives on the
  188. Print Queue page (Queue + History + Timeline +
  189. Pipelines). Old standalone URL redirects. */}
  190. <Route path="pipelines/runs" element={<Navigate to="/queue?tab=pipelines" replace />} />
  191. <Route path="stats" element={<StatsPage />} />
  192. <Route path="profiles" element={<ProfilesPage />} />
  193. <Route path="finance" element={<PermissionRoute permission="cost_centers:read_own"><FinancePage /></PermissionRoute>} />
  194. <Route path="maintenance" element={<MaintenancePage />} />
  195. <Route path="projects" element={<ProjectsPage />} />
  196. <Route path="projects/:id" element={<ProjectDetailPage />} />
  197. <Route path="inventory" element={<InventoryPage />} />
  198. <Route path="files" element={<FileManagerPage />} />
  199. <Route path="files/trash" element={<LibraryTrashPage />} />
  200. <Route path="makerworld" element={<PermissionRoute permission="makerworld:view"><MakerworldPage /></PermissionRoute>} />
  201. <Route path="settings" element={<PermissionRoute permission="settings:read"><SettingsPage /></PermissionRoute>} />
  202. <Route path="groups/new" element={<PermissionRoute permission="groups:create"><GroupEditPage /></PermissionRoute>} />
  203. <Route path="groups/:id/edit" element={<PermissionRoute permission="groups:update"><GroupEditPage /></PermissionRoute>} />
  204. <Route path="printer-locations" element={<PermissionRoute permission="printers:read"><PrinterLocationsPage /></PermissionRoute>} />
  205. <Route path="users" element={<Navigate to="/settings?tab=users" replace />} />
  206. <Route path="groups" element={<Navigate to="/settings?tab=users" replace />} />
  207. <Route path="system" element={<SystemInfoPage />} />
  208. <Route path="notifications" element={<NotificationsPage />} />
  209. <Route path="gcode-viewer" element={<GCodeViewerPage />} />
  210. <Route path="external/:id" element={<ExternalLinkPage />} />
  211. <Route path="camera-tokens" element={<Navigate to="/settings?tab=apikeys#card-camera-tokens" replace />} />
  212. </Route>
  213. </Routes>
  214. </BrowserRouter>
  215. </SliceJobTrackerProvider>
  216. </ColorCatalogProvider>
  217. </ThemeProvider>
  218. </AuthProvider>
  219. </QueryClientProvider>
  220. </ToastProvider>
  221. </ErrorBoundary>
  222. );
  223. }
  224. export default App;