mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-07-17 10:08:46 +02:00
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:
@@ -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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
Reference in New Issue
Block a user