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,
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;
}
},

View File

@@ -20,12 +20,27 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Probe auth state by hitting a protected endpoint.
// If the ws_token cookie is valid, the server returns 200; else 401.
fetch("/api/v1/auth/me", { credentials: "include" })
.then((r) => setAuthenticated(r.ok))
.catch(() => setAuthenticated(false))
.finally(() => setIsLoading(false));
// Probe auth state by hitting a protected endpoint. An HTTP status is an
// authoritative answer (200 = in, 401 = out); a thrown fetch is a NETWORK
// failure and must not log the user out — retry once before giving up.
let cancelled = false;
const probe = () => fetch("/api/v1/auth/me", { credentials: "include" });
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 () => {

View File

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

View File

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