This commit is contained in:
2026-04-05 21:46:25 +02:00
parent eb983799cd
commit c03a5c1cbc
5 changed files with 56 additions and 117 deletions

View File

@@ -8,128 +8,71 @@ Reference: `Docs/Refactoring.md` for full analysis of each issue.
## Open Issues ## Open Issues
### Replace `react-simple-maps` with `d3-geo` in WorldMap ### Backend Architecture
The current `WorldMap` component (`frontend/src/components/WorldMap.tsx`) uses the `react-simple-maps` library (`ComposableMap`, `ZoomableGroup`, `Geography`, `useGeographies`). This library wraps d3-geo but adds a heavy abstraction layer and fetches the TopoJSON geography file from a remote CDN at runtime. Replace it with direct d3-geo rendering, following the pattern demonstrated in the reference project at `/media/lukas/Volume/repo/worldmaptest/`. - **Replace the single shared SQLite connection.**
- Current startup code opens one `aiosqlite.Connection` and reuses it for every request.
- This should be replaced with either a connection pool or request-scoped connections to avoid concurrency and locking issues.
- Update request dependencies, application lifecycle, and tests to use the new pattern.
Reference: `Docs/Features.md` §4 (World Map View) for the full feature specification. - **Refactor dependency wiring and shared resource management.**
- Remove hidden module-level import coupling between routers, services, and shared utilities.
- Introduce explicit factories or providers for shared resources such as DB, HTTP client session, scheduler, and settings.
- Ensure routers depend on injected providers rather than global state or dynamic imports.
**All existing features must be preserved.** The component's public API (`WorldMapProps`) and behaviour must remain identical so that `MapPage.tsx`, `HistoryPage.tsx`, and the existing unit test continue to work after the migration. - **Harden fail2ban integration.**
- Remove the `sys.path` hack that locates `fail2ban-master` at runtime.
- Replace it with a deterministic packaging or configuration model so the backend does not depend on repository layout.
- Refactor `Fail2BanClient` so concurrency control is instance-based and not backed by hidden module globals.
--- - **Improve startup / setup guard behavior.**
- Convert `SetupRedirectMiddleware` from an on-demand DB check into a startup/initialisation guard where possible.
- Cache setup completion in a safe way and provide an explicit invalidation path if the application state changes.
- Reduce middleware responsibility and avoid DB access during normal request dispatch.
#### Task 1 — Swap npm dependencies [DONE] - **Make deployment configuration explicit.**
- Move hard-coded environment assumptions such as CORS origins into settings.
- Ensure `fail2ban_socket`, `fail2ban_config_dir`, and startup commands are fully configurable via `Settings`.
- Document production-ready defaults separately from development defaults.
Remove `react-simple-maps` and `@types/react-simple-maps` from `frontend/package.json`. Add the following packages that the new implementation requires: ### Reliability and Resilience
- `d3-geo` — geographic projection and SVG path generation. - **Add backend lifecycle tests for resource cleanup.**
- `@types/d3-geo` — TypeScript definitions for d3-geo. - Verify startup opens and initialises DB, HTTP session, scheduler, and geo cache correctly.
- `topojson-client` — converts TopoJSON to GeoJSON `FeatureCollection`. - Verify shutdown closes those resources cleanly.
- `@types/topojson-client` — TypeScript definitions for topojson-client.
- `world-atlas` — provides the `countries-110m.json` TopoJSON file as a local npm asset (no more CDN fetch at runtime).
Run `npm install` and verify the lock file updates cleanly. - **Add concurrency/regression coverage for DB and fail2ban socket use.**
- Add tests that simulate multiple concurrent requests using the same DB dependency.
- Add tests around fail2ban socket retries, protocol errors, and rate limiting.
--- - **Improve state caching and invalidation.**
- Add tests for session cache invalidation on logout.
- Add tests for setup completion caching so stale state is never served.
#### Task 2 — Rewrite `WorldMap.tsx` to use d3-geo directly [DONE] ### Backend Feature Work
Rewrite the component so that it renders a plain `<svg>` with `<path>` elements generated by d3-geo instead of the react-simple-maps wrappers. The implementation should follow this approach (as seen in the reference project): - **Document and implement backend-safe environment-driven CORS.**
- Add support for production and local development origins through configuration.
- Avoid a hardcoded Vite origin in the core app factory.
1. **Import the TopoJSON locally**`import worldData from "world-atlas/countries-110m.json"` instead of fetching from a CDN URL. Use `topojson-client`'s `feature()` to extract the GeoJSON `FeatureCollection` once (memoised). - **Centralise scheduler job registration.**
- Refactor APScheduler registration so background tasks are registered through a common lifecycle helper.
- Ensure jobs can be discovered, replaced, and tested without requiring implicit `app.state` side effects.
2. **Create a projection** — Use `geoMercator()` from d3-geo (matching the current Mercator projection) with `.fitSize([width, height], featureCollection)` to auto-scale. Memoise the projection so it is only recomputed when the geometry changes. - **Strengthen fail2ban error handling and reporting.**
- Standardise `502` responses for connection/protocol failures across all endpoints.
- Add structured logging for retries and fatal socket failures.
- Ensure the UI can distinguish offline fail2ban from internal backend failures.
3. **Create a path generator**`geoPath().projection(projection)`. Memoise. - **Improve documentation of backend responsibilities.**
- Keep `Docs/Tasks.md` aligned with the backend architecture review.
- Add references to the backend modules, resource lifecycle, and dependency model in the documentation.
4. **Render countries** — Map over the GeoJSON features and render a `<path>` element for each country. Use the `ISO_NUMERIC_TO_ALPHA2` lookup (already exists in `frontend/src/data/isoNumericToAlpha2.ts`) to translate the numeric feature id to the alpha-2 code expected by the `countries` prop. ### Priority Execution Plan
5. **Preserve colour coding** — Continue using `getBanCountColor()` from `frontend/src/utils/mapColors.ts` to compute each country's fill colour based on its ban count and the three threshold props. 1. Fix the global SQLite connection pattern and tests.
2. Refactor dependency injection / explicit shared resources.
6. **Preserve ban-count labels** — For every country with `count > 0`, compute the centroid with `pathGenerator.centroid(feature)` and render a `<text>` element at that position showing the count. Countries with zero bans must remain blank and transparent (no fill, no label). 3. Harden fail2ban client concurrency and packaging.
4. Convert setup guard to a safer startup-driven model.
7. **Preserve country selection** — Clicking a country calls `onSelectCountry` with the alpha-2 code (or `null` to deselect). The selected country must receive a distinct brand fill colour, matching the current behaviour. 5. Add deployment-safe configuration and production-ready CORS.
6. Add lifecycle and concurrency regression tests.
8. **Preserve hover tooltip** — On `mouseenter` / `mousemove` / `mouseleave`, show/hide a tooltip portal (`createPortal` into `document.body`) displaying the country name and ban count. Use the same Fluent UI styled tooltip div that the current implementation uses.
9. **Preserve keyboard accessibility** — Each country with a known alpha-2 code must have `role="button"`, `tabIndex={0}`, an `aria-label` (`"CC: N ban(s)"`), and `aria-pressed` when selected. `Enter` and `Space` must trigger selection/deselection.
10. **Use a `viewBox`-based responsive SVG** — Set `viewBox="0 0 {width} {height}"` and `style={{ width: "100%", height: "auto" }}` so the map scales with its container, matching the reference project's approach.
---
#### Task 3 — Implement zoom and pan without `react-simple-maps` [DONE]
The current implementation relies on `ZoomableGroup` from react-simple-maps for zoom/pan. Reimplement this using a `<g>` wrapper with an SVG `transform` attribute driven by React state:
1. **State:** Track `zoom` (number, 18) and `center` (translate offset `[x, y]`).
2. **Zoom controls:** Keep the three overlay buttons (Zoom In `+`, Zoom Out ``, Reset `⟲`) in the top-right corner. Each button adjusts the `zoom` state by ±0.5, clamped to `[1, 8]`. Reset sets zoom to 1 and center to `[0, 0]`.
3. **Mouse-wheel zoom:** Attach a `wheel` event handler to the SVG that increments/decrements zoom on scroll, zooming toward the cursor position.
4. **Click-and-drag pan:** Track `mousedown``mousemove``mouseup` on the SVG to translate the `center` offset. Only pan when the drag exceeds a small threshold (e.g. 3 px) to avoid conflicting with country click events.
5. **Touch support (stretch goal):** Optionally support pinch-to-zoom and touch-drag for tablet users.
6. **Apply transform:** Wrap all `<path>` and `<text>` elements in a `<g transform="translate(tx, ty) scale(zoom)">` group. Alternatively, use `d3-zoom` if a more robust implementation is preferred, but keep React as the rendering layer (no d3 DOM manipulation).
---
#### Task 4 — Update hover and selection styles to use CSS transitions [DONE]
The reference project applies hover highlights via CSS classes (`.country`, `.country.hovered`) with CSS `transition` instead of the react-simple-maps `style={{ default, hover, pressed }}` object. Adopt the same approach:
- Define CSS classes (or Fluent UI `makeStyles` rules) for default, hovered, and selected states.
- Apply the correct class based on component state (`isSelected`, `isHovered`).
- Use a CSS `transition` on `fill` and `stroke` for a smooth 150 ms highlight effect.
- This avoids the react-simple-maps per-geography style object entirely.
Ensure the selected state still uses `tokens.colorBrandBackground` / `tokens.colorBrandBackgroundHover` / `tokens.colorBrandBackgroundPressed` from Fluent UI so the map integrates visually with the rest of the application.
---
#### Task 5 — Update the WorldMap unit test [DONE]
The existing test at `frontend/src/components/__tests__/WorldMap.test.tsx` mocks `react-simple-maps`. After the migration those mocks are invalid. Update the test:
1. **Remove the `vi.mock("react-simple-maps", ...)` block.**
2. **Mock the TopoJSON data instead.** Since the new implementation imports `world-atlas/countries-110m.json` directly, mock that module to return a minimal TopoJSON object containing a single country feature (e.g. id `"840"` for the US). Use `topojson-client`'s `feature()` to verify the mock produces a valid GeoJSON feature.
3. **Keep the same assertions:** tooltip appears on hover with country name and ban count, tooltip disappears on mouse leave, country element has correct ARIA attributes (`role="button"`, `aria-label`, `aria-pressed`).
4. **Verify zoom controls render:** assert that the three zoom buttons (Zoom In, Zoom Out, Reset) are present and have the correct `aria-label` values.
5. Also verify that tests in `MapPage.test.tsx` and `HistoryPage.test.tsx` still pass (they mock `WorldMap` at the component level so they should be unaffected, but confirm).
---
#### Task 6 — Remove CDN dependency and verify offline capability [DONE]
The old implementation fetched geography data from `https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json` at runtime. The new implementation bundles the data via the `world-atlas` npm package, so:
1. Delete the `GEO_URL` constant.
2. Confirm the TopoJSON file is included in the Vite bundle (imported as a JSON module).
3. Verify the map renders correctly without any network request for geography data (check the browser network tab or write a test that asserts no fetch calls are made for the old CDN URL).
---
#### Task 7 — Final integration smoke test [DONE]
After all changes, manually verify the following against the feature specification in `Docs/Features.md` §4:
- Countries are colour-coded by ban count (transparent → green → yellow → red) using smooth interpolation.
- Ban count numbers are displayed centred inside each country that has bans.
- Countries with zero bans are transparent with no label.
- Clicking a country filters the companion ban table below.
- Clicking the same country again deselects it.
- Zoom in / zoom out / reset buttons work correctly (range 1×8×).
- Mouse-wheel zoom and click-drag pan work.
- Tooltip appears on hover showing country name and localised ban count.
- Keyboard navigation works (Tab to focus, Enter/Space to toggle selection).
- The map is responsive and scales with the container width.
- Time-range selector on `MapPage` still updates the map data correctly.
- Colour thresholds from settings are applied (thresholdLow, thresholdMedium, thresholdHigh props).
- Run `npm run test` — all existing tests pass.
- Run `npm run build` — production build succeeds with no errors or warnings.

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "bangui-backend" name = "bangui-backend"
version = "0.9.15" version = "0.9.18"
description = "BanGUI backend — fail2ban web management interface" description = "BanGUI backend — fail2ban web management interface"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = [ dependencies = [

View File

@@ -1,12 +1,12 @@
{ {
"name": "bangui-frontend", "name": "bangui-frontend",
"version": "0.9.15", "version": "0.9.17",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "bangui-frontend", "name": "bangui-frontend",
"version": "0.9.15", "version": "0.9.17",
"dependencies": { "dependencies": {
"@fluentui/react-components": "^9.55.0", "@fluentui/react-components": "^9.55.0",
"@fluentui/react-icons": "^2.0.257", "@fluentui/react-icons": "^2.0.257",

View File

@@ -346,10 +346,6 @@ export function WorldMap({
return null; return null;
} }
const centroid = pathGenerator.centroid(featureItem);
const [cx, cy] = centroid;
const isCentroidValid = Number.isFinite(cx) && Number.isFinite(cy);
return ( return (
<g key={String(rawId)}> <g key={String(rawId)}>
<path <path

View File

@@ -488,7 +488,7 @@ export function HistoryPage(): React.JSX.Element {
setIpFilter(value); setIpFilter(value);
}} }}
/> />
</div>
{/* ---------------------------------------------------------------- */} {/* ---------------------------------------------------------------- */}
{/* Summary */} {/* Summary */}