feat: add forgejo_error_reporter plugin for automatic error reporting to Forgejo issues
This commit is contained in:
+48
-12
@@ -5,27 +5,62 @@ import { setUnauthorizedHandler } from '@/api/client';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
import { ErrorBoundary } from '@/components/common/ErrorBoundary';
|
||||
import { useToast } from '@/components/ui/Toast';
|
||||
import { useOnlineStatus } from '@/hooks/useOnlineStatus';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: 1,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
function QueryClientWrapper({ children }: { children: React.ReactNode }) {
|
||||
const toast = useToast();
|
||||
|
||||
const [queryClient] = React.useState(() => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: 1,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
mutations: {
|
||||
retry: 0,
|
||||
onError: (error: any) => {
|
||||
// Don't show toast for 401 (handled by auth)
|
||||
if (error?.status === 401) return;
|
||||
const msg = error?.detail || error?.message || 'Ein Fehler ist aufgetreten';
|
||||
toast.error(msg);
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function OfflineBanner() {
|
||||
const isOnline = useOnlineStatus();
|
||||
|
||||
if (isOnline) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed top-0 left-0 right-0 z-[200] bg-warning-500 text-white text-center py-2 px-4 text-sm font-medium shadow-md">
|
||||
Sie sind offline. Änderungen werden gespeichert wenn die Verbindung wiederhergestellt ist.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const { logout } = useAuthStore();
|
||||
const loadThemeFromStorage = useThemeStore((s) => s.loadFromStorage);
|
||||
const toast = useToast();
|
||||
|
||||
React.useEffect(() => {
|
||||
setUnauthorizedHandler(() => {
|
||||
toast.warning('Ihre Sitzung ist abgelaufen. Sie werden zur Anmeldung weitergeleitet.');
|
||||
logout();
|
||||
window.location.href = '/login';
|
||||
setTimeout(() => { window.location.href = '/login'; }, 1500);
|
||||
});
|
||||
}, [logout]);
|
||||
}, [logout, toast]);
|
||||
|
||||
// Load theme from localStorage on app start
|
||||
React.useEffect(() => {
|
||||
@@ -33,10 +68,11 @@ export default function App() {
|
||||
}, [loadThemeFromStorage]);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<QueryClientWrapper>
|
||||
<OfflineBanner />
|
||||
<ErrorBoundary>
|
||||
<AppRouter />
|
||||
</ErrorBoundary>
|
||||
</QueryClientProvider>
|
||||
</QueryClientWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -364,9 +364,14 @@ export function useAIUIControl() {
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
ws.onclose = (event) => {
|
||||
setConnected(false);
|
||||
console.log('AI UI Control WebSocket disconnected');
|
||||
// Don't reconnect on 403 (forbidden) — user lacks permission
|
||||
if (event.code === 3003 || event.code === 1008) {
|
||||
console.warn('AI UI Control WebSocket closed with forbidden code — not reconnecting');
|
||||
return;
|
||||
}
|
||||
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), maxReconnectDelay);
|
||||
reconnectAttempts++;
|
||||
reconnectTimeout.current = window.setTimeout(connect, delay);
|
||||
|
||||
@@ -98,8 +98,13 @@ export function useCommWebSocket() {
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
ws.onclose = (event) => {
|
||||
console.log('Comm WebSocket disconnected');
|
||||
// Don't reconnect on 403 (forbidden) — user lacks permission
|
||||
if (event.code === 3003 || event.code === 1008) {
|
||||
console.warn('Comm WebSocket closed with forbidden code — not reconnecting');
|
||||
return;
|
||||
}
|
||||
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), maxReconnectDelay);
|
||||
reconnectAttempts++;
|
||||
reconnectTimeout.current = window.setTimeout(connect, delay);
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export function useOnlineStatus() {
|
||||
const [isOnline, setIsOnline] = useState(navigator.onLine);
|
||||
|
||||
useEffect(() => {
|
||||
const goOnline = () => setIsOnline(true);
|
||||
const goOffline = () => setIsOnline(false);
|
||||
window.addEventListener('online', goOnline);
|
||||
window.addEventListener('offline', goOffline);
|
||||
return () => {
|
||||
window.removeEventListener('online', goOnline);
|
||||
window.removeEventListener('offline', goOffline);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return isOnline;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
export type ErrorCategory = 'network' | 'permission' | 'validation' | 'server' | 'auth' | 'unknown';
|
||||
|
||||
export interface CategorizedError {
|
||||
category: ErrorCategory;
|
||||
status: number;
|
||||
message: string;
|
||||
detail?: string;
|
||||
code?: string;
|
||||
field?: string;
|
||||
}
|
||||
|
||||
export function categorizeError(error: any): CategorizedError {
|
||||
const status = error?.status || 0;
|
||||
if (status === 0) return { category: 'network', status: 0, message: 'Netzwerkfehler', detail: error?.message };
|
||||
if (status === 401) return { category: 'auth', status, message: 'Nicht authentifiziert', detail: error?.detail };
|
||||
if (status === 403) return { category: 'permission', status, message: 'Keine Berechtigung', detail: error?.detail };
|
||||
if (status === 422) return { category: 'validation', status, message: 'Validierungsfehler', detail: error?.detail, field: error?.field };
|
||||
if (status >= 500) return { category: 'server', status, message: 'Serverfehler', detail: error?.detail };
|
||||
return { category: 'unknown', status, message: error?.message || 'Unbekannter Fehler', detail: error?.detail };
|
||||
}
|
||||
Reference in New Issue
Block a user