feature/ignore-self-toggle #1

Merged
lukas.pupkalipinski merged 97 commits from feature/ignore-self-toggle into main 2026-03-14 21:19:28 +01:00
No description provided.
lukas.pupkalipinski added 97 commits 2026-03-14 21:17:57 +01:00
Backend (tasks 1.1, 1.5–1.8):
- pyproject.toml with FastAPI, Pydantic v2, aiosqlite, APScheduler 3.x,
  structlog, bcrypt; ruff + mypy strict configured
- Pydantic Settings (BANGUI_ prefix env vars, fail-fast validation)
- SQLite schema: settings, sessions, blocklist_sources, import_log;
  WAL mode + foreign keys; idempotent init_db()
- FastAPI app factory with lifespan (DB, aiohttp session, scheduler),
  CORS, unhandled-exception handler, GET /api/health
- Fail2BanClient: async Unix-socket wrapper using run_in_executor,
  custom error types, async context manager
- Utility modules: ip_utils, time_utils, constants
- 47 tests; ruff 0 errors; mypy --strict 0 errors

Frontend (tasks 1.2–1.4):
- Vite + React 18 + TypeScript strict; Fluent UI v9; ESLint + Prettier
- Custom brand theme (#0F6CBD, WCAG AA contrast) with light/dark variants
- Typed fetch API client (ApiError, get/post/put/del) + endpoints constants
- tsc --noEmit 0 errors
Backend (tasks 2.1–2.6, 2.10):
- settings_repo: get/set/delete/get_all CRUD for the key-value settings table
- session_repo: create/get/delete/delete_expired for session rows
- setup_service: bcrypt password hashing, one-time-only enforcement,
  run_setup() / is_setup_complete() / get_password_hash()
- auth_service: login() with bcrypt verify + token creation,
  validate_session() with expiry check, logout()
- setup router: GET /api/setup (status), POST /api/setup (201 / 409)
- auth router: POST /api/auth/login (token + HttpOnly cookie),
               POST /api/auth/logout (clears cookie, idempotent)
- SetupRedirectMiddleware: 307 → /api/setup for all API paths until setup done
- require_auth dependency: cookie or Bearer token → Session or 401
- conftest.py: manually bootstraps app.state.db for router tests
  (ASGITransport does not trigger ASGI lifespan)
- 85 tests pass; ruff 0 errors; mypy --strict 0 errors

Frontend (tasks 2.7–2.9):
- types/auth.ts, types/setup.ts, api/auth.ts, api/setup.ts
- AuthProvider: sessionStorage-backed context (isAuthenticated, login, logout)
- RequireAuth: guard component → /login?next=<path> when unauthenticated
- SetupPage: Fluent UI form, client-side validation, inline errors
- LoginPage: single password input, ?next= redirect after success
- DashboardPage: placeholder (full impl Stage 5)
- App.tsx: full route tree (/setup, /login, /, *)
Cover Python, Node, secrets, databases, OS artefacts, and editors.
Previous entry was minimal; this replaces it with a well-structured,
commented ignore file suitable for a full-stack Python/React project.
Adds a dedicated .gitignore for the FastAPI/Python backend covering
virtualenvs, build artefacts, test caches, type-checker output,
local SQLite databases, and secrets.
Adds a dedicated .gitignore for the React/Vite/TypeScript frontend
covering node_modules, build output, TypeScript incremental build
info, test coverage, and environment files.
- Add ban_service reading fail2ban SQLite DB via read-only aiosqlite
- Add geo_service resolving IPs via ip-api.com with 10k in-memory cache
- Add GET /api/dashboard/bans and GET /api/dashboard/accesses endpoints
- Add TimeRange, DashboardBanItem, DashboardBanListResponse, AccessListItem,
  AccessListResponse models in models/ban.py
- Build BanTable component (Fluent UI DataGrid) with bans/accesses modes,
  pagination, loading/error/empty states, and ban-count badges
- Build useBans hook managing time-range and pagination state
- Update DashboardPage: status bar + time-range toolbar + tab switcher
- Add 37 new backend tests (ban service, geo service, dashboard router)
- All 141 tests pass; ruff/mypy --strict/tsc --noEmit clean
- jail_service.py: list/detail/control/ban/unban/ignore-list/IP-lookup
- jails.py router: 11 endpoints including ignore list management
- bans.py router: active bans, ban, unban
- geo.py router: IP lookup with geo enrichment
- models: Jail.actions, ActiveBan.country/.banned_at optional, GeoDetail
- 217 tests pass (40 service + 36 router + 141 existing), 76% coverage
- Frontend: types/jail.ts, api/jails.ts, hooks/useJails.ts
- JailsPage: jail overview table with controls, ban/unban forms,
  active bans table, IP lookup
- JailDetailPage: full detail, start/stop/idle/reload, patterns,
  ignore list management
- config_service.py: read/write jail config via asyncio.gather, global
  settings, in-process regex validation, log preview via _read_tail_lines
- server_service.py: read/write server settings, flush logs
- config router: 9 endpoints for jail/global config, regex-test,
  logpath management, log preview
- server router: GET/PUT settings, POST flush-logs
- models/config.py expanded with JailConfig, GlobalConfigUpdate,
  LogPreview* models
- 285 tests pass (68 new), ruff clean, mypy clean (44 files)
- Frontend: types/config.ts, api/config.ts, hooks/useConfig.ts,
  ConfigPage.tsx full implementation (Jails accordion editor,
  Global config, Server settings, Regex Tester with preview)
- Fixed pre-existing frontend lint: JSX.Element → React.JSX.Element
  (10 files), void/promise patterns in useServerStatus + useJails,
  no-misused-spread in client.ts, eslint.config.ts self-excluded
- BansByCountryResponse model added to ban.py
- bans_by_country() service: parallel geo lookup via asyncio.gather,
  aggregation by ISO alpha-2 country code (up to 2 000 bans)
- GET /api/dashboard/bans/by-country endpoint in dashboard router
- 290 tests pass (5 new), ruff + mypy clean (44 files)
- isoNumericToAlpha2.ts: 249-entry ISO numeric → alpha-2 static map
- types/map.ts, api/map.ts, hooks/useMapData.ts created
- WorldMap.tsx: react-simple-maps Mercator SVG map, per-country ban
  count overlay, colour intensity scaling, country click filtering,
  GeoLayer nested-component pattern for useGeographies context
- MapPage.tsx: time-range selector, WorldMap, country filter info bar,
  summary line, companion FluentUI Table with country filter
- Frontend tsc + ESLint clean (0 errors/warnings)
- history.py models: HistoryBanItem, HistoryListResponse, IpTimelineEvent, IpDetailResponse
- history_service.py: list_history() with dynamic WHERE clauses (range/jail/ip
  prefix/all-time), get_ip_detail() with timeline aggregation
- history.py router: GET /api/history + GET /api/history/{ip} (404 for unknown)
- Fixed latent bug in ban_service._parse_data_json: json.loads('null') -> None
  -> AttributeError; now checks isinstance(parsed, dict) before assigning obj
- 317 tests pass (27 new), ruff + mypy clean (46 files)
- types/history.ts, api/history.ts, hooks/useHistory.ts created
- HistoryPage.tsx: filter bar (time range/jail/IP), DataGrid table,
  high-ban-count row highlighting, per-IP IpDetailView with timeline,
  pagination
- Frontend tsc + ESLint clean (0 errors/warnings)
- Tasks.md Stage 9 marked done
- blocklist_repo.py: CRUD for blocklist_sources table
- import_log_repo.py: add/list/get-last log entries
- blocklist_service.py: source CRUD, preview, import (download/validate/ban),
  import_all, schedule get/set/info
- blocklist_import.py: APScheduler task (hourly/daily/weekly schedule triggers)
- blocklist.py router: 9 endpoints (list/create/update/delete/preview/import/
  schedule-get+put/log)
- blocklist.py models: ScheduleFrequency (StrEnum), ScheduleConfig, ScheduleInfo,
  ImportSourceResult, ImportRunResult, PreviewResponse
- 59 new tests (18 repo + 19 service + 22 router); 374 total pass
- ruff clean, mypy clean for Stage 10 files
- types/blocklist.ts, api/blocklist.ts, hooks/useBlocklist.ts
- BlocklistsPage.tsx: source management, schedule picker, import log table
- Frontend tsc + ESLint clean
- 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
- Add SetupGuard component: redirects to /setup if setup not complete,
  shown as spinner while loading. All routes except /setup now wrapped.
- SetupPage redirects to /login on mount when setup already done.
- Fix async blocking: offload bcrypt.hashpw and bcrypt.checkpw to
  run_in_executor so they never stall the asyncio event loop.
- Hash password with SHA-256 (SubtleCrypto) before transmission; added
  src/utils/crypto.ts with sha256Hex(). Backend stores bcrypt(sha256).
- Add Makefile with make up/down/restart/logs/clean targets.
- Add tests: _check_password async, concurrent bcrypt, expired session,
  login-without-setup, run_setup event-loop interleaving.
- Update Architekture.md and Features.md to reflect all changes.
Vite runs inside the frontend container where 'localhost' resolves to
the container itself, not the backend.  Change the /api proxy target
from http://localhost:8000 to http://backend:8000 so the request is
routed to the backend service over the compose network.
The previous 'down -v' flag is unreliable across compose implementations.
Declare all four dev volumes (bangui-dev_*) and remove them explicitly
with 'podman/docker volume rm' so clean always leaves a pristine state.
clean now removes the locally-built backend image (bangui-dev_backend)
in addition to volumes, so the next 'make up' rebuilds from scratch
ensuring code changes land in the new image.
Public images (fail2ban, node:22-alpine) are left untouched.
Added standalone 'make build' target for explicit rebuilds.
- Add v7_startTransition and v7_relativeSplatPath future flags to
  BrowserRouter to silence React Router deprecation warnings
- Add hidden autocomplete='username' inputs to LoginPage and SetupPage
  so password managers and browsers stop warning about missing username
  fields in password forms
- Mount fail2ban-dev-config volume into backend container at /config:ro
  so ban_service can open the fail2ban SQLite database returned by
  'get dbfile'; this fixes the 500 on GET /api/dashboard/bans
- Track compose.debug.yml in git (was previously untracked)
react-simple-maps types declare path as always non-null, but it is
undefined during the initial render before the MapProvider context
has fully initialised. Guard with an early return after all hooks
have been called, casting through unknown to reflect the true runtime
type without triggering the @typescript-eslint/no-unnecessary-condition
rule.
In the Docker image, the app source is copied to /app/app/ (not
backend/app/), so parents[2] resolved to '/' instead of /app.
This left the fail2ban package absent from sys.path, causing every
pickle.loads() call on socket responses to raise:

  ModuleNotFoundError: No module named 'fail2ban'

Replace the hardcoded parents[2] with a walk-up search that iterates
over all ancestors until it finds a fail2ban-master/ sibling directory.
Works correctly in both local dev and Docker without environment-specific
path magic.
_is_not_found_error in jail_service did not match the concatenated form
'unknownjailexception' that fail2ban produces when it serialises
UnknownJailException, so JailOperationError was raised instead of
JailNotFoundError and every ban attempt in the import loop failed
individually, skipping all 27 840 IPs before returning an error.

Two changes:
- Add 'unknownjail' to the phrase list in _is_not_found_error so that
  UnknownJailException is correctly mapped to JailNotFoundError.
- In blocklist_service.import_source, catch JailNotFoundError explicitly
  and break out of the loop immediately with a warning log instead of
  retrying on every IP.
fetchAccesses was passing the hardcoded absolute path /api/dashboard/accesses
to get(), which prepends BASE_URL (/api), producing /api/api/dashboard/accesses.
Added ENDPOINTS.dashboardAccesses and switched to use it, consistent with every
other function in dashboard.ts.
- Add bangui-sim filter (filter.d/bangui-sim.conf) matching the
  simulated authentication failure log format
- Add bangui-sim jail (jail.d/bangui-sim.conf) with maxretry=3,
  bantime=60s, findtime=120s, ignoreip safeguard, polling backend
- Mount Docker/logs/ into fail2ban container at /remotelogs/bangui
  in compose.debug.yml
- Add simulate_failed_logins.sh to write synthetic failure lines
- Add check_ban_status.sh with optional --unban flag
- Add dev-ban-test Makefile target for one-command smoke testing
- Write Docker/fail2ban-dev-config/README.md with setup and
  troubleshooting docs
- Update .gitignore to track custom config files while still
  excluding auto-generated linuxserver fail2ban files
The backend container mounted fail2ban-dev-config as an anonymous named
volume, while the fail2ban container used a bind-mount of the same local
directory. The backend's /config was therefore always empty, causing
sqlite3.OperationalError when ban_service attempted to open the path
returned by 'get dbfile' (/config/fail2ban/fail2ban.sqlite3).

Change the backend volume declaration from the named volume reference
to the same bind-mount used by fail2ban:
  fail2ban-dev-config:/config:ro  →  ./fail2ban-dev-config:/config:ro

Also removes the now-unused 'fail2ban-dev-config' named-volume entry.

Affected endpoints (all returned HTTP 500, now return HTTP 200):
  GET /api/dashboard/bans
  GET /api/dashboard/accesses
  GET /api/dashboard/bans/by-country
Task 1 — fix Stop/Reload Jail returning 404
  Root cause: reload_jail and reload_all sent an empty config stream
  (["reload", name, [], []]).  In fail2ban's reload protocol the end-of-
  reload phase deletes every jail still in reload_state — i.e. every jail
  that received no configuration commands.  An empty stream means *all*
  affected jails are silently removed from the daemon's runtime, causing
  everything touching those jails afterwards (including stop) to receive
  UnknownJailException → HTTP 404.

  Fixes:
  - reload_jail: send ["start", name] in the config stream; startJail()
    removes the jail from reload_state so the end phase commits instead of
    deletes, and un-idles the jail.
  - reload_all: fetch current jail list first, build a ["start", name]
    entry for every active jail, then send reload --all with that stream.
  - stop_jail: made idempotent — if the jail is already gone (not-found
    error) the operation silently succeeds (200 OK) rather than returning
    404, matching the user expectation that stop = ensure-stopped.
  - Router: removed dead JailNotFoundError handler from stop endpoint.

  391 tests pass (2 new), ruff clean, mypy clean (pre-existing
  config.py error unchanged).

Task 2 — access list simulator
  - Docker/simulate_accesses.sh: writes fake HTTP-scan log lines in
    custom format (bangui-access: http scan from <IP> ...) to
    Docker/logs/access.log so the bangui-access jail detects them.
  - fail2ban/filter.d/bangui-access.conf: failregex matching the above.
  - fail2ban/jail.d/bangui-access.conf: polling jail on access.log,
    same settings as bangui-sim (maxretry=3, bantime=60s).
  - .gitignore: whitelist new bangui-access.conf files.
  - Docker/fail2ban-dev-config/README.md: added "Testing the Access
    List Feature" section with step-by-step instructions and updated
    Configuration Reference + Troubleshooting.
Two root causes:
1. Docker/compose.debug.yml volume mount ./Docker/logs was already
   correct (./logs) — no change needed there.
2. Docker/logs/access.log did not exist on first checkout because
   *.log is gitignored. fail2ban fails to start if the file is absent.

Fix: touch Docker/logs/access.log and auth.log in the Makefile 'up'
target so both stub files are always created before the stack starts,
regardless of whether they were previously generated by simulation scripts.
- Implement ban model, service, and router endpoints in backend
- Add ban table component and dashboard integration in frontend
- Update ban-related types and API endpoints
- Add comprehensive tests for ban service and dashboard router
- Update documentation (Features, Tasks, Architecture, Web-Design)
- Clean up old fail2ban configuration files
- Update Makefile with new commands
The blocklist import service targets a dedicated jail called
'blocklist-import' (BLOCKLIST_JAIL constant in blocklist_service.py),
but that jail was never defined in the dev fail2ban configuration.
Every import attempt immediately failed with UnknownJailException.

Add Docker/fail2ban-dev-config/fail2ban/jail.d/blocklist-import.conf:
a manual-ban jail with no log-based detection that accepts banip
commands only, using iptables-allports with a 1-week bantime.

Also track the new file in .gitignore (whitelist) and fix a
pre-existing blank-line-with-whitespace lint error in setup_service.py.
- Task 1: Mark imported blocklist IP addresses
  - Add BanOrigin type and _derive_origin() to ban.py model
  - Populate origin field in ban_service list_bans() and bans_by_country()
  - BanTable and MapPage companion table show origin badge column
  - Tests: origin derivation in test_ban_service.py and test_dashboard.py

- Task 2: Add origin filter to dashboard and world map
  - ban_service: _origin_sql_filter() helper; origin param on list_bans()
    and bans_by_country()
  - dashboard router: optional origin query param forwarded to service
  - Frontend: BanOriginFilter type + BAN_ORIGIN_FILTER_LABELS in ban.ts
  - fetchBans / fetchBansByCountry forward origin to API
  - useBans / useMapData accept and pass origin; page resets on change
  - BanTable accepts origin prop; DashboardPage adds segmented filter
  - MapPage adds origin Select next to time-range picker
  - Tests: origin filter assertions in test_ban_service and test_dashboard
- Add persistent geo_cache SQLite table (db.py)
- Rewrite geo_service: batch API (100 IPs/call), two-tier cache,
  no caching of failed lookups so they are retried
- Pre-warm geo cache from DB on startup (main.py lifespan)
- Rewrite bans_by_country: SQL GROUP BY ip aggregation + lookup_batch
  instead of 2000-row fetch + asyncio.gather individual calls
- Pre-warm geo cache after blocklist import (blocklist_service)
- Add 300ms debounce to useMapData hook to cancel stale requests
- Add perf benchmark asserting <2s for 10k bans
- Add seed_10k_bans.py script for manual perf testing
- Add 5-min negative cache (_neg_cache) so failing IPs are throttled
  rather than hammering the API on every request
- Add MaxMind GeoLite2 fallback (init_geoip / _geoip_lookup) that fires
  when ip-api fails; controlled by BANGUI_GEOIP_DB_PATH env var
- Fix lookup_batch bug: failed API results were stored in positive cache
- Add _persist_neg_entry: INSERT OR IGNORE into geo_cache with NULL
  country_code so re-resolve can find historically failed IPs
- Add POST /api/geo/re-resolve: clears neg cache, batch-retries all
  geo_cache rows with country_code IS NULL, returns resolved/total count
- BanTable + MapPage: wrap the country — placeholder in a Fluent UI
  Tooltip explaining the retry behaviour
- Add geoip2>=4.8.0 dependency; geoip_db_path config setting
- Tests: add TestNegativeCache (4), TestGeoipFallback (4), TestReResolve (4)
When the most recent scheduled import completed with errors, surface the
failure in the persistent app shell:
- A warning MessageBar appears at top of main content area
- An amber badge is rendered on the Blocklists sidebar nav item

Backend: add last_run_errors: bool | None to ScheduleInfo model and
populate it in get_schedule_info() from the latest import_log row.

Frontend: extend ScheduleInfo type, add useBlocklistStatus polling hook,
wire both indicators into MainLayout.

Tests: 3 new service tests + 1 new router test (433 total, all pass).
- Send fail2ban's `unban --all` command via new `unban_all_ips()` service
  function; returns the count of unbanned IPs
- Add `UnbanAllResponse` Pydantic model (message + count)
- Add `DELETE /api/bans/all` router endpoint; handles 502 on socket error
- Frontend: `bansAll` endpoint constant, `unbanAllBans()` API call,
  `UnbanAllResponse` type, `unbanAll` action in `useActiveBans` hook
- JailsPage: "Clear All Bans" button (visible when bans > 0) with a
  Fluent UI confirmation Dialog before executing the operation
- 7 new tests (3 service, 4 router); 440 total pass, 82% coverage
geoip2 is an optional dependency used only when a MaxMind mmdb path is
configured. Importing it unconditionally at module level caused the server
to crash on startup with ModuleNotFoundError when the package was absent
from the environment.

Move the imports under TYPE_CHECKING (for static analysis) and add lazy
local imports inside init_geoip() and _geoip_lookup() where geoip2 is
actually needed at runtime. The server now starts normally without a
MaxMind database, and geoip2 is loaded on demand if the feature is used.
list_bans() was calling geo_service.lookup() once per IP on the
page (e.g. 100 sequential HTTP requests), hitting the ip-api.com
free-tier single-IP limit of 45 req/min.  IPs beyond the ~45th
were added to the in-process negative cache (5 min TTL) and showed
as no country until the TTL expired.  The map endpoint never had
this problem because it used lookup_batch (100 IPs per POST).

Add http_session and app_db params to list_bans().  When
http_session is provided (production path), the entire page is
resolved in one lookup_batch() call instead of N individual ones.
The legacy geo_enricher callback is kept for test compatibility.
Update the dashboard router to use the batch path directly.

Adds 3 tests covering the batch geo path, failure resilience, and
http_session priority over geo_enricher.
- Remove per-IP db.commit() from _persist_entry() and _persist_neg_entry();
  add a single commit after the full lookup_batch() chunk loop instead.
  Reduces commits from ~5,200 to 1 per bans/by-country request.

- Remove db dependency from GET /api/dashboard/bans and
  GET /api/dashboard/bans/by-country; pass app_db=None so no SQLite
  writes occur during read-only requests.

- Add _dirty set to geo_service; _store() marks resolved IPs dirty.
  New flush_dirty(db) batch-upserts all dirty entries in one transaction.
  New geo_cache_flush APScheduler task flushes every 60 s so geo data
  is persisted without blocking requests.
- Cache setup_completed flag in app.state._setup_complete_cached after
  first successful is_setup_complete() call; all subsequent API requests
  skip the DB query entirely (one-way transition, cleared on restart).
- Add in-memory session token TTL cache (10 s) in require_auth; the second
  request with the same token within the window skips session_repo.get_session.
- Call invalidate_session_cache() on logout so revoked tokens are evicted
  immediately rather than waiting for TTL expiry.
- Add clear_session_cache() for test isolation.
- 5 new tests covering the cached fast-path for both optimisations.
- 460 tests pass, 83% coverage, zero ruff/mypy warnings.
- Install Recharts v3 as the project charting library
- Add chartTheme utility with Fluent UI v9 token resolution helper
  and a 5-colour categorical palette (resolves CSS vars at runtime)
- Add TopCountriesPieChart: top-4 + Other slice, Tooltip, Legend
- Add TopCountriesBarChart: horizontal top-20 bar chart
- Add useDashboardCountryData hook (wraps /api/dashboard/bans/by-country)
- Integrate both charts into DashboardPage in a responsive chartsRow
  (side-by-side on wide screens, stacked on narrow)
- All tsc --noEmit and eslint checks pass with zero warnings
Implement time-bucketed ban aggregation for dashboard trend charts:

- Add BanTrendBucket / BanTrendResponse Pydantic models and
  BUCKET_SECONDS / BUCKET_SIZE_LABEL / bucket_count helpers to ban.py
- Add ban_service.ban_trend(): queries fail2ban DB with SQL bucket
  grouping, fills zero-count buckets, respects origin filter
- Add GET /api/dashboard/bans/trend route in dashboard.py
- 20 new tests (10 service, 10 router); 480 total pass, 83% coverage
- ruff + mypy --strict clean
- Add BanTrendBucket / BanTrendResponse interfaces to types/ban.ts
- Add dashboardBansTrend endpoint constant to api/endpoints.ts
- Add fetchBanTrend() to api/dashboard.ts
- Create useBanTrend hook with abort-safe data fetching
- Create BanTrendChart: AreaChart with gradient fill, dynamic
  X-axis labels per range, custom tooltip, loading/error/empty states
- tsc --noEmit and ESLint pass with zero warnings
Add full-width 'Ban Trend' section card above the country charts.
BanTrendChart shares the existing timeRange / originFilter state;
loading, error, and empty states are handled inside the component.
- backend: GET /api/dashboard/bans/by-jail endpoint
  - JailBanCount + BansByJailResponse Pydantic models in ban.py
  - bans_by_jail() service function with origin filter support
  - Route added to dashboard router
  - 17 new tests (7 service, 10 router); full suite 497 passed, 83% coverage

- frontend: JailDistributionChart component
  - JailBanCount / BansByJailResponse types in types/ban.ts
  - dashboardBansByJail endpoint constant in api/endpoints.ts
  - fetchBansByJail() in api/dashboard.ts
  - useJailDistribution hook in hooks/useJailDistribution.ts
  - JailDistributionChart component (horizontal bar chart, Recharts)
  - DashboardPage: full-width Jail Distribution section below Top Countries
Task 6.1 - Consistent loading/error/empty states across all charts:
- Add ChartStateWrapper shared component with Spinner, error MessageBar
  + Retry button, and friendly empty message
- Expose reload() in useBanTrend, useJailDistribution,
  useDashboardCountryData hooks
- Update BanTrendChart and JailDistributionChart to use ChartStateWrapper
- Add empty state to TopCountriesBarChart and TopCountriesPieChart
- Replace manual loading/error logic in DashboardPage with ChartStateWrapper

Task 6.2 - Frontend tests (5 files, 20 tests):
- Install Vitest v4, jsdom, @testing-library/react, @testing-library/jest-dom
- Add vitest.config.ts (separate from vite.config.ts to avoid Vite v5/v7 clash)
- Add src/setupTests.ts with jest-dom matchers and ResizeObserver/matchMedia stubs
- Tests: ChartStateWrapper (7), BanTrendChart (4), JailDistributionChart (4),
  TopCountriesPieChart (2), TopCountriesBarChart (3)

Task 6.3 - Full QA:
- ruff: clean
- mypy --strict: 52 files, no issues
- pytest: 497 passed
- tsc --noEmit: clean
- eslint: clean (added test-file override for explicit-function-return-type)
- vite build: success
- Wrap long dict literal in test_geo_service.py across multiple lines (E501)
- Combine nested with statements in test_jail_service.py (SIM117)
- Add vitest.config.ts to tsconfig.node.json include so ESLint
  parserOptions.project resolves it correctly
- Create DashboardFilterBar component with time-range and origin-filter
  toggle-button groups in a single card row (Stage 7, Tasks 7.1–7.3)
- Integrate filter bar below ServerStatusBar in DashboardPage; remove
  filter toolbars from the Ban List section header (Task 7.2)
- Add 6 tests covering rendering, active-state reflection, and callbacks
- tsc --noEmit, eslint, npm run build, npm test all pass (27/27 tests)
All error-handling branches in app/routers/jails.py were previously
untested: every Fail2BanConnectionError (502) path, several
JailNotFoundError (404) and JailOperationError (409) paths, and the
toggle_ignore_self endpoint which had zero coverage.

Added 26 new test cases across three new test classes
(TestIgnoreIpEndpoints extended, TestToggleIgnoreSelf,
TestFail2BanConnectionErrors) covering every remaining branch.

- app/routers/jails.py: 61% → 100% line coverage
- Overall backend coverage: 83% → 85%
- Total test count: 497 → 523 (all pass)
- ruff check and mypy --strict clean
- Replace str(exc) with repr(exc) in lookup() and _batch_api_call()
  so exception class name is always present even for no-message errors
  (e.g. aiohttp.ServerDisconnectedError() whose str() is empty)
- Add exc_type=type(exc).__name__ field to network-error log events
  for easy structured-log filtering
- Move import aiohttp to runtime import; use aiohttp.ClientTimeout()
  instead of raw float, removing # type: ignore[arg-type] workarounds
- Add TestErrorLogging with 3 tests covering empty-message exceptions
The JailAccordionPanel previously allowed deleting log paths but
had no UI to add new ones. The backend endpoint, API helper, and
hook all existed; only the UI was missing.

Changes:
- ConfigPage.tsx: import addLogPath/AddLogPathRequest; add state
  (newLogPath, newLogPathTail, addingLogPath) and handleAddLogPath
  callback to JailAccordionPanel; render inline form below the
  log-path list with Input, Switch (tail/head), and labeled Add
  button that appends on success and surfaces errors inline.
- ConfigPageLogPath.test.tsx: 6 tests covering render, disabled
  state, enabled state, successful add, success feedback, and API
  error handling. All 33 frontend tests pass.
Task 4 (Better Jail Configuration) implementation:
- Add fail2ban_config_dir setting to app/config.py
- New file_config_service: list/view/edit/create jail.d, filter.d, action.d files
  with path-traversal prevention and 512 KB content size limit
- New file_config router: GET/PUT/POST endpoints for jail files, filter files,
  and action files; PUT .../enabled for toggle on/off
- Extend config_service with delete_log_path() and add_log_path()
- Add DELETE /api/config/jails/{name}/logpath and POST /api/config/jails/{name}/logpath
- Extend geo router with re-resolve endpoint; add geo_re_resolve background task
- Update blocklist_service with revised scheduling helpers
- Update Docker compose files with BANGUI_FAIL2BAN_CONFIG_DIR env var and
  rw volume mount for the fail2ban config directory
- Frontend: new Jail Files, Filters, Actions tabs in ConfigPage; file editor
  with accordion-per-file, editable textarea, save/create; add/delete log paths
- Frontend: types in types/config.ts; API calls in api/config.ts and api/endpoints.ts
- 63 new backend tests (test_file_config_service, test_file_config, test_geo_re_resolve)
- 6 new frontend tests in ConfigPageLogPath.test.tsx
- ruff, mypy --strict, tsc --noEmit, eslint: all clean; 617 backend tests pass
- Backend: Add BantimeEscalation + BantimeEscalationUpdate Pydantic models
  to app/models/config.py; add bantime_escalation field to Jail in jail.py
- Backend: jail_service.get_jail_detail() fetches 7 bantime.* socket commands
  (increment, factor, formula, multipliers, maxtime, rndtime, overalljails)
  and populates bantime_escalation on the returned Jail object
- Backend: config_service.get_jail_config() fetches same 7 commands;
  update_jail_config() writes escalation fields when provided
- Frontend: Add BantimeEscalation + BantimeEscalationUpdate interfaces to
  types/config.ts; extend JailConfig + JailConfigUpdate; extend Jail in
  types/jail.ts
- Frontend: JailDetailPage.tsx adds BantimeEscalationSection component that
  renders only when increment is enabled (shows factor, formula, multipliers,
  max_time, rnd_time, overall_jails)
- Frontend: ConfigPage.tsx JailAccordionPanel adds full escalation edit form
  (Switch for enable/disable, number inputs for factor/max_time/rnd_time,
  text inputs for formula/multipliers, Switch for overall_jails);
  handleSave includes bantime_escalation in the JailConfigUpdate payload
- Tests: Update ConfigPageLogPath.test.tsx mock to include bantime_escalation:null
- Docs: Mark Task 6 as DONE in Tasks.md
- Add use_dns and prefregex fields to JailConfig model (backend + frontend types)
- Add prefregex to JailConfigUpdate; validate as regex before writing
- Fetch usedns and prefregex in get_jail_config via asyncio.gather
- Write usedns and prefregex in update_jail_config
- ConfigPage JailAccordionPanel: editable date_pattern input, dns_mode
  Select dropdown (yes/warn/no/raw), and prefregex input
- 8 new service unit tests + 3 new router integration tests
- 628 tests pass; 85% line coverage; ruff/mypy/tsc/eslint clean
- tests/test_tasks/test_blocklist_import.py: 14 tests, 96% coverage
- tests/test_tasks/test_health_check.py: 12 tests, 100% coverage
- tests/test_tasks/test_geo_cache_flush.py: 8 tests, 100% coverage
- tests/test_services/test_fail2ban_client.py: 24 new tests, 96% coverage

Total: 50 new tests (628 → 678 passing). Overall coverage 85% → 87%.
ruff, mypy --strict, tsc, and eslint all clean.
- Add conffile_parser.py: reads, writes and manipulates fail2ban .conf
  files while preserving comments and section structure
- Extend config models with ActionConfig, FilterConfig, ConfFileContent,
  and related Pydantic schemas for jails, actions, and filters
- file_config_service.py: service layer for reading, writing, and
  validating fail2ban conf files (jail.local, action.d/*, filter.d/*)
- file_config.py: REST router exposing GET/PUT endpoints for conf-file
  contents, sections, and key-value pairs; supports jails, actions,
  and filters
- test_conffile_parser.py: unit tests for section/key parsing, comment
  preservation, and round-trip write correctness
- test_file_config_service.py: service-level tests with mock filesystem
- test_file_config.py: router integration tests covering GET / PUT
  endpoints for jails, actions, and filters
- types/config.ts: TypeScript interfaces for ActionConfig, FilterConfig,
  JailFileConfig, ConfFileContent, and related request/response shapes
- api/config.ts: typed API functions for reading and writing conf files
- api/endpoints.ts: add /config/file/* endpoint constants
- useJailFileConfig: manages jail.local section state with dirty tracking
- useActionConfig: manages action .conf file state
- useFilterConfig: manages filter .conf file state
- useAutoSave: debounced auto-save with status indicator support
- Extract tab components: JailsTab, ActionsTab, FiltersTab, JailFilesTab,
  GlobalTab, ServerTab, ConfFilesTab, RegexTesterTab, MapTab, ExportTab
- Add form components: JailFileForm, ActionForm, FilterForm
- Add AutoSaveIndicator, RegexList, configStyles, and barrel index
- ConfigPage now composes these components; greatly reduces file size
- Add tests: ConfigPage.test.tsx, useAutoSave.test.ts
- .github/agents/ProcessTasks.agent.md: Copilot agent definition
- eslint.config.ts: minor lint rule adjustment
- Docs/Tasks.md: update completed and in-progress task status
Add PUT endpoints for overwriting raw content of jail.d, filter.d, and
action.d config files. Mirrors the existing GET endpoints so the frontend
can show an editable raw-text view of each config file.
- ConfigListDetail: reusable two-pane master/detail layout (list + detail)
  with active/inactive badges, sorted active-first, keyboard navigation,
  and responsive collapse to Dropdown below 900 px
- RawConfigSection: collapsible raw-text editor with save/feedback for
  any config file, backed by configurable fetch/save callbacks
- useConfigActiveStatus: hook that derives active jail, filter, and action
  sets from the live jails list and jail config data
Replace Accordion-based config tabs with the new ConfigListDetail two-pane
layout. Each tab now shows a searchable list with active/inactive badges
(active items sorted first) on the left and a structured form editor with
a collapsible raw-text export section on the right.
- configStyles.ts: add listDetailRoot, listPane, listItem, listItemSelected,
  detailPane style slots
- index.ts: export ConfigListDetail and RawConfigSection
- api/config.ts: add writeFilterFile and writeActionFile API helpers
- setupTests.ts: add ResizeObserver and matchMedia mocks for Fluent UI v9
- ConfigPageLogPath.test.tsx: update to render inside FluentProvider
- Docs/Tasks.md: mark config view redesign task as complete
- Features.md §6: describe list/detail layout with active/inactive badges,
  active-first sort, and per-item collapsible raw config editing
- Architekture.md routers: add file_config.py router entry
- Architekture.md services: add file_config_service.py and conffile_parser.py
- Architekture.md components: add ConfigListDetail, RawConfigSection,
  AutoSaveIndicator
- Architekture.md hooks: add useConfigActiveStatus, useFilterConfig,
  useActionConfig, useJailFileConfig, useAutoSave
- Architekture.md API layer: expand config.ts entry with full function list
- Backend: config_file_service.py parses jail.conf/jail.local/jail.d/*
  following fail2ban merge order; discovers jails not running in fail2ban
- Backend: 3 new API endpoints (GET /jails/inactive, POST /jails/{name}/activate,
  POST /jails/{name}/deactivate); moved /jails/inactive before /jails/{name}
  to fix route-ordering conflict
- Frontend: ActivateJailDialog component with optional parameter overrides
- Frontend: JailsTab extended with inactive jail list and InactiveJailDetail pane
- Frontend: JailsPage JailOverviewSection shows inactive jails with toggle
- Tests: 57 service tests + 16 router tests for all new endpoints (all pass)
- Docs: Features.md, Architekture.md, Tasks.md updated; Tasks 1.1-1.5 marked done
- Add list_filters() and get_filter() to config_file_service.py:
  scans filter.d/, parses [Definition] + [Init] sections, merges .local
  overrides, and cross-references running jails to set active/used_by_jails
- Add FilterConfig.active, used_by_jails, source_file, has_local_override
  fields to the Pydantic model; add FilterListResponse and FilterNotFoundError
- Add GET /api/config/filters and GET /api/config/filters/{name} to config.py
- Remove the shadowed GET /api/config/filters list route from file_config.py;
  rename GET /api/config/filters/{name} raw variant to /filters/{name}/raw
- Update frontend: fetchFilterFiles() adapts FilterListResponse -> ConfFilesResponse;
  add fetchFilters() and fetchFilter() to api/config.ts; remove unused
  fetchFilterFiles/fetchActionFiles calls from useConfigActiveStatus
- Fix ConfigPageLogPath test mock to include fetchInactiveJails and related
  exports introduced by Stage 1
- Backend: 169 tests pass, mypy --strict clean, ruff clean
- Frontend: 63 tests pass, tsc --noEmit clean, eslint clean
- PUT /api/config/filters/{name}: updates failregex/ignoreregex/datepattern/
  journalmatch in filter.d/{name}.local; validates regex via re.compile();
  supports ?reload=true
- POST /api/config/filters: creates filter.d/{name}.local from FilterCreateRequest;
  returns 409 if file already exists
- DELETE /api/config/filters/{name}: deletes .local only; returns 409 for
  conf-only (readonly) filters
- POST /api/config/jails/{name}/filter: assigns filter to jail by writing
  'filter = {name}' to jail.d/{jail}.local; supports ?reload=true
- New models: FilterUpdateRequest, FilterCreateRequest, AssignFilterRequest
- New service helpers: _safe_filter_name, _validate_regex_patterns,
  _write_filter_local_sync, _set_jail_local_key_sync
- Fixed .local-only filter discovery in _parse_filters_sync (5-tuple return)
- Fixed get_filter extension stripping (.local is 6 chars not 5)
- Renamed file_config.py raw-write routes to /raw suffix
  (PUT /filters/{name}/raw, POST /filters/raw) to avoid routing conflicts
- Full service + router tests; all 930 tests pass
- Rewrite FiltersTab: use fetchFilters() for FilterConfig[] with embedded active
  status; show 'Active — sshd, apache-auth' badge labels; FilterDetail sub-
  component with source_file/override badges, FilterForm, Assign button, raw
  config section
- New AssignFilterDialog: selects jail from enabled-jails list, calls
  POST /config/jails/{name}/filter with optional fail2ban reload
- New CreateFilterDialog: name+failregex+ignoreregex form, calls
  POST /config/filters, closes and selects new filter on success
- Extend ConfigListDetail: add listHeader (for Create button) and
  itemBadgeLabel (for custom badge text) optional props
- Fix updateFilterFile bug: was PUT /config/filters/{name} (structured
  endpoint), now correctly PUT /config/filters/{name}/raw
- Fix createFilterFile bug: was POST /config/filters, now POST /config/filters/raw
- Add updateFilter, createFilter, deleteFilter, assignFilterToJail to api/config.ts
- Add FilterUpdateRequest, FilterCreateRequest, AssignFilterRequest to types/config.ts
- Add configFiltersRaw, configJailFilter endpoints
- Tests: 24 new tests across FiltersTab, AssignFilterDialog, CreateFilterDialog
  (all 89 frontend tests passing)
- ActionConfig extended with active, used_by_jails, source_file, has_local_override
- New models: ActionListResponse, ActionUpdateRequest, ActionCreateRequest, AssignActionRequest
- New service functions: list_actions, get_action, update_action, create_action, delete_action, assign_action_to_jail, remove_action_from_jail
- New error classes: ActionNotFoundError, ActionAlreadyExistsError, ActionReadonlyError, ActionNameError
- New router endpoints: GET/PUT/POST/DELETE /api/config/actions, POST/DELETE /api/config/jails/{name}/action
- Service + router tests: 290 tests passing, mypy strict clean, ruff clean
- ActionsTab rewritten with master/detail layout (mirrors FiltersTab)
- New AssignActionDialog and CreateActionDialog components
- ActionConfig type extended with active, used_by_jails, source_file, has_local_override
- New API functions: fetchActions, fetchAction, updateAction, createAction, deleteAction, assignActionToJail, removeActionFromJail
- useActionConfig updated to use new structured endpoints
- index.ts barrel exports updated
- Fail2BanConfigParser class: merge order, include directives (before/after),
  variable interpolation %(var)s, split_multiline, ordered_conf_files
- config_writer: write_local_override, remove_local_key, delete_local_file
  with atomic writes (os.replace), per-file threading locks, .local-only guard
- 79 tests in tests/test_utils/ (all passing)
- mypy --strict: 60 source files, 0 errors
- ruff: all checks passed
- Remove Export tab and all its imports from ConfigPage.tsx
- Remove Refresh and Reload fail2ban buttons from JailsTab; clean up
  associated state (reloading, reloadMsg, deactivating) and handlers
- Add Create Config button to Jails tab list pane (listHeader pattern);
  create CreateJailDialog component that calls createJailConfigFile API
- Remove Active/Inactive and 'Has local override' badges from FilterDetail
  and ActionDetail; remove now-unused Badge imports
- Replace read-only log path spans with editable Input fields in JailConfigDetail
- Export CreateJailDialog from components/config/index.ts
- Mark all 5 tasks done in Docs/Tasks.md
Task 1: Backend/LogEncoding/DatePattern dropdowns in JailConfigDetail
- Added BACKENDS, LOG_ENCODINGS, DATE_PATTERN_PRESETS constants
- Backend and Log Encoding: <Input readOnly> → <Select> (editable, auto-saves)
- Date Pattern: <Input> → <Combobox freeform> with presets
- Extended JailConfigUpdate model (backend, log_encoding) and service
- Added readOnly prop to JailConfigDetail (all fields, toggles, buttons)
- Extended RegexList with readOnly prop

Task 2: Fix raw action/filter config always blank
- Added key={selectedAction.name} to ActionDetail in ActionsTab
- Added key={selectedFilter.name} to FilterDetail in FiltersTab

Task 3: Inactive jail full GUI same as active jails
- Extended InactiveJail Pydantic model with all config fields
- Added _parse_time_to_seconds helper to config_file_service
- Updated _build_inactive_jail to populate all extended fields
- Extended InactiveJail TypeScript type to match
- Rewrote InactiveJailDetail to reuse JailConfigDetail (readOnly=true)

Task 4: Fix banaction interpolation error when activating jails
- _write_local_override_sync now includes banaction=iptables-multiport
  and banaction_allports=iptables-allports in every .local file
Add alignItems: "end" to the fieldRow grid style so that all grid
cells align their content to the bottom edge of the row. This ensures
the DNS Mode <Select> and the Date Pattern <Combobox> sit on the same
horizontal baseline even though Date Pattern carries a hint line that
makes it taller.

All other fieldRow usages have consistent hint presence across their
fields, so no visual regressions are introduced.
Stage 1.1-1.3: reload_all include/exclude_jails params already implemented;
  added keyword-arg assertions in router and service tests.

Stage 2.1/6.1: _send_command_sync retry loop (3 attempts, 150ms exp backoff)
  retrying on EAGAIN/ECONNREFUSED/ENOBUFS; immediate raise on all other errors.

Stage 2.2: asyncio.Lock at module level in jail_service.reload_all to
  serialize concurrent reload--all commands.

Stage 3.1: activate_jail re-queries _get_active_jail_names after reload;
  returns active=False with descriptive message if jail did not start.

Stage 4.1/6.2: asyncio.Semaphore (max 10) in Fail2BanClient.send, lazy-
  initialized; logs fail2ban_command_waiting_semaphore at debug when waiting.

Stage 5.1/5.2: unit tests asserting reload_all is called with include_jails
  and exclude_jails; activation verification happy/sad path tests.

Stage 6.3: TestSendCommandSyncRetry (5 cases) + TestFail2BanClientSemaphore
  concurrency test.

Stage 7.1-7.3: _since_unix uses time.time(); bans_by_jail debug logging with
  since_iso; diagnostic warning when total==0 despite table rows; unit test
  verifying the warning fires for stale data.
The Jail page is now a pure operational view showing only jails that
fail2ban reports as active. The backend GET /api/jails already queried
only the fail2ban socket status command, so no backend changes were
needed.

Frontend changes:
- Remove Inactive Jails table, Show-inactive toggle, and all related
  state (showInactive, inactiveJails, activateTarget)
- Remove fetchInactiveJails() call and loadInactive/handleActivated
  callbacks
- Remove ActivateJailDialog import and usage
- Remove unused imports: useCallback, useEffect, Switch, InactiveJail

Inactive-jail discovery and activation remain fully functional via the
Configuration page Jails tab (JailsTab.tsx) — unchanged.
Task 2: adds a new Log tab to the Configuration page.

Backend:
- New Pydantic models: Fail2BanLogResponse, ServiceStatusResponse
  (backend/app/models/config.py)
- New service methods in config_service.py:
    read_fail2ban_log() — queries socket for log target/level, validates the
    resolved path against a safe-prefix allowlist (/var/log) to prevent
    path traversal, then reads the tail of the file via the existing
    _read_tail_lines() helper; optional substring filter applied server-side.
    get_service_status() — delegates to health_service.probe() and appends
    log level/target from the socket.
- New endpoints in routers/config.py:
    GET /api/config/fail2ban-log?lines=200&filter=...
    GET /api/config/service-status
  Both require authentication; log endpoint returns 400 for non-file log
  targets or path-traversal attempts, 502 when fail2ban is unreachable.

Frontend:
- New LogTab.tsx component:
    Service Health panel (Running/Offline badge, version, jail count, bans,
    failures, log level/target, offline warning banner).
    Log viewer with color-coded lines (error=red, warning=yellow,
    debug=grey), toolbar (filter input + debounce, lines selector, manual
    refresh, auto-refresh with interval selector), truncation notice, and
    auto-scroll to bottom on data updates.
  fetchData uses Promise.allSettled so a log-read failure never hides the
  service-health panel.
- Types: Fail2BanLogResponse, ServiceStatusResponse (types/config.ts)
- API functions: fetchFail2BanLog, fetchServiceStatus (api/config.ts)
- Endpoint constants (api/endpoints.ts)
- ConfigPage.tsx: Log tab added after existing tabs

Tests:
- Backend service tests: TestReadFail2BanLog (6), TestGetServiceStatus (2)
- Backend router tests: TestGetFail2BanLog (8), TestGetServiceStatus (3)
- Frontend: LogTab.test.tsx (8 tests)

Docs:
- Features.md: Log section added under Configuration View
- Architekture.md: config.py router and config_service.py descriptions updated
- Tasks.md: Task 2 marked done
- Backend: extend activate_jail() with pre-validation and 4-attempt post-reload
  health probe; add validate_jail_config() and rollback_jail() service functions
- Backend: new endpoints POST /api/config/jails/{name}/validate,
  GET /api/config/pending-recovery, POST /api/config/jails/{name}/rollback
- Backend: extend JailActivationResponse with fail2ban_running + validation_warnings;
  add JailValidationIssue, JailValidationResult, PendingRecovery, RollbackResponse models
- Backend: health_check task tracks last_activation and creates PendingRecovery
  record when fail2ban goes offline within 60 s of an activation
- Backend: add fail2ban_start_command setting (configurable start cmd for rollback)
- Frontend: ActivateJailDialog — pre-validation on open, crash-detected callback,
  extended spinner text during activation+verify
- Frontend: JailsTab — Validate Config button for inactive jails, validation
  result panels (blocking errors + advisory warnings)
- Frontend: RecoveryBanner component — polls pending-recovery, shows full-width
  alert with Disable & Restart / View Logs buttons
- Frontend: MainLayout — mount RecoveryBanner at layout level
- Tests: 19 new backend service tests (validate, rollback, filter/action parsing)
  + 6 health_check crash-detection tests + 11 router tests; 5 RecoveryBanner
  frontend tests; fix mock setup in existing activate_jail tests
Backend:
- Add JailBannedIpsResponse Pydantic model (ban.py)
- Add get_jail_banned_ips() service: server-side pagination, optional
  IP substring search, geo enrichment on page slice only (jail_service.py)
- Add GET /api/jails/{name}/banned endpoint with page/page_size/search
  query params, 400/404/502 error handling (routers/jails.py)
- 23 new tests: 13 service tests + 10 router tests (all passing)

Frontend:
- Add JailBannedIpsResponse TS interface (types/jail.ts)
- Add jailBanned endpoint helper (api/endpoints.ts)
- Add fetchJailBannedIps() API function (api/jails.ts)
- Add BannedIpsSection component: Fluent UI DataGrid, debounced search
  (300 ms), prev/next pagination, page-size dropdown, per-row unban
  button, loading spinner, empty state, error MessageBar (BannedIpsSection.tsx)
- Mount BannedIpsSection in JailDetailPage between stats and patterns
- 12 new Vitest tests for BannedIpsSection (all passing)
backend/tests/test_routers/test_file_config.py:
  - TestListActionFiles.test_200_returns_files: GET /api/config/actions is
    handled by config.router (registered before file_config.router), so mock
    config_file_service.list_actions and assert on ActionListResponse.actions
  - TestCreateActionFile.test_201_creates_file: same route conflict; mock
    config_file_service.create_action and use ActionCreateRequest body format

frontend/src/components/__tests__/ConfigPageLogPath.test.tsx:
  - Log paths are rendered as <Input value={path}>, not text nodes; replace
    getByText() with getByDisplayValue() for both test assertions
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
After deactivation the endpoint now calls _run_probe to flush the
cached server status immediately, matching the activate_jail behaviour
added in Task 5. Without this, the dashboard active-jail count could
remain stale for up to 30 s after a deactivation reload.

- config.py: capture result, await _run_probe, return result
- test_config.py: add test_deactivate_triggers_health_probe; fix 3
  pre-existing UP017 ruff warnings (datetime.UTC alias)
- test_health.py: update test to assert the new fail2ban field
Two frontend bugs and one mypy false positive fixed:

- ActivateJailDialog: Activate button was never disabled when
  blockingIssues.length > 0 (missing condition in disabled prop).
- ActivateJailDialog: handleConfirm called onActivated() even when
  the backend returned active=false (blocked activation). Dialog now
  stays open and shows result.message instead.
- config.py: Settings() call flagged by mypy --strict because
  pydantic-settings loads required fields from env vars at runtime;
  suppressed with a targeted type: ignore[call-arg] comment.

Tests: added ActivateJailDialog.test.tsx (5 tests covering button state,
backend-rejection handling, success path, and crash detection callback).
Implements the missing UI control for POST /api/jails/{name}/ignoreself:
- Add jailIgnoreSelf endpoint constant to endpoints.ts
- Add toggleIgnoreSelf(name, on) API function to jails.ts
- Expose toggleIgnoreSelf action from useJailDetail hook
- Replace read-only 'ignore self' badge with a Fluent Switch in
  IgnoreListSection to allow enabling/disabling the flag per jail
- Add 5 vitest tests for checked/unchecked state and toggle behaviour
Task 1: Move Configuration to last position in sidebar NAV_ITEMS

Task 2: Add automatic rollback when jail activation fails
- Back up .local override file before writing
- Restore original file (or delete) on reload failure, health-check
  failure, or jail not appearing post-reload
- Return recovered=True/False in JailActivationResponse
- Show warning/critical banner in ActivateJailDialog based on recovery
- Add _restore_local_file_sync and _rollback_activation_async helpers
- Add 3 new tests: rollback on reload failure, health-check failure,
  and double failure (recovered=False)

Task 3: Color pie chart legend labels to match their slice color
- legendFormatter now returns ReactNode with span style={{ color }}
- Import LegendPayload from recharts/types/component/DefaultLegendContent
lukas.pupkalipinski merged commit 4be2469f92 into main 2026-03-14 21:19:28 +01:00
lukas.pupkalipinski deleted branch feature/ignore-self-toggle 2026-03-14 21:19:28 +01:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: lukas.pupkalipinski/BanGUI#1