Task 1: Remove ActiveBansSection from JailsPage
- Delete buildBanColumns, fmtTimestamp, ActiveBansSection
- Remove Dialog/Delete/Dismiss imports, ActiveBan type
- Update JSDoc to reflect three sections
Task 2: Remove JailDistributionChart from Dashboard
- Delete import and JSX block from DashboardPage.tsx
Task 3: Fix transparent pie chart (TopCountriesPieChart)
- Add Cell import and per-slice <Cell fill={slice.fill}> children inside <Pie>
- Suppress @typescript-eslint/no-deprecated (recharts v3 types)
Task 4: Allow /config/log as safe log prefix
- Add '/config/log' to _SAFE_LOG_PREFIXES in config_service.py
- Update error message to list both allowed directories
Task 5: Block jail activation on missing filter/logpath
- activate_jail refuses to proceed when filter/logpath issues found
- ActivateJailDialog treats all validation issues as blocking
- Trigger immediate _run_probe after activation in config router
- /api/health now reports fail2ban online/offline from cached probe
- Add TestActivateJailBlocking tests; fix existing tests to mock validation
14 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.
Task 1 — Remove "Currently Banned IPs" section from the Jails page
Page: /jails — rendered by frontend/src/pages/JailsPage.tsx
The Jails page currently has four sections: Jail Overview, Ban / Unban IP, Currently Banned IPs, and IP Lookup. Remove the "Currently Banned IPs" section entirely.
What to do
- In
frontend/src/pages/JailsPage.tsx, find theActiveBansSectionsub-component (the function that renders the "Currently Banned IPs" heading, the DataGrid of active bans, the refresh/clear-all buttons, and the confirmation dialog). Delete the entireActiveBansSectionfunction. - In the
JailsPagecomponent at the bottom of the same file, remove the<ActiveBansSection />JSX element from the returned markup. - Remove any imports, hooks, types, or helper functions that were only used by
ActiveBansSectionand are now unused (e.g.useActiveBansif it is no longer referenced elsewhere, thebuildBanColumnshelper, theActiveBantype import, etc.). Check the remaining code —useActiveBansis also destructured forbanIp/unbanIpinJailsPage, so keep it if still needed there. - Update the file-level JSDoc comment at the top of the file: change the list from four sections to three and remove the "Currently Banned IPs" bullet.
- Update the JSDoc on the
JailsPagecomponent to say "three sections" instead of "four sections".
Verification
npx tsc --noEmitpasses with no errors.npx eslint src/pages/JailsPage.tsxreports no warnings.- The
/jailspage renders without the "Currently Banned IPs" section; the remaining three sections (Jail Overview, Ban/Unban IP, IP Lookup) still work.
Task 2 — Remove "Jail Distribution" section from the Dashboard
Page: / (Dashboard) — rendered by frontend/src/pages/DashboardPage.tsx
The Dashboard currently shows: Server Status Bar, Filter Bar, Ban Trend, Top Countries, Jail Distribution, and Ban List. Remove the "Jail Distribution" section.
What to do
- In
frontend/src/pages/DashboardPage.tsx, delete the entire{/* Jail Distribution section */}block — this is the<div className={styles.section}>that wraps the<JailDistributionChart>component (approximately lines 164–177). - Remove the
JailDistributionChartimport at the top of the file (import { JailDistributionChart } from "../components/JailDistributionChart";). - If the
JailDistributionChartcomponent file (frontend/src/components/JailDistributionChart.tsx) is not imported anywhere else in the codebase, you may optionally delete it, but this is not required.
Verification
npx tsc --noEmitpasses with no errors.npx eslint src/pages/DashboardPage.tsxreports no warnings.- The Dashboard renders without the "Jail Distribution" chart; all other sections remain functional.
Task 3 — Fix transparent pie chart in "Top Countries" on the Dashboard
Page: / (Dashboard) — pie chart rendered by frontend/src/components/TopCountriesPieChart.tsx
The pie chart in the "Top Countries" section is transparent (invisible slices). The root cause is that each slice's fill colour is set on the data objects but the <Pie> component does not apply them. Recharts needs either a fill prop on <Pie>, per-slice <Cell> elements, or the data items' fill field to be picked up correctly.
What to do
- Open
frontend/src/components/TopCountriesPieChart.tsx. - The
buildSliceshelper already attaches a resolvedfillcolour string to everySliceDataitem. However, the<Pie>element does not render individual<Cell>elements to apply those colours. - Import
Cellfromrecharts:import { Cell, Legend, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts"; - Inside the
<Pie>element, add child<Cell>elements that map each slice to its colour:This ensures each pie slice is painted with the colour from<Pie data={slices} dataKey="value" nameKey="name" cx="50%" cy="50%" outerRadius={90} label={…} labelLine={false} > {slices.map((slice, index) => ( <Cell key={index} fill={slice.fill} /> ))} </Pie>CHART_PALETTEthatbuildSlicesresolved.
Verification
npx tsc --noEmitpasses with no errors.- The pie chart on the Dashboard now displays coloured slices (blue, red, green, gold, purple) matching the Fluent palette.
- The legend and tooltip still work and show correct country names and percentages.
Task 4 — Fix log viewer rejecting fail2ban log path under /config/log
Error: API error 400: {"detail":"Log path '/config/log/fail2ban/fail2ban.log' is outside the allowed directory. Only paths under /var/log are permitted."}
Root cause: The linuxserver/fail2ban Docker image writes its own log to /config/log/fail2ban/fail2ban.log (this is configured via logtarget in Docker/fail2ban-dev-config/fail2ban/fail2ban.conf). In the Docker Compose setup, the /config volume is shared between the fail2ban and backend containers, so the file exists and is readable. However, the backend's read_fail2ban_log function in backend/app/services/config_service.py hard-codes the allowed path prefix list as:
_SAFE_LOG_PREFIXES: tuple[str, ...] = ("/var/log",)
This causes any log target under /config/log/ to be rejected with a 400 error.
What to do
- Open
backend/app/services/config_service.py. - Find the
_SAFE_LOG_PREFIXESconstant (line ~771). Add"/config/log"as a second allowed prefix:This is safe because:_SAFE_LOG_PREFIXES: tuple[str, ...] = ("/var/log", "/config/log")/config/logis a volume mount controlled by the Docker Compose setup, not user input.- The path is still validated via
Path.resolve()to prevent traversal (e.g./config/log/../../etc/shadowresolves outside and is rejected).
- Update the error message on the next line (inside
read_fail2ban_log) to reflect both allowed directories:"Only paths under /var/log or /config/log are permitted." - Update the existing test
test_path_outside_safe_dir_raises_operation_errorinbackend/tests/test_services/test_config_service.py— it currently patches_SAFE_LOG_PREFIXESto("/var/log",)in the assertion, so it will still pass. Verify no other tests break.
Verification
python -m pytest backend/tests/test_services/test_config_service.py -qpasses.- The log viewer page in the UI successfully loads the fail2ban log file at
/config/log/fail2ban/fail2ban.logwithout a 400 error.
Task 5 — Harden jail activation: block on missing logpaths and improve post-activation health checks
Problem description
Activating a jail whose logpath references a non-existent file (e.g. airsonic-auth.conf referencing a log file that doesn't exist) causes fail2ban to crash entirely. All previously running jails go down (Active Jails drops from 2 → 0). The GUI still shows "Service Health: Running" because:
- The backend
/api/healthendpoint (backend/app/routers/health.py) only checks that the FastAPI process itself is alive — it does not probe fail2ban. So Docker health checks pass even when fail2ban is dead. - The background health probe runs every 30 seconds. If the user checks the dashboard during the window between the crash and the next probe, the cached status is stale (still shows "Running").
- The pre-activation validation (
_validate_jail_config_syncinbackend/app/services/config_file_service.py) treats missing log paths as warnings only (field="logpath"issues). TheActivateJailDialogfrontend component filters these as "advisory" and does not block activation. This means a jail with a non-existent log file is activated, causing fail2ban to crash on reload.
What to do
Part A — Block activation when log files don't exist (backend)
File: backend/app/services/config_file_service.py
-
In
_validate_jail_config_sync()(around line 715–723), change the log path existence check from a warning to an error for literal, non-glob paths. Currently it appends aJailValidationIssuewithfield="logpath"but the function still returnsvalid=Trueif these are the only issues. Instead, treat a missing logpath as a blocking validation failure — thevalidfield at the bottom (line ~729) already useslen(issues) == 0, so if the issue is appended it will setvalid=False.The current code already does this correctly — the issue is in
activate_jail()itself. Find the post-validation block (around line 1130–1138) wherewarningsare collected. Currently activation always proceeds regardless of validation result. Change this: -
In
activate_jail(), after running_validate_jail_config_sync, checkvalidation_result.valid. If it isFalseand any issue hasfieldin("filter", "logpath"), refuse to activate. Return aJailActivationResponsewithactive=False,fail2ban_running=True, and a descriptivemessagelisting the blocking issues. This prevents writingenabled = trueand reloading fail2ban with a known-bad config.# Block activation on critical validation failures (missing filter or logpath). blocking = [i for i in validation_result.issues if i.field in ("filter", "logpath")] if blocking: log.warning("jail_activation_blocked", jail=name, issues=[str(i) for i in blocking]) return JailActivationResponse( name=name, active=False, fail2ban_running=True, validation_warnings=[f"{i.field}: {i.message}" for i in validation_result.issues], message=( f"Jail {name!r} cannot be activated: " + "; ".join(i.message for i in blocking) ), )
Part B — Block activation in the frontend dialog
File: frontend/src/components/config/ActivateJailDialog.tsx
Currently blockingIssues is computed by filtering out logpath issues (line ~175):
const blockingIssues = validationIssues.filter((i) => i.field !== "logpath");
Change this so that logpath issues are blocking too:
const blockingIssues = validationIssues.filter(
(i) => i.field !== "logpath" || i.message.includes("not found"),
);
Or simply remove the logpath exclusion entirely so all validation issues block:
const blockingIssues = validationIssues; // all issues block activation
const advisoryIssues: JailValidationIssue[] = []; // nothing is advisory anymore
The "Activate" button should remained disabled when blockingIssues.length > 0 (this logic already exists).
Part C — Run an immediate health probe after activation (backend)
File: backend/app/routers/config.py — activate_jail endpoint (around line 640–660)
After config_file_service.activate_jail() returns, trigger an immediate health check so the cached status is updated right away (instead of waiting up to 30 seconds):
# Force an immediate health probe to refresh cached status.
from app.tasks.health_check import _run_probe
await _run_probe(request.app)
Add this right after the last_activation recording block (around line 653), before the return result. This ensures the dashboard immediately reflects the current fail2ban state.
Part D — Include fail2ban liveness in /api/health endpoint
File: backend/app/routers/health.py
The current health endpoint always returns {"status": "ok"}. Enhance it to also report fail2ban status from the cached probe:
@router.get("/health", summary="Application health check")
async def health_check(request: Request) -> JSONResponse:
cached: ServerStatus = getattr(
request.app.state, "server_status", ServerStatus(online=False)
)
return JSONResponse(content={
"status": "ok",
"fail2ban": "online" if cached.online else "offline",
})
Keep the HTTP status code as 200 so Docker health checks don't restart the backend container when fail2ban is down. But having "fail2ban": "offline" in the response allows monitoring and debugging.
Tests to add or update
File: backend/tests/test_services/test_config_file_service.py
-
Add test:
test_activate_jail_blocked_when_logpath_missing— mock_validate_jail_config_syncto returnvalid=Falsewith alogpathissue. Assertactivate_jail()returnsactive=Falseandfail2ban_running=Truewithout callingreload_all. -
Add test:
test_activate_jail_blocked_when_filter_missing— same pattern but with afilterissue. -
Add test:
test_activate_jail_proceeds_when_only_regex_warnings— mock validation with a non-blockingfailregexissue and assert activation still proceeds.
File: backend/tests/test_routers/test_config.py
- Add test:
test_activate_returns_400_style_response_on_missing_logpath— POST to/api/config/jails/{name}/activatewith a jail that has a missing logpath. Assert the response body hasactive=Falseand contains the logpath error message.
File: backend/tests/test_tasks/test_health_check.py
- Existing tests should still pass —
_run_probebehavior is unchanged.
Verification
-
Run backend tests:
.venv/bin/python -m pytest backend/tests/ -q --tb=shortAll tests pass with no failures.
-
Run frontend type check and lint:
cd frontend && npx tsc --noEmit && npx eslint src/components/config/ActivateJailDialog.tsx -
Manual test with running server:
- Go to
/config, find a jail with a non-existent logpath (e.g.airsonic-auth). - Click "Activate" — the dialog should show a blocking error about the missing log file and the Activate button should be disabled.
- Verify that fail2ban is still running with the original jails intact (Active Jails count unchanged).
- Go to dashboard — "Service Health" should correctly reflect the live fail2ban state.
- Go to