feat(frontend): dedupe /public/settings via shared usePublicSettings hook (#325)

Pre-dedup: 7 calls to /api/public/settings on a single /admin/login page
load — 4 from raw-fetch consumers + 3 from React Query consumers using
inconsistent queryKeys. Captured live in Chrome DevTools.

Post-dedup: 1 call. Verified by tests/e2e/public-settings-dedup.spec.ts.

Adds:
- frontend/src/hooks/usePublicSettings.ts — single React Query hook,
  60s staleTime, queryKey ['public-settings']. Vitest with mocked api
  proves multi-mount dedup.
- Extended PublicSettings interface with seo_meta_* fields used by
  RobotsMetaTags and branding_logo_* fields used by GalleryView/AdminHeader.

Migrates 19 call sites across 4 risk-ordered rounds:
- Round A (high fan-in): GlobalThemeProvider, MaintenanceContext (with
  refetchInterval to preserve maintenance polling), MaintenanceWrapper
  (drops the now-redundant per-route ping; axios interceptor already
  handles 503), AdminHeader.
- Round B (gallery/login): GalleryView, GalleryPage, ClientAccessPage,
  AdminLoginPage, MaintenanceMode.
- Round C (decorative): RobotsMetaTags, DynamicFavicon, CMSContentBlock,
  ReCaptcha, useWatermarkSettings (rips out raw fetch + local state),
  LegalPage.
- Round D (queryKey alignment): useLocalizedDate, UserPhotoUpload,
  CreateEventPage. EventDetailsPage Round D ships in the follow-up
  commit that adds presigned-download UI on the same page.

App.tsx (AnalyticsBootstrap) and EventDetailsPage are deferred to
later commits — both files mix #325 changes with backend feature work.
This commit is contained in:
Paul Nothaft
2026-04-28 10:01:53 +02:00
parent 46bc894d91
commit 3d4ae4d7e9
23 changed files with 258 additions and 271 deletions
+6 -31
View File
@@ -1,6 +1,5 @@
import React, { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { MaintenanceMode } from './MaintenanceMode';
import { useMaintenanceMode } from '../contexts/MaintenanceContext';
import { setMaintenanceModeCallback, api } from '../config/api';
@@ -9,12 +8,16 @@ interface MaintenanceWrapperProps {
children: React.ReactNode;
}
// Maintenance detection now lives in two places:
// 1. The axios interceptor in config/api.ts flips the flag on any 503 response.
// 2. MaintenanceContext polls /public/settings every 30s and reads the explicit
// maintenance_mode field (via the shared usePublicSettings hook).
// This wrapper only needs to gate the rendered tree on the resulting state.
export const MaintenanceWrapper: React.FC<MaintenanceWrapperProps> = ({ children }) => {
const location = useLocation();
const { isMaintenanceMode, setMaintenanceMode } = useMaintenanceMode();
const [hasAdminSession, setHasAdminSession] = useState(false);
// Check if current route is admin route
const isAdminRoute = location.pathname.startsWith('/admin');
useEffect(() => {
@@ -45,40 +48,12 @@ export const MaintenanceWrapper: React.FC<MaintenanceWrapperProps> = ({ children
};
}, [isAdminRoute]);
// Register the maintenance mode callback
useEffect(() => {
setMaintenanceModeCallback((enabled: boolean) => {
setMaintenanceMode(enabled);
});
}, [setMaintenanceMode]);
// Check maintenance mode on mount and when location changes
useQuery({
queryKey: ['maintenance-check', location.pathname],
queryFn: async () => {
try {
// Make a lightweight request to check maintenance status
await api.get('/public/settings');
// If successful, maintenance mode is off
setMaintenanceMode(false);
return { maintenance: false };
} catch (error: any) {
if (error.response?.status === 503) {
// Only set maintenance mode for non-admin routes or unauthenticated admin routes
if (!isAdminRoute || !hasAdminSession) {
setMaintenanceMode(true);
return { maintenance: true };
}
}
return { maintenance: false };
}
},
staleTime: 30000, // Check every 30 seconds
retry: false, // Don't retry on failure
enabled: (!isAdminRoute || !hasAdminSession) && !isMaintenanceMode, // Don't check if already in maintenance
});
// Show maintenance page if in maintenance mode and not on admin route with auth
if (isMaintenanceMode && (!isAdminRoute || !hasAdminSession)) {
return <MaintenanceMode />;
}