From 759efb9fa860b6ea9ecd728c1414045f05e6d458 Mon Sep 17 00:00:00 2001 From: Jon Saad-Falcon Date: Sat, 28 Feb 2026 18:02:09 +0000 Subject: [PATCH] Add frontend CI workflow, error boundary, and VITE_API_URL support - .github/workflows/frontend.yml: type-check + build on push/PR - ErrorBoundary component wraps App with catch + retry UI - API client reads VITE_API_URL env var for non-same-origin deploys - vite-env.d.ts declares ImportMetaEnv type Co-Authored-By: Claude Opus 4.6 --- .github/workflows/frontend.yml | 37 ++++++++++ frontend/src/api/client.ts | 2 +- frontend/src/components/ErrorBoundary.tsx | 86 +++++++++++++++++++++++ frontend/src/main.tsx | 5 +- frontend/src/vite-env.d.ts | 9 +++ 5 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/frontend.yml create mode 100644 frontend/src/components/ErrorBoundary.tsx create mode 100644 frontend/src/vite-env.d.ts diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml new file mode 100644 index 00000000..b6590a76 --- /dev/null +++ b/.github/workflows/frontend.yml @@ -0,0 +1,37 @@ +name: Frontend CI + +on: + push: + branches: [main] + paths: + - 'frontend/**' + - '.github/workflows/frontend.yml' + pull_request: + branches: [main] + paths: + - 'frontend/**' + - '.github/workflows/frontend.yml' + workflow_dispatch: + +concurrency: + group: frontend-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - run: npm ci + - run: npx tsc --noEmit + - run: npm run build diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index f7a64d21..de9a0c6d 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -1,6 +1,6 @@ import type { ModelInfo, SavingsData, ServerInfo } from '../types'; -const BASE = ''; // relative to same origin +const BASE = import.meta.env.VITE_API_URL || ''; // relative to same origin by default export async function fetchModels(): Promise { const res = await fetch(`${BASE}/v1/models`); diff --git a/frontend/src/components/ErrorBoundary.tsx b/frontend/src/components/ErrorBoundary.tsx new file mode 100644 index 00000000..c370c090 --- /dev/null +++ b/frontend/src/components/ErrorBoundary.tsx @@ -0,0 +1,86 @@ +import { Component } from 'react'; +import type { ErrorInfo, ReactNode } from 'react'; + +interface Props { + children: ReactNode; +} + +interface State { + hasError: boolean; + error: Error | null; +} + +export class ErrorBoundary extends Component { + constructor(props: Props) { + super(props); + this.state = { hasError: false, error: null }; + } + + static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, info: ErrorInfo) { + console.error('ErrorBoundary caught:', error, info.componentStack); + } + + render() { + if (this.state.hasError) { + return ( +
+
+

Something went wrong

+

+ {this.state.error?.message || 'An unexpected error occurred.'} +

+ +
+
+ ); + } + return this.props.children; + } +} + +const styles: Record = { + container: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: '100vh', + backgroundColor: '#1a1a1e', + color: '#e2e8f0', + fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif', + }, + card: { + textAlign: 'center' as const, + padding: 32, + maxWidth: 420, + }, + heading: { + fontSize: 20, + fontWeight: 600, + marginBottom: 12, + }, + message: { + fontSize: 14, + color: '#94a3b8', + marginBottom: 24, + lineHeight: 1.5, + }, + button: { + padding: '10px 24px', + borderRadius: 8, + border: 'none', + backgroundColor: '#2563eb', + color: 'white', + fontSize: 14, + fontWeight: 500, + cursor: 'pointer', + }, +}; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index fad42318..24513258 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,10 +1,13 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; +import { ErrorBoundary } from './components/ErrorBoundary'; import App from './App'; import './App.css'; createRoot(document.getElementById('root')!).render( - + + + , ); diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 00000000..29c29a18 --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +interface ImportMetaEnv { + readonly VITE_API_URL: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +}