Files
backspace/packages/web/src/App.tsx
T
Jannis Braun 7e30db3773 feat(testing): smoke-test script + VITE_FORCE_BOOT_STALL gate
- App.tsx and main.tsx ErrorBoundary gate rendererReady() on
  VITE_FORCE_BOOT_STALL build env var. When set, the ping is suppressed
  so the main-process boot timer fires (exercising the renderer-stalled
  recovery path without hand-editing source).
- vite-env.d.ts declares the env var type so TS doesn't complain.
- scripts/smoke-recovery.sh automates scenarios 2 (bad URL → load-failed)
  and 4 (forced stall → renderer-stalled), grepping stderr for the
  [recovery] entered: ... lines added by the prior commit. Backs up and
  restores the user's instance-url.json. Manual scenarios documented in
  the script header.
2026-05-03 13:21:54 +02:00

103 lines
3.5 KiB
TypeScript

import React, { useEffect } from 'react';
import { Routes, Route, Navigate, useSearchParams } from 'react-router-dom';
import { LoginPage } from './components/auth/LoginPage';
import { RegisterPage } from './components/auth/RegisterPage';
import { AppLayout } from './components/layout/AppLayout';
import { JoinPage } from './components/JoinPage';
import { SwAutoUpdate } from './components/ui/SwUpdatePrompt';
import { ScreenSharePicker } from './components/voice/ScreenSharePicker';
import { useAuthStore } from './stores/authStore';
import { isElectron } from './platform/platform';
function ProtectedRoute({ children }: { children: React.ReactNode }) {
const token = useAuthStore((s) => s.token);
if (!token) return <Navigate to="/login" replace />;
return <>{children}</>;
}
function AuthRedirect({ children }: { children: React.ReactNode }) {
const token = useAuthStore((s) => s.token);
const [searchParams] = useSearchParams();
const redirect = searchParams.get('redirect');
if (token) {
if (redirect && redirect.startsWith('/') && !redirect.startsWith('//')) {
return <Navigate to={redirect} replace />;
}
return <Navigate to="/channels/@me" replace />;
}
return <>{children}</>;
}
export function App() {
// Boot-completion ping — disarms the main-process boot timer.
// MUST be first hook on the unconditional render path. Single fire on
// first commit; main owns "armed once" gating so duplicate calls (e.g.
// ErrorBoundary path) are idempotent. Semantic: "renderer survived
// render," NOT "data loaded." A spinner on screen counts as boot success.
//
// Gated on VITE_FORCE_BOOT_STALL so the smoke-test harness can build a
// variant that intentionally never pings, exercising the renderer-stalled
// recovery path without hand-editing source. Set 'true' / '1' to force stall.
useEffect(() => {
if (import.meta.env.VITE_FORCE_BOOT_STALL) return;
if (typeof window.backspace?.rendererReady === 'function') {
window.backspace.rendererReady();
}
}, []);
const showTitleBar = isElectron();
return (
<div className={`flex flex-col ${showTitleBar ? 'h-screen' : 'contents'}`}>
{showTitleBar && <>
<div className="h-8 flex-shrink-0 bg-surface-base titlebar-drag" />
<div className="h-px flex-shrink-0 bg-border-hard" />
</>}
<div className={showTitleBar ? 'flex-1 min-h-0' : 'contents'}>
<SwAutoUpdate />
<ScreenSharePicker />
<Routes>
<Route
path="/login"
element={
<AuthRedirect>
<LoginPage />
</AuthRedirect>
}
/>
<Route
path="/register"
element={
<AuthRedirect>
<RegisterPage />
</AuthRedirect>
}
/>
<Route
path="/channels/:spaceId/:channelId?"
element={
<ProtectedRoute>
<AppLayout />
</ProtectedRoute>
}
/>
<Route
path="/join/:inviteCode"
element={<JoinPage />}
/>
<Route
path="/explore"
element={
<ProtectedRoute>
<AppLayout />
</ProtectedRoute>
}
/>
<Route path="/" element={<Navigate to="/channels/@me" replace />} />
<Route path="*" element={<Navigate to="/channels/@me" replace />} />
</Routes>
</div>
</div>
);
}