# Officer.dev Frontend Architecture ## Executive Summary Officer's frontend is a modern React 19 SPA (Single Page Application) built with: - **React 19** with automatic compiler optimizations (no useCallback/useMemo needed) - **React Router 7** for client-side navigation - **React Query** for server state management - **Tailwind CSS 4** with custom components for styling - **Shadcn/ui** component library base - **WebSockets** for real-time terminal and AI chat - **Modular architecture** with shared component and hook libraries This document covers the frontend architecture, component patterns, state management, and development practices. --- ## Table of Contents 1. [Architecture Overview](#architecture-overview) 2. [Technology Stack](#technology-stack) 3. [Directory Structure](#directory-structure) 4. [Routing & Navigation](#routing--navigation) 5. [Component Architecture](#component-architecture) 6. [State Management](#state-management) 7. [API Communication](#api-communication) 8. [Real-Time Features](#real-time-features) 9. [Styling & Theming](#styling--theming) 10. [Performance & Optimization](#performance--optimization) 11. [Development Workflow](#development-workflow) --- ## Architecture Overview ### High-Level Architecture ``` ┌──────────────────────────────────────────────────────────┐ │ Browser Application │ ├──────────────────────────────────────────────────────────┤ │ │ │ App.tsx (Router Setup) │ │ ↓ │ │ BrowserRouter │ │ ├─ Authentication Routes (public) │ │ │ └─ Landing, SignIn, SignUp, ForgotPassword │ │ │ │ │ └─ Dashboard Routes (protected) │ │ ├─ HomeScreen │ │ ├─ SettingsPages (Profile, System, Resources) │ │ ├─ Automation │ │ ├─ SessionListPage (Chat with Pi) │ │ ├─ PlansScreen │ │ ├─ FilesScreen │ │ ├─ CodeEditorScreen │ │ ├─ SkillsScreen │ │ ├─ TasksScreen │ │ ├─ ProcessesScreen │ │ ├─ TaskLogsScreen │ │ ├─ WorkspacesScreen │ │ ├─ ProjectListScreen │ │ ├─ ProjectScreen │ │ └─ TerminalScreen │ │ │ │ ↓ Shared State/Context │ │ │ │ ├─ AuthContext (useAuth) │ │ ├─ GlobalState (useGlobalState) │ │ ├─ ServerSettings (useServerSettings) │ │ └─ ReactQuery (useQuery, useMutation) │ │ │ │ ↓ WebSocket Connections │ │ │ │ ├─ Terminal WebSocket │ │ │ └─ Interactive shell in xterm.js │ │ │ │ │ └─ Pi Chat WebSocket │ │ └─ Real-time streaming chat with Claude │ │ │ └──────────────────────────────────────────────────────────┘ ↓ HTTP/REST & WebSocket ┌────────────────┐ │ Officer API │ │ (port 5000) │ └────────────────┘ ``` ### Data Flow ``` User Interaction (Click, Type, etc.) ↓ Component Event Handler ↓ State Update (useState/Context/Query) ↓ API Call (HTTP or WebSocket) ↓ Server Processing ↓ Response ↓ Update Component State ↓ Re-render ↓ Updated UI ``` --- ## Technology Stack ### Core Frontend Framework - **React 19** - Latest with automatic compiler optimizations - **React DOM 19** - DOM rendering - **React Router 7** - Client-side routing (file-based patterns support) - **TypeScript 5.9** - Strict mode enabled ### State Management - **React Context** - Component tree data sharing - **React Query (TanStack)** - Server state, caching, synchronization - **Custom hooks** - Encapsulated business logic - **Zustand** - Optional lightweight state (if used) ### UI & Styling - **Tailwind CSS 4** - Utility-first CSS framework - **Shadcn/ui** - Accessible component library base - **Radix UI** - Headless components for accessibility - **Lucide React** - Icon library - **Tabler Icons** - Additional icons - **Class Variance Authority (CVA)** - Component style variations ### Real-Time Communication - **WebSocket API** - Native browser WebSocket - **Json RPC** - Message protocol over WebSocket ### Code Editor & Terminal - **Monaco Editor** (@monaco-editor/react) - VS Code-like editor - **xterm.js** (@xterm/xterm) - Terminal emulator - **@uiw/react-textarea-code-editor** - Simple code editing ### Data Visualization & Rich Content - **Recharts** - Chart library - **React Markdown** - Markdown rendering - **Shiki** - Syntax highlighting - **HTML2Canvas** - Screenshot capture - **React Three Fiber** - 3D rendering (if used) ### Form & Validation - **React Hook Form** - Form state management - **Zod** - Schema validation - **@hookform/resolvers** - Form validation resolvers ### UI Components & Utilities - **Sonner** - Toast notifications - **Vaul** - Drawer component - **React Resizable Panels** - Resizable layout panels - **React Virtual** - Virtual scrolling - **Input OTP** - OTP input component - **React Spinners** - Loading animations - **React CountUp** - Animated numbers ### Authentication - **@simplewebauthn/browser** - WebAuthn/passkey support - **@react-oauth/google** - Google OAuth integration - **JWT Decode** - Token decoding ### External Services - **Googleapis** - Google API integration - **Nodemailer** (backend) - Email sending ### Development & Build Tools - **Bun** - Runtime and package manager - **Vite** - Build tool and dev server (if used) - **Playwright** - E2E testing - **Testing Library** - Component testing utilities - **Happy DOM** - DOM testing --- ## Directory Structure ### Frontend Layout ``` src/ ├── apps/ │ └── officer-web/ # Main dashboard app │ ├── App.tsx # Root component & routing │ ├── frontend.tsx # Vite entry point (if applicable) │ ├── index.html # HTML template │ │ │ ├── Screens/ # Page-level components │ │ ├── Authentication/ # Auth screens (public) │ │ │ ├── LandingPage.tsx │ │ │ ├── VerifyScreen.tsx │ │ │ ├── SignInScreen.tsx │ │ │ ├── SignUpScreen.tsx │ │ │ ├── ForgotPassword.tsx │ │ │ ├── ResetPassword.tsx │ │ │ └── SignoutScreen.tsx │ │ │ │ │ └── Dashboard/ # Dashboard screens (protected) │ │ ├── HomeScreen.tsx │ │ ├── SettingsPages/ │ │ │ ├── ProfileSettings.tsx │ │ │ ├── SystemSettings.tsx │ │ │ └── ResourceSettings.tsx │ │ ├── Automation.tsx │ │ ├── SessionListPage.tsx (Pi Chat) │ │ ├── PlansScreen.tsx │ │ ├── FilesScreen.tsx │ │ ├── CodeEditorScreen.tsx │ │ ├── SkillsScreen.tsx │ │ ├── TasksScreen.tsx │ │ ├── ProcessesScreen.tsx │ │ ├── TaskLogsScreen.tsx │ │ ├── WorkspacesScreen.tsx │ │ ├── WorkspaceScreen.tsx │ │ ├── ProjectListScreen.tsx │ │ ├── ProjectScreen.tsx │ │ └── TerminalScreen.tsx │ │ │ ├── state/ # Component state │ │ ├── useAuth.ts # Authentication state │ │ ├── useInitialData.ts # Initial data loading │ │ └── useServerSettings.ts # Server configuration │ │ │ ├── lib/ # Client utilities │ │ ├── api.ts # API client │ │ └── websocket.ts # WebSocket utilities │ │ │ ├── styles/ # Global styles │ │ ├── global.css │ │ └── tailwind.css │ │ │ └── locales/ # Translations │ ├── en.json │ └── ... │ └── workspaces/ # Shared libraries ├── components/ # Reusable React components │ ├── ui/ # Basic UI components │ │ ├── button.tsx │ │ ├── input.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── select.tsx │ │ ├── tabs.tsx │ │ ├── card.tsx │ │ └── ...more │ │ │ ├── Complex/ # Feature components │ │ ├── DataTable/ │ │ ├── MarkdownEditor.tsx │ │ ├── ColorPicker.tsx │ │ ├── CommandBlock.tsx │ │ └── ... │ │ │ ├── Workspace/ # Workspace-specific │ ├── Dialogs/ # Dialog components │ ├── ErrorDialogs/ # Error handling UI │ ├── Logos/ # Logo variants │ └── package.json │ ├── hooks/ # Custom React hooks │ ├── useAuth.ts # Auth state hook │ ├── useClient.ts # API client hook │ ├── useQuery.ts # Data fetching │ ├── useMutation.ts # Data mutation │ └── ... │ ├── helpers/ # Utility functions │ ├── formatters.ts # Date, number formatting │ ├── validators.ts # Input validation │ ├── converters.ts # Type conversions │ └── ... │ ├── state/ # Global state │ ├── useGlobalState.ts # Global state hook │ └── ... │ ├── types/ # Type definitions │ └── index.ts # Export all types │ ├── config/ # Configuration │ ├── constants.ts │ └── env.ts │ ├── definitions/ # Enums and constants │ ├── roles.ts │ ├── statuses.ts │ └── ... │ ├── widgets/ # Complex UI widgets │ └── ... │ └── i18n/ # Internationalization └── ... ``` --- ## Routing & Navigation ### React Router Setup: `App.tsx` ```typescript import { BrowserRouter, Routes, Route, Navigate } from 'react-router'; import { useAuth } from 'hooks/useAuth'; import { useServerSettings } from 'state/useServerSettings'; export function App() { const { isLoading, isAuthenticated } = useAuth(); const { onboardingComplete, isLoading: isServerSettingsLoading } = useServerSettings(); // Show nothing while loading auth state if (isLoading || isServerSettingsLoading) return null; return ( {!isAuthenticated && ( } /> } /> } /> } /> } /> )} {isAuthenticated && onboardingComplete && ( {/* Home & Settings */} } /> } /> } /> } /> {/* Features */} } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> {/* Workspaces & Projects */} } /> } /> } /> } /> } /> {/* Development */} } /> {/* Auth */} } /> {/* Fallback */} } /> )} ); } ``` ### Layout Components **AuthenticationLayout** - Wrapper for public pages ```typescript export const AuthenticationLayout = ({ children }: Props) => { return (
{children}
); }; ``` **DashboardLayout** - Wrapper for protected pages ```typescript export const DashboardLayout = ({ children }: Props) => { return (
{children}
); }; ``` --- ## Component Architecture ### Component Patterns #### 1. Functional Components with Props All components are functional components using React 19: ```typescript type ButtonProps = { children: React.ReactNode; onClick: () => void; variant?: 'primary' | 'secondary' | 'outline'; size?: 'sm' | 'md' | 'lg'; disabled?: boolean; }; export const Button = ({ children, onClick, variant = 'primary', size = 'md', disabled = false, }: ButtonProps) => { return ( ); }; ``` #### 2. Component Organization Complex components follow a structured pattern: ```typescript // Directory structure ComponentName/ ├── index.tsx # Exports the component ├── ComponentName.tsx # Main implementation ├── hooks/ # Local hooks │ └── useComponentState.ts ├── types.ts # Component types └── utils.ts # Helper functions // index.tsx export { ComponentName } from './ComponentName'; export type { ComponentNameProps } from './types'; // ComponentName.tsx import { useComponentState } from './hooks/useComponentState'; import type { ComponentNameProps } from './types'; export const ComponentName = ({ prop1, prop2 }: ComponentNameProps) => { const { state, actions } = useComponentState(); return (
{/* Render */}
); }; ``` #### 3. React 19 Patterns **NO useCallback - React 19's compiler handles optimization:** ```typescript // ❌ BAD - Unnecessary useCallback export const Form = () => { const handleSubmit = useCallback((data) => { api.post('/data', data); }, []); return ; }; // ✅ GOOD - Plain function export const Form = () => { const handleSubmit = (data) => { api.post('/data', data); }; return ; }; ``` **NO useMemo - Compiler handles memoization:** ```typescript // ❌ BAD - Unnecessary useMemo export const List = ({ items, filter }) => { const filtered = useMemo( () => items.filter(i => i.type === filter), [items, filter] ); return ; }; // ✅ GOOD - Direct calculation export const List = ({ items, filter }) => { const filtered = items.filter(i => i.type === filter); return ; }; ``` **Avoid stale closures - access object properties directly:** ```typescript // ❌ BAD - Destructuring creates stale references export const Editor = ({ state }) => { const { content, save } = state; useEffect(() => { const timer = setTimeout(() => { save(content); // 'content' is stale! }, 1000); return () => clearTimeout(timer); }, [content, save]); }; // ✅ GOOD - Direct property access export const Editor = ({ state }) => { useEffect(() => { const timer = setTimeout(() => { state.save(state.content); // Always fresh }, 1000); return () => clearTimeout(timer); }, [state]); }; ``` #### 4. Keyboard Event Handling Always prevent default for game/editor controls: ```typescript export const CodeEditor = () => { const handleKeyDown = (ev: KeyboardEvent) => { // Prevent Space from scrolling page if (ev.code === 'Space') { ev.preventDefault(); // Handle space key } // Prevent Escape from closing dialogs if (ev.key === 'Escape') { ev.preventDefault(); // Handle escape } }; return
{/* ... */}
; }; ``` #### 5. Button Focus Management Buttons retain focus after clicking, interfering with keyboard shortcuts: ```typescript export const Toolbar = () => { return ( <> ); }; ``` --- ## State Management ### Authentication State: `useAuth` ```typescript // Location: workspaces/hooks/useAuth.ts export const useAuth = () => { const [isLoading, setIsLoading] = useState(true); const [isAuthenticated, setIsAuthenticated] = useState(false); const [user, setUser] = useState(null); const [token, setToken] = useState( () => localStorage.getItem('token') ); // Load auth state on mount useEffect(() => { const load = async () => { if (!token) { setIsAuthenticated(false); setIsLoading(false); return; } try { // Verify token validity const response = await fetch('/api/auth/verify', { headers: { Authorization: `Bearer ${token}` }, }); if (response.ok) { const user = await response.json(); setUser(user); setIsAuthenticated(true); } else { setIsAuthenticated(false); localStorage.removeItem('token'); } } catch { setIsAuthenticated(false); } finally { setIsLoading(false); } }; load(); }, [token]); const login = async (email: string, password: string) => { const response = await fetch('/api/auth/signin', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), }); if (response.ok) { const { token, user } = await response.json(); localStorage.setItem('token', token); setToken(token); setUser(user); setIsAuthenticated(true); return { success: true }; } return { success: false, error: await response.text() }; }; const logout = async () => { try { await fetch('/api/auth/signout', { method: 'POST', headers: { Authorization: `Bearer ${token}` }, }); } catch { // Ignore errors during logout } finally { localStorage.removeItem('token'); setToken(null); setUser(null); setIsAuthenticated(false); } }; return { isLoading, isAuthenticated, user, token, login, logout, }; }; // Usage in components export const Dashboard = () => { const { user, logout } = useAuth(); return (

Welcome, {user?.email}

); }; ``` ### Server Settings State: `useServerSettings` ```typescript export const useServerSettings = () => { const [isLoading, setIsLoading] = useState(true); const [onboardingComplete, setOnboardingComplete] = useState(false); const [plugins, setPlugins] = useState([]); const [settings, setSettings] = useState(null); useEffect(() => { const load = async () => { try { const response = await fetch('/api/server-settings'); if (response.ok) { const data = await response.json(); setOnboardingComplete(data.onboardingComplete); setPlugins(data.plugins); setSettings(data.settings); } } catch { console.error('Failed to load server settings'); } finally { setIsLoading(false); } }; load(); }, []); return { isLoading, onboardingComplete, plugins, settings, }; }; ``` ### Global State: `useGlobalState` For component-tree-wide state, use Context: ```typescript type GlobalContextType = { theme: 'light' | 'dark'; setTheme: (theme: 'light' | 'dark') => void; sidebarOpen: boolean; setSidebarOpen: (open: boolean) => void; }; const GlobalContext = createContext(null); export const GlobalProvider = ({ children }: { children: React.ReactNode }) => { const [theme, setTheme] = useState<'light' | 'dark'>(() => { const saved = localStorage.getItem('theme'); return (saved as 'light' | 'dark') || 'dark'; }); const [sidebarOpen, setSidebarOpen] = useState(true); useEffect(() => { localStorage.setItem('theme', theme); document.documentElement.classList.toggle('dark', theme === 'dark'); }, [theme]); return ( {children} ); }; export const useGlobalState = () => { const context = useContext(GlobalContext); if (!context) { throw new Error('useGlobalState must be used within GlobalProvider'); } return context; }; ``` ### React Query for Server State ```typescript // useQueryPlan.ts export const useQueryPlan = (id: number) => { return useQuery({ queryKey: ['plans', id], queryFn: async () => { const response = await fetch(`/api/plans/${id}`, { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }, }); if (!response.ok) throw new Error('Failed to load plan'); return response.json(); }, }); }; // useMutationCreatePlan.ts export const useMutationCreatePlan = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async (data: CreatePlanInput) => { const response = await fetch('/api/plans', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('token')}`, }, body: JSON.stringify(data), }); if (!response.ok) throw new Error('Failed to create plan'); return response.json(); }, onSuccess: () => { // Invalidate related queries queryClient.invalidateQueries({ queryKey: ['plans'] }); }, }); }; // In component export const CreatePlanForm = () => { const { mutate, isPending } = useMutationCreatePlan(); const handleSubmit = (data: CreatePlanInput) => { mutate(data); }; return (
{/* Form fields */}
); }; ``` --- ## API Communication ### Client Initialization: `src/apps/officer-web/lib/api.ts` ```typescript // Centralized API client const API_BASE = import.meta.env.VITE_API_URL || '/api'; export const apiClient = { async fetch( endpoint: string, options: RequestInit = {}, ): Promise { const token = localStorage.getItem('token'); return fetch(`${API_BASE}${endpoint}`, { ...options, headers: { 'Content-Type': 'application/json', ...(token && { Authorization: `Bearer ${token}` }), ...options.headers, }, }); }, async get(endpoint: string): Promise { const response = await this.fetch(endpoint); if (!response.ok) throw new Error(`GET ${endpoint} failed`); return response.json(); }, async post(endpoint: string, body?: any): Promise { const response = await this.fetch(endpoint, { method: 'POST', body: body ? JSON.stringify(body) : undefined, }); if (!response.ok) throw new Error(`POST ${endpoint} failed`); return response.json(); }, async put(endpoint: string, body?: any): Promise { const response = await this.fetch(endpoint, { method: 'PUT', body: body ? JSON.stringify(body) : undefined, }); if (!response.ok) throw new Error(`PUT ${endpoint} failed`); return response.json(); }, async delete(endpoint: string): Promise { const response = await this.fetch(endpoint, { method: 'DELETE' }); if (!response.ok) throw new Error(`DELETE ${endpoint} failed`); return response.json(); }, }; // Usage const user = await apiClient.get('/users/me'); await apiClient.post('/plans', { title: 'New Plan' }); ``` --- ## Real-Time Features ### Terminal WebSocket: `TerminalScreen.tsx` ```typescript import { XTerm } from '@xterm/xterm'; import { FitAddon } from '@xterm/addon-fit'; import '@xterm/xterm/css/xterm.css'; export const TerminalScreen = () => { const terminalRef = useRef(null); const xtermRef = useRef(null); const wsRef = useRef(null); useEffect(() => { const term = new XTerm({ cols: 120, rows: 40, theme: { background: '#0f172a', foreground: '#e2e8f0' }, }); const fitAddon = new FitAddon(); term.loadAddon(fitAddon); // Mount terminal if (terminalRef.current) { term.open(terminalRef.current); fitAddon.fit(); } xtermRef.current = term; // Connect WebSocket const token = localStorage.getItem('token'); const cwd = '/home/user'; // Or from settings const ws = new WebSocket( `ws://localhost:5000/api/terminal/ws?token=${token}&cwd=${encodeURIComponent(cwd)}&cols=120&rows=40` ); ws.onopen = () => { console.log('Terminal connected'); }; ws.onmessage = (event) => { const message = JSON.parse(event.data); if (message.type === 'output') { term.write(message.data); // Display output } else if (message.type === 'error') { console.error('Terminal error:', message.error); } }; ws.onerror = () => { term.write('\r\nConnection error\r\n'); }; ws.onclose = () => { term.write('\r\nDisconnected\r\n'); }; // Send input from terminal term.onData((data) => { ws.send(JSON.stringify({ type: 'input', data })); }); wsRef.current = ws; // Handle resize const handleResize = () => { fitAddon.fit(); const { cols, rows } = term; ws.send(JSON.stringify({ type: 'resize', cols, rows })); }; window.addEventListener('resize', handleResize); // Cleanup return () => { window.removeEventListener('resize', handleResize); ws.close(); term.dispose(); }; }, []); return
; }; ``` ### Pi Chat WebSocket: `SessionListPage.tsx` ```typescript export const SessionListPage = ({ sessionId, isNew }: Props) => { const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const [isLoading, setIsLoading] = useState(false); const wsRef = useRef(null); // Connect to Pi chat WebSocket useEffect(() => { const token = localStorage.getItem('token'); const url = `ws://localhost:5000/api/pi/chat/ws?token=${token}&sessionId=${sessionId}`; const ws = new WebSocket(url); ws.onopen = () => { console.log('Pi chat connected'); }; ws.onmessage = (event) => { const message = JSON.parse(event.data); switch (message.type) { case 'message_update': // Stream response text if (message.event.type === 'text_delta') { setMessages(prev => { const last = prev[prev.length - 1]; if (last && last.role === 'assistant') { return [ ...prev.slice(0, -1), { ...last, content: last.content + message.event.delta }, ]; } return prev; }); } break; case 'tool_execution_start': setMessages(prev => [ ...prev, { role: 'tool', toolName: message.toolName, content: 'Executing...' }, ]); break; case 'agent_end': setIsLoading(false); break; } }; ws.onerror = (error) => { console.error('Pi chat error:', error); setIsLoading(false); }; wsRef.current = ws; return () => ws.close(); }, [sessionId]); // Send message const handleSendMessage = () => { if (!input.trim() || !wsRef.current) return; const userMessage = input; setInput(''); setIsLoading(true); // Add user message to UI setMessages(prev => [ ...prev, { role: 'user', content: userMessage }, ]); // Send to Pi wsRef.current.send(JSON.stringify({ type: 'prompt', text: userMessage, })); }; return (
{messages.map((msg, i) => (
{msg.content}
))} {isLoading &&
Loading...
}
setInput(e.target.value)} onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()} placeholder="Ask Pi anything..." disabled={isLoading} className="flex-1 px-4 py-2 border rounded" />
); }; ``` --- ## Styling & Theming ### Tailwind CSS Integration Officer uses Tailwind CSS 4 with a custom configuration: ```javascript // tailwind.config.js export default { content: [ './src/**/*.{ts,tsx}', ], theme: { extend: { colors: { // Custom color palette if needed }, spacing: { // Custom spacing }, animation: { // Custom animations }, }, }, plugins: [ require('tailwindcss-animate'), ], }; ``` ### Shadcn/ui Components Reusable UI components from shadcn/ui: ```typescript // Button component wrapper import { Button as ShadcnButton } from '@/components/ui/button'; export const Button = (props) => ( ); // Usage ``` ### Custom Theming Support for light/dark themes: ```typescript // Global theme management export const useTheme = () => { const { theme, setTheme } = useGlobalState(); useEffect(() => { document.documentElement.classList.toggle('dark', theme === 'dark'); localStorage.setItem('theme', theme); }, [theme]); return { theme, setTheme }; }; // In component export const ThemeToggle = () => { const { theme, setTheme } = useTheme(); return ( ); }; ``` --- ## Performance & Optimization ### Image Optimization Use Next Image component or lazy loading: ```typescript import { lazy, Suspense } from 'react'; // Lazy load heavy components const CodeEditor = lazy(() => import('./CodeEditor')); export const FeaturePage = () => { return ( Loading...
}> ); }; ``` ### Virtual Scrolling for Large Lists Use React Virtual for efficient rendering: ```typescript import { useVirtualizer } from '@tanstack/react-virtual'; export const VirtualList = ({ items }: { items: Item[] }) => { const parentRef = useRef(null); const virtualizer = useVirtualizer({ count: items.length, getScrollElement: () => parentRef.current, estimateSize: () => 50, }); return (
{virtualizer.getVirtualItems().map(virtualItem => (
{items[virtualItem.index]?.name}
))}
); }; ``` ### Code Splitting with React Router Routes are automatically code-split: ```typescript // Lazy load screen components const HomeScreen = lazy(() => import('./HomeScreen')); const SettingsScreen = lazy(() => import('./SettingsScreen')); }> } /> } /> ``` ### React Query Caching Automatic caching and background updates: ```typescript // Data is cached per queryKey const { data: plans } = useQuery({ queryKey: ['plans'], // Cached queryFn: fetchPlans, staleTime: 1000 * 60 * 5, // 5 minutes gcTime: 1000 * 60 * 10, // 10 minutes (was cacheTime) }); // Background refetch when window regains focus useQuery({ queryKey: ['plans'], queryFn: fetchPlans, refetchOnWindowFocus: true, }); ``` --- ## Development Workflow ### Development Server ```bash # Start Bun dev server with hot reload bun dev # Runs on http://localhost:5000/ # Frontend and backend both reload on file changes ``` ### Environment Variables ```bash # .env (client-side, public) VITE_API_URL=http://localhost:5000/api ``` ### TypeScript Type Checking ```bash # Run TypeScript compiler tsc --noEmit # Included in build process bun run build ``` ### Code Formatting ```bash # Format all files bun format # Format specific files bun format:check # Prettier config in .prettierrc { "semi": true, "singleQuote": true, "trailingComma": "all", "printWidth": 120 } ``` ### Component Development When creating new features: 1. **Create component structure** ``` src/apps/officer-web/Screens/MyFeature/ ├── index.tsx # Exports ├── MyFeatureScreen.tsx # Component ├── hooks/ │ └── useMyFeature.ts ├── types.ts └── utils.ts ``` 2. **Define types** ```typescript // types.ts export type MyFeatureProps = { onSubmit: (data: FormData) => void; disabled?: boolean; }; ``` 3. **Implement component** ```typescript // MyFeatureScreen.tsx import { useMyFeature } from './hooks/useMyFeature'; import type { MyFeatureProps } from './types'; export const MyFeatureScreen = ({ onSubmit, disabled }: MyFeatureProps) => { const { state, actions } = useMyFeature(); return (/* JSX */); }; ``` 4. **Export** ```typescript // index.tsx export { MyFeatureScreen } from './MyFeatureScreen'; export type { MyFeatureProps } from './types'; ``` --- ## Integration Points ### API Endpoints Used - **Auth**: `/api/auth/*` (signin, signup, verify, etc.) - **Plans**: `/api/plans` (GET, POST, PUT, DELETE) - **Skills**: `/api/skills` (GET, POST) - **Tasks**: `/api/tasks` (CRUD operations) - **Files**: `/api/file-browser` (navigation, upload) - **Settings**: `/api/user` (preferences) - **Terminal WS**: `/api/terminal/ws` (real-time shell) - **Pi Chat WS**: `/api/pi/chat/ws` (real-time AI assistant) ### State Flow from Server to UI ``` Database ↓ API Response ↓ React Query Cache ↓ Component State ↓ Rendered UI ``` --- ## Debugging ### Browser DevTools ```javascript // Log component props console.log('Props:', props); // Log state updates console.log('State changed:', newState); // React DevTools browser extension // Inspect component tree and props ``` ### React Query Devtools ```typescript // Installed: @tanstack/react-query-devtools import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; export function App() { return ( <> {/* App content */} ); } ``` --- ## Common Patterns ### Loading States ```typescript export const DataComponent = () => { const { data, isLoading, error } = useQuery({ queryKey: ['data'], queryFn: fetchData, }); if (isLoading) return ; if (error) return ; if (!data) return ; return ; }; ``` ### Form Handling with React Hook Form ```typescript import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; const schema = z.object({ email: z.string().email('Invalid email'), password: z.string().min(8, 'Min 8 chars'), }); type FormData = z.infer; export const LoginForm = () => { const { register, handleSubmit, formState: { errors } } = useForm({ resolver: zodResolver(schema), }); const onSubmit = async (data: FormData) => { await apiClient.post('/auth/signin', data); }; return (
{errors.email && {errors.email.message}} {errors.password && {errors.password.message}}
); }; ``` ### Toast Notifications ```typescript import { toast } from 'sonner'; // Success toast.success('Operation completed!'); // Error toast.error('Something went wrong', { description: 'Please try again later', }); // Custom toast.custom((t) => (
Custom notification
)); ``` --- ## Related Documentation - **Backend Architecture**: See `OFFICERDEV_BACKEND.md` - **CONVENTIONS.md**: Detailed code patterns and style - **CLAUDE.md**: Project overview - **apps/officer-web/CLAUDE.md**: Frontend-specific patterns --- ## Summary Officer's frontend is a modern React 19 SPA featuring: - Type-safe component architecture - Real-time WebSocket communication - Server state management with React Query - Beautiful UI with Tailwind CSS and shadcn/ui - Strong React patterns without useCallback/useMemo - Comprehensive authentication and authorization - Responsive design for all devices - AI-powered development assistance via Pi - Interactive terminal emulation The architecture prioritizes developer experience, type safety, and performance while providing a rich, interactive user experience for life management and AI-assisted development.