Stage 11: polish, cross-cutting concerns & hardening

- 11.1 MainLayout health indicator: warning MessageBar when fail2ban offline
- 11.2 formatDate utility + TimezoneProvider + GET /api/setup/timezone
- 11.3 Responsive sidebar: auto-collapse <640px, media query listener
- 11.4 PageFeedback (PageLoading/PageError/PageEmpty), BanTable updated
- 11.5 prefers-reduced-motion: disable sidebar transition
- 11.6 WorldMap ARIA: role/tabIndex/aria-label/onKeyDown for countries
- 11.7 Health transition logging (fail2ban_came_online/went_offline)
- 11.8 Global handlers: Fail2BanConnectionError/ProtocolError -> 502
- 11.9 379 tests pass, 82% coverage, ruff+mypy+tsc+eslint clean
- Timezone endpoint: setup_service.get_timezone, 5 new tests
This commit is contained in:
2026-03-01 15:59:06 +01:00
parent 1efa0e973b
commit 1cdc97a729
19 changed files with 649 additions and 45 deletions

View File

@@ -0,0 +1,95 @@
/**
* TimezoneProvider.
*
* Fetches the IANA timezone configured during the BanGUI setup wizard and
* makes it available throughout the component tree via React Context.
*
* The timezone is fetched once at mount. On error or before the initial
* fetch resolves, the value defaults to ``"UTC"`` so date-formatting callers
* always receive a safe fallback.
*/
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import { fetchTimezone } from "../api/setup";
// ---------------------------------------------------------------------------
// Context definition
// ---------------------------------------------------------------------------
interface TimezoneContextValue {
/** IANA timezone string, e.g. ``"Europe/Berlin"`` or ``"UTC"``. */
timezone: string;
}
const TimezoneContext = createContext<TimezoneContextValue>({ timezone: "UTC" });
// ---------------------------------------------------------------------------
// Provider
// ---------------------------------------------------------------------------
export interface TimezoneProviderProps {
children: React.ReactNode;
}
/**
* Wrap the application (or authenticated shell) with this provider to make the
* configured timezone available via {@link useTimezone}.
*
* @example
* ```tsx
* <TimezoneProvider>
* <App />
* </TimezoneProvider>
* ```
*/
export function TimezoneProvider({
children,
}: TimezoneProviderProps): React.JSX.Element {
const [timezone, setTimezone] = useState<string>("UTC");
const load = useCallback((): void => {
fetchTimezone()
.then((resp) => { setTimezone(resp.timezone); })
.catch(() => {
// Silently fall back to UTC; the backend may not be reachable yet.
});
}, []);
useEffect(() => {
load();
}, [load]);
const value = useMemo<TimezoneContextValue>(() => ({ timezone }), [timezone]);
return (
<TimezoneContext.Provider value={value}>{children}</TimezoneContext.Provider>
);
}
// ---------------------------------------------------------------------------
// Hook
// ---------------------------------------------------------------------------
/**
* Return the IANA timezone string configured during setup.
*
* Must be used inside a {@link TimezoneProvider}.
*
* @returns The configured timezone, e.g. ``"Europe/Berlin"``.
*
* @example
* ```tsx
* const { timezone } = useTimezone();
* const label = formatDate(item.created_at, timezone);
* ```
*/
export function useTimezone(): string {
return useContext(TimezoneContext).timezone;
}