Auth probe retries network failures; lazy context init; CK repair logging

A thrown fetch in the auth probe is a network failure, not a 401 — it
now retries once before treating the user as logged out, and logs the
real cause (FE-15). Theme and privacy state initialize lazily from
localStorage so neither the wrong theme nor unmasked sensitive values
flash on reload (FE-07). The CopilotKit orphan-tool-call repair logs
when it has to skip instead of failing silently (FE-11).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Carlos Escalante
2026-06-10 17:30:26 -06:00
parent 2a68a05ffd
commit b5bb2e8562
4 changed files with 37 additions and 23 deletions

View File

@@ -382,7 +382,10 @@ app.all("/api/copilotkit/*", async (c) => {
headers: outbound.headers, headers: outbound.headers,
body: JSON.stringify({ ...body, messages: paired }), body: JSON.stringify({ ...body, messages: paired }),
}); });
} catch { } catch (err) {
// Skipping the repair is survivable; doing it silently is not —
// orphan tool calls then fail downstream with no trace of why.
console.error("[copilotkit] orphan-tool-call repair skipped:", err);
return; return;
} }
}, },

View File

@@ -20,12 +20,27 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
useEffect(() => { useEffect(() => {
// Probe auth state by hitting a protected endpoint. // Probe auth state by hitting a protected endpoint. An HTTP status is an
// If the ws_token cookie is valid, the server returns 200; else 401. // authoritative answer (200 = in, 401 = out); a thrown fetch is a NETWORK
fetch("/api/v1/auth/me", { credentials: "include" }) // failure and must not log the user out — retry once before giving up.
.then((r) => setAuthenticated(r.ok)) let cancelled = false;
.catch(() => setAuthenticated(false)) const probe = () => fetch("/api/v1/auth/me", { credentials: "include" });
.finally(() => setIsLoading(false)); probe()
.catch(() => new Promise((r) => setTimeout(r, 1500)).then(probe))
.then((r) => {
if (!cancelled) setAuthenticated((r as Response).ok);
})
.catch(() => {
// Network still down: leave unauthenticated but log the real cause.
console.warn("Auth probe failed twice (network) — backend unreachable");
if (!cancelled) setAuthenticated(false);
})
.finally(() => {
if (!cancelled) setIsLoading(false);
});
return () => {
cancelled = true;
};
}, []); }, []);
const logout = async () => { const logout = async () => {

View File

@@ -8,11 +8,10 @@ const PrivacyContext = createContext<{
}>({ privacyMode: false, togglePrivacy: () => {} }); }>({ privacyMode: false, togglePrivacy: () => {} });
export function PrivacyProvider({ children }: { children: ReactNode }) { export function PrivacyProvider({ children }: { children: ReactNode }) {
const [privacyMode, setPrivacyMode] = useState<boolean>(false); // Lazy initializer: sensitive values must never flash unmasked on reload.
const [privacyMode, setPrivacyMode] = useState<boolean>(
useEffect(() => { () => localStorage.getItem("privacyMode") === "true",
setPrivacyMode(localStorage.getItem("privacyMode") === "true"); );
}, []);
useEffect(() => { useEffect(() => {
document.documentElement.classList.toggle("privacy", privacyMode); document.documentElement.classList.toggle("privacy", privacyMode);

View File

@@ -10,18 +10,15 @@ const ThemeContext = createContext<{
}>({ theme: "dark", toggleTheme: () => {} }); }>({ theme: "dark", toggleTheme: () => {} });
export function ThemeProvider({ children }: { children: ReactNode }) { export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>("dark"); // Lazy initializer: resolving the theme before first paint avoids the
// flash of wrong theme an effect-based init caused.
// Initialize once on mount (localStorage + prefers-color-scheme). const [theme, setTheme] = useState<Theme>(() => {
useEffect(() => {
const saved = localStorage.getItem("theme") as Theme | null; const saved = localStorage.getItem("theme") as Theme | null;
const initial: Theme = saved if (saved) return saved;
? saved return window.matchMedia("(prefers-color-scheme: dark)").matches
: window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark" ? "dark"
: "light"; : "light";
setTheme(initial); });
}, []);
useEffect(() => { useEffect(() => {
document.documentElement.classList.toggle("dark", theme === "dark"); document.documentElement.classList.toggle("dark", theme === "dark");