Files
llink/js/src/App.tsx
T
Claude c00a7a439a feat(electron): error handling foundation
Adds the infrastructure for a coherent client-side error story:

- `lib/errors.ts`: canonical ApiError + QuotaExceededError, `toUserMessage`
  (friendly strings for ApiError/ZodError/network errors, strips Electron
  IPC message prefixes), `logError` (expected), `reportError` (unexpected).
- `lib/query-client.ts`: QueryClient factory with sane retry defaults (no
  retry on 4xx except 408/429, 2 retries otherwise; 0 mutation retries),
  `QueryCache` onError logs + opts in via `meta.toastOnError`, and
  `MutationCache` onError toasts `toUserMessage(err)` by default with
  `meta.suppressToast` as the opt-out.
- `components/app-error-boundary.tsx` + `error-fallback.tsx`: two boundaries
  (top-level outside the router, route-level inside) with a Card-based
  fallback offering 'Go home' + 'Try again'. Route boundary resets on
  pathname change and clears React Query error cache on retry.
- `main/ipc-utils.ts` + main.ts migration: `safeHandle` wraps ipcMain.handle
  so main-process failures log with full stack and surface a sanitized
  message to the renderer. `link:fetch-metadata` keeps its null contract
  but now logs.
- `useCreateParticle` opts out of the global toast (compose-overlay renders
  its own quota UX) so nothing double-toasts.

Render crashes now have a recovery UI, every mutation gets a free error
toast, and silent-catch cleanup + Sentry land in follow-up PRs.
2026-04-16 23:29:36 +00:00

98 lines
2.9 KiB
TypeScript

import { useEffect } from "react";
import { HashRouter, Routes, Route, useNavigate } from "react-router-dom";
import { TooltipProvider } from "@/components/ui/tooltip";
import { useAuthStore } from "@/stores/auth-store";
import { LoginPage } from "@/features/auth/login-page";
import { QueryClientProvider } from '@tanstack/react-query';
import SettingsPage from "@/features/settings-page";
import AudioVideoSettingsPage from "@/features/settings/audio-video-settings-page";
import NetworkSelector from "@/features/network-selector";
import NetworkRoot from "@/features/network-root";
import ParticleViewResolver from "@/features/particles/particle-view-resolver";
import Layout from "@/features/layout";
import NetworkSettingsPage from "@/features/network-settings";
import { Toaster } from "@/components/ui/sonner";
import { PusherProvider } from "@/lib/pusher-provider";
import { createQueryClient } from "@/lib/query-client";
import {
RouteErrorBoundary,
TopLevelErrorBoundary,
} from "@/components/app-error-boundary";
const queryClient = createQueryClient();
const App = () => {
const status = useAuthStore((s) => s.status);
const restoreSession = useAuthStore((s) => s.restoreSession);
useEffect(() => {
restoreSession();
}, [restoreSession]);
if (status === "idle" || status === "restoring") {
return (
<div className="flex min-h-screen items-center justify-center">
<p className="text-muted-foreground text-sm">Loading...</p>
</div>
);
}
if (status !== "authenticated") {
return <LoginPage />;
}
return (
<PusherProvider>
<AuthenticatedApp />
</PusherProvider>
);
};
function AutoplayNavigationListener() {
const navigate = useNavigate();
useEffect(() => {
return window.electronAutoplay.onNavigate((data) => {
navigate(`/${data.networkId}/${data.streamId}`);
});
}, [navigate]);
return null;
}
function AuthenticatedApp() {
return (
<HashRouter>
<AutoplayNavigationListener />
<RouteErrorBoundary>
<Routes>
<Route path="settings" element={<SettingsPage />} />
<Route path="settings/audio-video" element={<AudioVideoSettingsPage />} />
<Route path="/">
<Route index element={<Layout><NetworkSelector /></Layout>} />
<Route path=":networkId">
<Route index element={<Layout><NetworkRoot /></Layout>} />
<Route path="settings" element={<NetworkSettingsPage />} />
<Route path="*" element={<ParticleViewResolver />} />
</Route>
</Route>
</Routes>
</RouteErrorBoundary>
</HashRouter>
);
}
const AppWithProviders = () => (
<TopLevelErrorBoundary>
<TooltipProvider>
<QueryClientProvider client={queryClient}>
<App />
<Toaster />
</QueryClientProvider>
</TooltipProvider>
</TopLevelErrorBoundary>
);
export default AppWithProviders;