App.tsx 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. import { Component, type ReactNode, type ErrorInfo } from 'react';
  2. import { BrowserRouter, Routes, Route, Navigate } 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 { ProfilesPage } from './pages/ProfilesPage';
  11. import { MaintenancePage } from './pages/MaintenancePage';
  12. import { ProjectsPage } from './pages/ProjectsPage';
  13. import { ProjectDetailPage } from './pages/ProjectDetailPage';
  14. import { FileManagerPage } from './pages/FileManagerPage';
  15. import { CameraPage } from './pages/CameraPage';
  16. import { StreamOverlayPage } from './pages/StreamOverlayPage';
  17. import { ExternalLinkPage } from './pages/ExternalLinkPage';
  18. import { GroupEditPage } from './pages/GroupEditPage';
  19. import InventoryPage from './pages/InventoryPage';
  20. import { SystemInfoPage } from './pages/SystemInfoPage';
  21. import { LoginPage } from './pages/LoginPage';
  22. import { SetupPage } from './pages/SetupPage';
  23. import { NotificationsPage } from './pages/NotificationsPage';
  24. import { useWebSocket } from './hooks/useWebSocket';
  25. import { useStreamTokenSync } from './hooks/useCameraStreamToken';
  26. import { ThemeProvider } from './contexts/ThemeContext';
  27. import { ToastProvider } from './contexts/ToastContext';
  28. import { AuthProvider, useAuth } from './contexts/AuthContext';
  29. import { SpoolBuddyLayout } from './components/spoolbuddy/SpoolBuddyLayout';
  30. import { SpoolBuddyDashboard } from './pages/spoolbuddy/SpoolBuddyDashboard';
  31. import { SpoolBuddyAmsPage } from './pages/spoolbuddy/SpoolBuddyAmsPage';
  32. import { SpoolBuddySettingsPage } from './pages/spoolbuddy/SpoolBuddySettingsPage';
  33. import { SpoolBuddyCalibrationPage } from './pages/spoolbuddy/SpoolBuddyCalibrationPage';
  34. import { SpoolBuddyWriteTagPage } from './pages/spoolbuddy/SpoolBuddyWriteTagPage';
  35. import { SpoolBuddyInventoryPage } from './pages/spoolbuddy/SpoolBuddyInventoryPage';
  36. class ErrorBoundary extends Component<{ children: ReactNode }, { error: Error | null; errorInfo: ErrorInfo | null }> {
  37. state = { error: null as Error | null, errorInfo: null as ErrorInfo | null };
  38. static getDerivedStateFromError(error: Error) {
  39. return { error };
  40. }
  41. componentDidCatch(error: Error, errorInfo: ErrorInfo) {
  42. this.setState({ errorInfo });
  43. console.error('React crash:', error, errorInfo);
  44. }
  45. render() {
  46. if (this.state.error) {
  47. return (
  48. <div style={{ padding: 24, color: '#ef4444', backgroundColor: '#18181b', minHeight: '100vh', fontFamily: 'monospace' }}>
  49. <h1 style={{ fontSize: 20, marginBottom: 12 }}>UI Crash</h1>
  50. <pre style={{ whiteSpace: 'pre-wrap', fontSize: 14 }}>{this.state.error.message}</pre>
  51. <pre style={{ whiteSpace: 'pre-wrap', fontSize: 12, color: '#a1a1aa', marginTop: 12 }}>
  52. {this.state.error.stack}
  53. </pre>
  54. <button
  55. onClick={() => { this.setState({ error: null, errorInfo: null }); }}
  56. style={{ marginTop: 16, padding: '8px 16px', backgroundColor: '#3b82f6', color: '#fff', border: 'none', borderRadius: 8, cursor: 'pointer' }}
  57. >
  58. Retry
  59. </button>
  60. </div>
  61. );
  62. }
  63. return this.props.children;
  64. }
  65. }
  66. const queryClient = new QueryClient({
  67. defaultOptions: {
  68. queries: {
  69. staleTime: 1000 * 60,
  70. retry: 1,
  71. },
  72. },
  73. });
  74. function StreamTokenSync() {
  75. useStreamTokenSync();
  76. return null;
  77. }
  78. function WebSocketProvider({ children }: { children: React.ReactNode }) {
  79. useWebSocket();
  80. return <>{children}</>;
  81. }
  82. function ProtectedRoute({ children }: { children: React.ReactNode }) {
  83. const { authEnabled, loading, user } = useAuth();
  84. if (loading) {
  85. return <div className="min-h-screen flex items-center justify-center">Loading...</div>;
  86. }
  87. if (authEnabled && !user) {
  88. return <Navigate to="/login" replace />;
  89. }
  90. return <>{children}</>;
  91. }
  92. function AdminRoute({ children }: { children: React.ReactNode }) {
  93. const { authEnabled, loading, user, isAdmin } = useAuth();
  94. if (loading) {
  95. return <div className="min-h-screen flex items-center justify-center">Loading...</div>;
  96. }
  97. // If auth is not enabled, allow access (backward compatibility)
  98. if (!authEnabled) {
  99. return <>{children}</>;
  100. }
  101. // If auth is enabled but no user, redirect to login
  102. if (!user) {
  103. return <Navigate to="/login" replace />;
  104. }
  105. // If user is not admin, redirect to home
  106. if (!isAdmin) {
  107. return <Navigate to="/" replace />;
  108. }
  109. return <>{children}</>;
  110. }
  111. function SetupRoute({ children }: { children: React.ReactNode }) {
  112. const { authEnabled, loading } = useAuth();
  113. if (loading) {
  114. return <div className="min-h-screen flex items-center justify-center">Loading...</div>;
  115. }
  116. // If auth is already enabled, redirect to login
  117. // Otherwise, allow access to setup page (even if setup was completed before)
  118. // This allows users to enable auth later if they skipped it during initial setup
  119. if (authEnabled) {
  120. return <Navigate to="/login" replace />;
  121. }
  122. return <>{children}</>;
  123. }
  124. function App() {
  125. return (
  126. <ErrorBoundary>
  127. <ThemeProvider>
  128. <ToastProvider>
  129. <QueryClientProvider client={queryClient}>
  130. <AuthProvider>
  131. <StreamTokenSync />
  132. <BrowserRouter>
  133. <Routes>
  134. {/* Setup page - only accessible if auth not enabled */}
  135. <Route path="/setup" element={<SetupRoute><SetupPage /></SetupRoute>} />
  136. {/* Login page */}
  137. <Route path="/login" element={<LoginPage />} />
  138. {/* Camera page - standalone, no layout, no WebSocket (doesn't need real-time updates) */}
  139. <Route path="/camera/:printerId" element={<CameraPage />} />
  140. {/* Stream overlay page - standalone for OBS/streaming embeds, no auth required */}
  141. <Route path="/overlay/:printerId" element={<StreamOverlayPage />} />
  142. {/* SpoolBuddy kiosk UI */}
  143. <Route element={<ProtectedRoute><WebSocketProvider><SpoolBuddyLayout /></WebSocketProvider></ProtectedRoute>}>
  144. <Route path="spoolbuddy" element={<SpoolBuddyDashboard />} />
  145. <Route path="spoolbuddy/ams" element={<SpoolBuddyAmsPage />} />
  146. <Route path="spoolbuddy/write-tag" element={<SpoolBuddyWriteTagPage />} />
  147. <Route path="spoolbuddy/inventory" element={<SpoolBuddyInventoryPage />} />
  148. <Route path="spoolbuddy/settings" element={<SpoolBuddySettingsPage />} />
  149. <Route path="spoolbuddy/calibration" element={<SpoolBuddyCalibrationPage />} />
  150. </Route>
  151. {/* Main app with WebSocket for real-time updates */}
  152. <Route element={<ProtectedRoute><WebSocketProvider><Layout /></WebSocketProvider></ProtectedRoute>}>
  153. <Route index element={<PrintersPage />} />
  154. <Route path="archives" element={<ArchivesPage />} />
  155. <Route path="queue" element={<QueuePage />} />
  156. <Route path="stats" element={<StatsPage />} />
  157. <Route path="profiles" element={<ProfilesPage />} />
  158. <Route path="maintenance" element={<MaintenancePage />} />
  159. <Route path="projects" element={<ProjectsPage />} />
  160. <Route path="projects/:id" element={<ProjectDetailPage />} />
  161. <Route path="inventory" element={<InventoryPage />} />
  162. <Route path="files" element={<FileManagerPage />} />
  163. <Route path="settings" element={<AdminRoute><SettingsPage /></AdminRoute>} />
  164. <Route path="groups/new" element={<AdminRoute><GroupEditPage /></AdminRoute>} />
  165. <Route path="groups/:id/edit" element={<AdminRoute><GroupEditPage /></AdminRoute>} />
  166. <Route path="users" element={<Navigate to="/settings?tab=users" replace />} />
  167. <Route path="groups" element={<Navigate to="/settings?tab=users" replace />} />
  168. <Route path="system" element={<SystemInfoPage />} />
  169. <Route path="notifications" element={<NotificationsPage />} />
  170. <Route path="external/:id" element={<ExternalLinkPage />} />
  171. </Route>
  172. </Routes>
  173. </BrowserRouter>
  174. </AuthProvider>
  175. </QueryClientProvider>
  176. </ToastProvider>
  177. </ThemeProvider>
  178. </ErrorBoundary>
  179. );
  180. }
  181. export default App;