From 83b2cb67b192e47e96b74e8958d75af543535845 Mon Sep 17 00:00:00 2001 From: Lukas Date: Wed, 20 May 2026 20:18:58 +0200 Subject: [PATCH] backup --- backend/app/main.py | 26 ++++++++++++++----- frontend/src/hooks/usePolledData.ts | 40 +++++++++++++++++++++-------- 2 files changed, 48 insertions(+), 18 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index 91e750d..c0a0ed6 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -242,9 +242,9 @@ async def _lifespan(app: FastAPI) -> AsyncGenerator[None, None]: # deployments, it should be replaced with a shared backend. _update_session_cache(app, settings) - # Initialize the global rate limiter (200 requests per 60 seconds per IP). + # Initialize the global rate limiter (600 requests per 60 seconds per IP). # Applied to all endpoints via middleware. Process-local implementation. - app.state.global_rate_limiter = GlobalRateLimiter(max_requests=200, window_seconds=60) + app.state.global_rate_limiter = GlobalRateLimiter(max_requests=600, window_seconds=60) log.info("bangui_started") @@ -1095,10 +1095,10 @@ def create_app(settings: Settings | None = None) -> FastAPI: if resolved_settings.session_cache_enabled and resolved_settings.session_cache_ttl_seconds > 0.0 else NoOpSessionCache() ) - # Initialize the global rate limiter (200 requests per 60 seconds per IP). + # Initialize the global rate limiter (600 requests per 60 seconds per IP). # This is also re-initialized in the lifespan, but must be present here # for tests that bypass the lifespan via ASGITransport. - app.state.global_rate_limiter = GlobalRateLimiter(max_requests=200, window_seconds=60) + app.state.global_rate_limiter = GlobalRateLimiter(max_requests=600, window_seconds=60) set_setup_complete_cache(app, False) @@ -1161,13 +1161,25 @@ def create_app(settings: Settings | None = None) -> FastAPI: path_prefixes=["/api/v1/history"], ) - # Global rate limiter for all other endpoints. - # 200 req/min per IP — default protection. + # Polling endpoints (blocklist schedule) get a dedicated bucket + # to avoid exhausting the global limit during normal frontend operation. app.add_middleware( RateLimitMiddleware, rate_limiter=app.state.global_rate_limiter, settings=resolved_settings, - skip_paths=["/api/v1/auth/login", "/api/v1/setup", "/api/v1/history"], + bucket_override="polling:read", + bucket_max_requests=10000, + bucket_window_seconds=60, + path_prefixes=["/api/v1/blocklists/schedule"], + ) + + # Global rate limiter for all other endpoints. + # 600 req/min per IP — default protection. + app.add_middleware( + RateLimitMiddleware, + rate_limiter=app.state.global_rate_limiter, + settings=resolved_settings, + skip_paths=["/api/v1/auth/login", "/api/v1/setup", "/api/v1/history", "/api/v1/blocklists/schedule"], ) # Validate middleware order before returning the app. diff --git a/frontend/src/hooks/usePolledData.ts b/frontend/src/hooks/usePolledData.ts index cea7cc0..d2d5de6 100644 --- a/frontend/src/hooks/usePolledData.ts +++ b/frontend/src/hooks/usePolledData.ts @@ -77,11 +77,34 @@ export function usePolledData( pauseWhenHidden = false, } = options; + // Stabilize fetcher/selector/onSuccess references so that useFetchData's + // refresh callback (and the useEffect that calls it) don't re-trigger on + // every render when callers pass inline functions. + const fetcherRef = useRef(fetcher); + fetcherRef.current = fetcher; + const selectorRef = useRef(selector); + selectorRef.current = selector; + const onSuccessRef = useRef(onSuccess); + onSuccessRef.current = onSuccess; + + const stableFetcher = useCallback( + (signal: AbortSignal) => fetcherRef.current(signal), + [], + ); + const stableSelector = useCallback( + (response: TResponse) => selectorRef.current(response), + [], + ); + const stableOnSuccess = useCallback( + (response: TResponse) => onSuccessRef.current?.(response), + [], + ); + const { data, loading, error, refresh } = useFetchData({ - fetcher, - selector, + fetcher: stableFetcher, + selector: stableSelector, errorMessage, - onSuccess, + onSuccess: onSuccessRef.current ? stableOnSuccess : undefined, initialData, }); @@ -151,15 +174,10 @@ export function usePolledData( return; } - // Record when polling starts and schedule first poll immediately + // Record when polling starts. The initial fetch is handled by useFetchData's + // mount effect, so we just mark the start time and let the loading-completion + // effect (above) schedule the first poll after the initial fetch finishes. pollStartTimeRef.current = performance.now(); - const id = window.setTimeout((): void => { - if (cancelledRef.current) return; - pollStartTimeRef.current = performance.now(); - refreshRef.current?.(); - }, 0); - - timeoutIdRef.current = id; return (): void => { cancelledRef.current = true;