Add startup retry logic and persist logs across refresh
All checks were successful
Release / build-and-release (push) Successful in 25m5s
All checks were successful
Release / build-and-release (push) Successful in 25m5s
Retry connectActiveProfile up to 3 times with 1s delay before showing the error page. Persist log buffer to sessionStorage so logs survive page refresh and are visible in the settings log viewer. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6f84964689
commit
fb92cfc12c
2 changed files with 48 additions and 8 deletions
27
src/App.tsx
27
src/App.tsx
|
|
@ -16,6 +16,8 @@ import ProfileSelectionPage from "./pages/ProfileSelectionPage";
|
||||||
import ErrorPage from "./components/shared/ErrorPage";
|
import ErrorPage from "./components/shared/ErrorPage";
|
||||||
|
|
||||||
const STARTUP_TIMEOUT_MS = 10_000;
|
const STARTUP_TIMEOUT_MS = 10_000;
|
||||||
|
const MAX_RETRIES = 3;
|
||||||
|
const RETRY_DELAY_MS = 1_000;
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
@ -23,29 +25,42 @@ export default function App() {
|
||||||
const [dbReady, setDbReady] = useState(false);
|
const [dbReady, setDbReady] = useState(false);
|
||||||
const [startupError, setStartupError] = useState<string | null>(null);
|
const [startupError, setStartupError] = useState<string | null>(null);
|
||||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const cancelledRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeProfile && !isLoading) {
|
if (activeProfile && !isLoading) {
|
||||||
setDbReady(false);
|
setDbReady(false);
|
||||||
setStartupError(null);
|
setStartupError(null);
|
||||||
|
cancelledRef.current = false;
|
||||||
|
|
||||||
timeoutRef.current = setTimeout(() => {
|
timeoutRef.current = setTimeout(() => {
|
||||||
setStartupError(t("error.startupTimeout"));
|
setStartupError(t("error.startupTimeout"));
|
||||||
}, STARTUP_TIMEOUT_MS);
|
}, STARTUP_TIMEOUT_MS);
|
||||||
|
|
||||||
connectActiveProfile()
|
const attemptConnect = async (attempt: number): Promise<void> => {
|
||||||
.then(() => {
|
try {
|
||||||
|
await connectActiveProfile();
|
||||||
|
if (cancelledRef.current) return;
|
||||||
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||||
setDbReady(true);
|
setDbReady(true);
|
||||||
})
|
} catch (err) {
|
||||||
.catch((err) => {
|
if (cancelledRef.current) return;
|
||||||
|
console.error(`Failed to connect profile (attempt ${attempt}/${MAX_RETRIES}):`, err);
|
||||||
|
if (attempt < MAX_RETRIES) {
|
||||||
|
await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
|
||||||
|
if (!cancelledRef.current) return attemptConnect(attempt + 1);
|
||||||
|
} else {
|
||||||
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||||
console.error("Failed to connect profile:", err);
|
|
||||||
setStartupError(err instanceof Error ? err.message : String(err));
|
setStartupError(err instanceof Error ? err.message : String(err));
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
attemptConnect(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
cancelledRef.current = true;
|
||||||
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||||
};
|
};
|
||||||
}, [activeProfile, isLoading, connectActiveProfile, t]);
|
}, [activeProfile, isLoading, connectActiveProfile, t]);
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,32 @@ export interface LogEntry {
|
||||||
type LogListener = () => void;
|
type LogListener = () => void;
|
||||||
|
|
||||||
const MAX_ENTRIES = 500;
|
const MAX_ENTRIES = 500;
|
||||||
|
const STORAGE_KEY = "simpl-resultat-logs";
|
||||||
const logs: LogEntry[] = [];
|
const logs: LogEntry[] = [];
|
||||||
const listeners = new Set<LogListener>();
|
const listeners = new Set<LogListener>();
|
||||||
|
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
|
|
||||||
|
function loadFromStorage() {
|
||||||
|
try {
|
||||||
|
const stored = sessionStorage.getItem(STORAGE_KEY);
|
||||||
|
if (stored) {
|
||||||
|
const parsed: LogEntry[] = JSON.parse(stored);
|
||||||
|
logs.push(...parsed);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore corrupted storage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveToStorage() {
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(logs));
|
||||||
|
} catch {
|
||||||
|
// ignore quota errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function addEntry(level: LogLevel, args: unknown[]) {
|
function addEntry(level: LogLevel, args: unknown[]) {
|
||||||
const message = args
|
const message = args
|
||||||
.map((a) => {
|
.map((a) => {
|
||||||
|
|
@ -31,6 +52,7 @@ function addEntry(level: LogLevel, args: unknown[]) {
|
||||||
logs.splice(0, logs.length - MAX_ENTRIES);
|
logs.splice(0, logs.length - MAX_ENTRIES);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveToStorage();
|
||||||
listeners.forEach((fn) => fn());
|
listeners.forEach((fn) => fn());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,6 +60,8 @@ export function initLogCapture() {
|
||||||
if (initialized) return;
|
if (initialized) return;
|
||||||
initialized = true;
|
initialized = true;
|
||||||
|
|
||||||
|
loadFromStorage();
|
||||||
|
|
||||||
const origLog = console.log.bind(console);
|
const origLog = console.log.bind(console);
|
||||||
const origWarn = console.warn.bind(console);
|
const origWarn = console.warn.bind(console);
|
||||||
const origError = console.error.bind(console);
|
const origError = console.error.bind(console);
|
||||||
|
|
@ -64,6 +88,7 @@ export function getLogs(): readonly LogEntry[] {
|
||||||
|
|
||||||
export function clearLogs() {
|
export function clearLogs() {
|
||||||
logs.length = 0;
|
logs.length = 0;
|
||||||
|
saveToStorage();
|
||||||
listeners.forEach((fn) => fn());
|
listeners.forEach((fn) => fn());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue