7.1 KiB
BanGUI — Task List
This document breaks the entire BanGUI project into development stages, ordered so that each stage builds on the previous one. Every task is described in prose with enough detail for a developer to begin work. References point to the relevant documentation.
Reference: Docs/Refactoring.md for full analysis of each issue.
Open Issues
TASK-001 — WorldMap: filter companion table by selected country (server-side)
Status: Done
Priority: Medium
Domain: Full-stack (backend + frontend)
References: Docs/Features.md §4, Docs/Web-Development.md
Background
The GET /api/dashboard/bans/by-country endpoint always returns the 200 most recent ban rows in bans (constant _MAX_COMPANION_BANS = 200 in backend/app/services/ban_service.py). MapPage.tsx stores a selectedCountry state and filters the returned rows client-side via visibleBans. This means the companion table can only show the fraction of a country's bans that fall within the global top-200 window. If the selected time range has, say, 1 500 bans and 300 are from China, but China's bans are not all in the top 200 overall, the table will silently display fewer than 300 rows.
When a country is selected the companion table must return the complete set of bans for that country so the user sees an accurate picture.
Desired behaviour
- No country selected → companion table shows the 200 most recent bans across all countries (existing behaviour, no change).
- Country selected → the server returns all ban entries for that country in the selected time window; no client-side row-count cap applies.
- Deselecting a country (clicking the same country again, or the "Clear filter" button) reverts to the default 200-row unfiltered view.
- The existing
visibleBansclient-side filter inMapPage.tsxcan remain as a defensive guard but must not be the only filter.
Implementation steps
-
Backend — router (
backend/app/routers/dashboard.py)- Add
country_code: str | None = Query(default=None, description="ISO alpha-2 country code to filter companion rows.")toget_bans_by_country. - Pass it to
ban_service.bans_by_country(..., country_code=country_code).
- Add
-
Backend — service (
backend/app/services/ban_service.py)- Add
country_code: str | None = Nonekeyword argument tobans_by_country. - After
geo_mapis built (existing geo-resolution step), collect IPs whose resolved country matchescountry_code. - For the fail2ban source: call
fail2ban_db_repo.get_currently_bannedwithip_filter=matched_ipsand nolimit(remove the_MAX_COMPANION_BANScap for filtered queries). - For the archive source: filter
all_rowsto those whose IP is inmatched_ipsand return all of them (skip thepage_size=_MAX_COMPANION_BANScall). - When
country_codeisNone, behaviour is identical to today.
- Add
-
Backend — repository (
backend/app/repositories/fail2ban_db_repo.py)- Add
ip_filter: list[str] | None = Nonetoget_currently_banned. - When provided and non-empty, append
AND ip IN ({placeholders})to the SQLWHEREclause, parameterised safely (never interpolated as a string).
- Add
-
Backend — repository (archive) (
backend/app/repositories/history_archive_repo.py)- Similarly add optional
ip_filterto the archive companion-rows query used frombans_by_country.
- Similarly add optional
-
Frontend — API client (
frontend/src/api/map.ts)- Add optional
countryCode?: stringparameter tofetchBansByCountry. - When set, append
country_code=<value>to the query string.
- Add optional
-
Frontend — hook (
frontend/src/hooks/useMapData.ts)- Add
countryCode?: stringto the function signature. - Include it in the
useCallbackdependency array and pass it tofetchBansByCountry.
- Add
-
Frontend — page (
frontend/src/pages/MapPage.tsx)- Pass
selectedCountry ?? undefinedascountryCodetouseMapData. - The hook's effect will re-fetch automatically when
selectedCountrychanges; the existinguseEffectthat resetspageto 1 already covers this.
- Pass
Testing guidance
- Select a country that has > 200 bans in the chosen time window; confirm the companion table shows more than the previous cap would allow.
- With no country selected, confirm only 200 rows are returned (no regression).
- Deselect the country; confirm the unfiltered 200-row view is restored.
- Test with the archive source as well as the fail2ban live source.
- Verify the
ip_filterSQL clause is parameterised and cannot be injected.
TASK-002 — WorldMap: sticky table header and sticky pagination bar
Priority: Low
Domain: Frontend only
References: Docs/Features.md §4, Docs/Web-Design.md, Docs/Web-Development.md
Background
The companion ban table in MapPage.tsx is wrapped in tableWrapper (CSS overflow: auto; maxHeight: 420px). Both the Fluent UI TableHeader row and the .pagination div inside tableWrapper scroll with the content. Once the user scrolls more than a few rows, the column header labels disappear and the pagination controls become unreachable without scrolling back to the top or bottom.
Desired behaviour
- The column header row (
TableHeader →TableRow → TableHeaderCell × 6) must remain fixed at the top of the scrollable container at all times. - The pagination / page-size bar (
.paginationdiv at the bottom oftableWrapper) must remain fixed at the bottom of the scrollable container at all times. - Rows in
TableBodyscroll normally between the two fixed ends. - No changes to the container height, overall layout, or other pages.
Implementation steps
All changes are in frontend/src/pages/MapPage.tsx.
-
Sticky table header cells
- In
useStyles(makeStyles), add a new class:stickyHeaderCell: { position: "sticky", top: 0, zIndex: 1, backgroundColor: tokens.colorNeutralBackground1, boxShadow: `0 1px 0 ${tokens.colorNeutralStroke2}`, }, - Apply
className={styles.stickyHeaderCell}to eachTableHeaderCellin the header row. - Note:
position: stickyon<tr>elements is unreliable across browsers for table layouts; apply it to each<th>(TableHeaderCell) instead.
- In
-
Sticky pagination bar
- In the existing
paginationentry inuseStyles, add:position: "sticky", bottom: 0, zIndex: 1, - The existing
backgroundColor: tokens.colorNeutralBackground2already prevents table rows from bleeding through.
- In the existing
-
No other changes — do not alter
tableWrapper, its height, or anything outsideMapPage.tsx.
Testing guidance
- Load the Map page with a time range that produces > 25 bans (enough to overflow the
420pxcontainer). - Scroll down through the table and confirm the column headers remain visible at the top.
- Scroll down and confirm the pagination bar remains visible at the bottom.
- Verify no visual artefacts (table body rows must not overlap or bleed through the sticky elements).
- Run
tsc --noEmit— zero type errors expected. - Run existing frontend tests:
vitest run— no regressions.