Files
BanGUI/Docs/Tasks.md
Lukas 576ec43854 Polish dashboard charts and add frontend tests (Stage 6)
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
2026-03-11 17:25:28 +01:00

20 KiB
Raw Blame History

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.


Stage 1 — Dashboard Charts Foundation

Task 1.1 — Install and configure a charting library

Status: done

The frontend currently has no charting library. Install Recharts (recharts) as the project charting library. Recharts is React-native, composable, and integrates cleanly with Fluent UI v9 theming.

Steps:

  1. Run npm install recharts in the frontend/ directory.
  2. Verify the dependency appears in package.json under dependencies.
  3. Confirm the build still succeeds with npm run build (no type errors, no warnings).

No wrapper or configuration file is needed — Recharts components are imported directly where used.

Acceptance criteria:

  • recharts is listed in frontend/package.json.
  • npm run build succeeds with zero errors or warnings.

Task 1.2 — Create a shared chart theme utility

Status: done

Create a small utility at frontend/src/utils/chartTheme.ts that exports a function (or constant object) mapping Fluent UI v9 design tokens to Recharts-compatible colour values. The charts must respect the current Fluent theme (light and dark mode). At minimum export:

  • A palette of 5+ distinct categorical colours for pie/bar slices, derived from Fluent token aliases (e.g. colorPaletteBlueBorderActive, colorPaletteRedBorderActive, etc.).
  • Axis/grid/tooltip colours derived from colorNeutralForeground2, colorNeutralStroke2, colorNeutralBackground1, etc.
  • A helper that returns the CSS value of a Fluent token at runtime (since Recharts needs literal CSS colour strings, not CSS custom properties).

Keep the file under 60 lines. No React components here — pure utility.

References: Web-Design.md § colour tokens.

Acceptance criteria:

  • The exported palette contains at least 5 distinct colours.
  • Colours change correctly between light and dark mode.
  • tsc --noEmit and eslint pass with zero warnings.

Stage 2 — Country Pie Chart (Top 4 + Other)

Task 2.1 — Create the TopCountriesPieChart component

Status: done

Create frontend/src/components/TopCountriesPieChart.tsx. This component renders a pie chart (Kuchendiagramm) showing the top 4 countries by ban count plus an "Other" slice that aggregates every remaining country.

Data source: The component receives the countries map (Record<string, number>) and country_names map (Record<string, string>) from the existing /api/dashboard/bans/by-country endpoint response (BansByCountryResponse). No new API endpoint is needed.

Aggregation logic (frontend):

  1. Sort the countries entries descending by ban count.
  2. Take the top 4 entries.
  3. Sum all remaining entries into a single "Other" bucket.
  4. The result is exactly 5 slices (or fewer if fewer than 5 countries exist).

Visual requirements:

  • Use <PieChart> and <Pie> from Recharts with <Cell> for per-slice colours from the chart theme palette (Task 1.2).
  • Display a <Tooltip> on hover showing the country name and ban count.
  • Display a <Legend> listing each slice with its country name (full name from country_names, not just the code) and percentage.
  • Label each slice with the percentage (use Recharts label prop or <Label>).
  • Use makeStyles for any layout styling. Follow Web-Design.md spacing and card conventions.
  • Wrap the chart in a responsive container so it scales with its parent.

Props interface:

interface TopCountriesPieChartProps {
  countries: Record<string, number>;
  countryNames: Record<string, string>;
}

Acceptance criteria:

  • Always renders exactly 5 slices (or fewer when data has < 5 countries).
  • The "Other" slice correctly sums all countries outside the top 4.
  • Tooltip displays country name + count on hover.
  • Legend shows country name + percentage.
  • Responsive — no horizontal overflow on narrow viewports.
  • tsc --noEmit passes. No any types. ESLint clean.

Task 2.2 — Create a useDashboardCountryData hook

Status: done

Create frontend/src/hooks/useDashboardCountryData.ts. This hook wraps the existing GET /api/dashboard/bans/by-country call and returns the data the dashboard charts need. The existing useMapData hook is designed for the map page and should not be reused because it is coupled to map-specific debouncing and state.

Signature:

function useDashboardCountryData(
  timeRange: TimeRange,
  origin: BanOriginFilter,
): {
  countries: Record<string, number>;
  countryNames: Record<string, string>;
  bans: DashboardBanItem[];
  total: number;
  isLoading: boolean;
  error: string | null;
};

Behaviour:

  • Call GET /api/dashboard/bans/by-country?range={timeRange} with optional origin query param (omit when "all").
  • Use the typed API client from api/client.ts.
  • Set isLoading while fetching, populate error on failure.
  • Re-fetch when timeRange or origin changes.
  • Mirror the data-fetching patterns used by useBans / useMapData.

Acceptance criteria:

  • Returns typed data matching BansByCountryResponse.
  • Re-fetches on param change.
  • tsc --noEmit and ESLint pass.

Task 2.3 — Integrate the pie chart into DashboardPage

Status: done

Add the TopCountriesPieChart below the ServerStatusBar and above the "Ban List" section on the DashboardPage. The chart must share the same timeRange and originFilter state that already exists on the page.

Layout:

  • Place the pie chart inside a new section card (reuse the section / sectionHeader pattern from the existing ban-list section).
  • Section title: "Top Countries".
  • The pie chart card sits in a future row of chart cards (see Task 3.3). For now, render it full-width. Use a CSS class name like chartsRow so the bar chart can be added beside it later.

Acceptance criteria:

  • The pie chart renders on the dashboard, respecting the selected time range and origin filter.
  • Changing the time range or origin filter re-renders the chart with new data.
  • The loading and error states from the hook are handled (show <Spinner> while loading, <MessageBar> on error).
  • tsc --noEmit and ESLint pass.

Stage 3 — Country Bar Chart (Top 20)

Task 3.1 — Create the TopCountriesBarChart component

Status: done

Create frontend/src/components/TopCountriesBarChart.tsx. This component renders a horizontal bar chart (Balkendiagramm) showing the top 20 countries by ban count.

Data source: Same countries and country_names maps from BansByCountryResponse — passed as props identical to the pie chart.

Aggregation logic (frontend):

  1. Sort the countries entries descending by ban count.
  2. Take the top 20 entries.
  3. No "Other" bucket — the bar chart is detail-focused.

Visual requirements:

  • Use <BarChart> (horizontal via layout="vertical") from Recharts with <Bar>, <XAxis>, <YAxis>, <CartesianGrid>, and <Tooltip>.
  • Y-axis shows country names (full name from country_names, truncated to ~20 chars with ellipsis if needed).
  • X-axis shows ban count (numeric).
  • Bars are coloured with the primary colour from the chart theme palette.
  • Tooltip shows the full country name and exact ban count.
  • Chart height should be dynamic based on the number of bars (e.g. barCount * 36px min), with a reasonable minimum height.
  • Wrap in a <ResponsiveContainer> for width.

Props interface:

interface TopCountriesBarChartProps {
  countries: Record<string, number>;
  countryNames: Record<string, string>;
}

Acceptance criteria:

  • Renders up to 20 bars, sorted descending.
  • Country names readable on the Y-axis; tooltip provides full detail.
  • Responsive width, dynamic height.
  • tsc --noEmit passes. No any. ESLint clean.

Task 3.2 — Integrate the bar chart into DashboardPage

Status: done

Add the TopCountriesBarChart to the dashboard alongside the pie chart.

Layout:

  • The charts section now contains two cards side-by-side in a responsive grid row (the chartsRow class from Task 2.3):
    • Left: Top Countries pie chart (Task 2.1).
    • Right: Top 20 Countries bar chart (Task 3.1).
  • On narrow screens (< 768 px viewport width) the cards should stack vertically.
  • Both charts consume data from the same useDashboardCountryData hook call — do not fetch twice.

Acceptance criteria:

  • Both charts render side by side on wide screens, stacked on narrow screens.
  • A single API call feeds both charts.
  • Time range / origin filter controls affect both charts.
  • Loading / error states handled for both.
  • tsc --noEmit and ESLint pass.

Stage 4 — Bans-Over-Time Trend Chart

Task 4.1 — Add a backend endpoint for time-series ban aggregation

Status: done

Added GET /api/dashboard/bans/trend. New Pydantic models BanTrendBucket and BanTrendResponse (plus BUCKET_SECONDS, BUCKET_SIZE_LABEL, bucket_count helpers) in ban.py. Service function ban_trend() in ban_service.py groups bans.timeofban into equal-width buckets via SQL and fills empty buckets with zero so the frontend always receives a gap-free series. Route added to dashboard.py. 20 new tests (10 service, 10 router) — all pass, total suite 480 passed, 83% coverage.

The existing endpoints return flat lists or country-aggregated counts but no time-bucketed series. A dashboard trend chart needs data grouped into time buckets.

Create a new endpoint: GET /api/dashboard/bans/trend.

Query params:

Param Type Default Description
range TimeRange "24h" Time-range preset.
origin BanOrigin | null null Optional filter by ban origin.

Response model (BanTrendResponse):

class BanTrendBucket(BaseModel):
    timestamp: str  # ISO 8601 UTC start of the bucket
    count: int      # Number of bans in this bucket

class BanTrendResponse(BaseModel):
    buckets: list[BanTrendBucket]
    bucket_size: str  # Human-readable label: "1h", "6h", "1d", "7d"

Bucket strategy:

Range Bucket size Example buckets
24h 1 hour 24 buckets
7d 6 hours 28 buckets
30d 1 day 30 buckets
365d 7 days ~52 buckets

Implementation:

  • Add the Pydantic models to backend/app/models/ban.py.
  • Add the service function in backend/app/services/ban_service.py. Query the fail2ban database (bans table), group rows by the computed bucket. Use SQL CAST((banned_at - ?) / bucket_seconds AS INTEGER) style bucketing.
  • Add the route in backend/app/routers/dashboard.py.
  • Follow the existing layering: router → service → repository.
  • Write tests for the new endpoint in backend/tests/test_routers/ and backend/tests/test_services/.

Acceptance criteria:

  • GET /api/dashboard/bans/trend?range=24h returns 24 hourly buckets.
  • Each bucket has a correct ISO 8601 timestamp and count.
  • Origin filter is applied correctly.
  • Empty buckets (zero bans) are included so the frontend has a continuous series.
  • Tests pass and cover happy path + empty data + origin filter.
  • ruff check and mypy --strict pass.

Task 4.2 — Create the BanTrendChart component

Status: done

Created frontend/src/components/BanTrendChart.tsx — an area chart using Recharts AreaChart with a gradient fill, human-readable X-axis time labels (format varies by time range), and a custom tooltip. Added BanTrendBucket/BanTrendResponse types to types/ban.ts, dashboardBansTrend constant to api/endpoints.ts, fetchBanTrend() to api/dashboard.ts, and the useBanTrend hook at hooks/useBanTrend.ts. Component handles loading (Spinner), error (MessageBar), and empty states inline. tsc --noEmit and ESLint pass with zero warnings.

Create frontend/src/components/BanTrendChart.tsx. This component renders an area/line chart showing the number of bans over time.

Data source: A new useBanTrend hook that calls GET /api/dashboard/bans/trend.

Visual requirements:

  • Use <AreaChart> (or <LineChart>) from Recharts with <Area>, <XAxis>, <YAxis>, <CartesianGrid>, <Tooltip>.
  • X-axis: time labels formatted human-readably (e.g. "Mon 14:00", "Mar 5").
  • Y-axis: ban count.
  • Area fill with a semi-transparent version of the primary chart colour.
  • Tooltip shows exact timestamp + count.
  • Responsive via <ResponsiveContainer>.

Acceptance criteria:

  • Displays a continuous time-series line with the correct number of data points for each range.
  • Readable axis labels for all four time ranges.
  • Responsive.
  • tsc --noEmit, ESLint clean.

Task 4.3 — Integrate the trend chart into DashboardPage

Status: done

Added a "Ban Trend" full-width section card to DashboardPage between the ServerStatusBar and the "Top Countries" section. The section renders <BanTrendChart timeRange={timeRange} origin={originFilter} />, sharing the same state already used by the country charts and ban list. Loading, error, and empty states are handled inside BanTrendChart itself. tsc --noEmit and ESLint pass with zero warnings.

Add the BanTrendChart to the dashboard page above the two country charts and below the ServerStatusBar.

Layout:

  • Full-width section card.
  • Section title: "Ban Trend".
  • Shares the same timeRange and originFilter state.

Acceptance criteria:

  • The trend chart renders on the dashboard showing bans over time.
  • Responds to time-range and origin-filter changes.
  • Loading/error states handled.
  • tsc --noEmit and ESLint pass.

Stage 5 — Jail Distribution Chart

Task 5.1 — Add a backend endpoint for ban counts per jail

Status: done

Added GET /api/dashboard/bans/by-jail. New Pydantic models JailBanCount and BansByJailResponse added to ban.py. Service function bans_by_jail() in ban_service.py queries the bans table with GROUP BY jail ORDER BY COUNT(*) DESC and applies the origin filter. Route added to dashboard.py. 7 new service tests (happy path, total equality, empty DB, time-window exclusion, origin filter variants) and 10 new router tests — all pass, total suite 497 passed, 83% coverage. ruff check and mypy --strict pass.

The existing GET /api/jails endpoint returns jail metadata with status.currently_banned — but this counts currently active bans, not historical bans in the selected time window. The dashboard needs historical ban counts per jail within the selected time range.

Create a new endpoint: GET /api/dashboard/bans/by-jail.

Query params:

Param Type Default Description
range TimeRange "24h" Time-range preset.
origin BanOrigin | null null Optional origin filter.

Response model (BansByJailResponse):

class JailBanCount(BaseModel):
    jail: str
    count: int

class BansByJailResponse(BaseModel):
    jails: list[JailBanCount]
    total: int

Implementation:

  • Query the bans table: SELECT jail, COUNT(*) FROM bans WHERE timestart >= ? GROUP BY jail ORDER BY COUNT(*) DESC.
  • Apply origin filter by checking whether jail == 'blocklist-import'.
  • Add models, service function, route, and tests following existing patterns.

Acceptance criteria:

  • Returns jail names with ban counts descending, within the selected time window.
  • Origin filter works correctly.
  • Tests covering happy path, empty data, and filter.
  • ruff check and mypy --strict pass.

Task 5.2 — Create the JailDistributionChart component

Status: done

Created frontend/src/components/JailDistributionChart.tsx — a horizontal bar chart using Recharts BarChart showing ban counts per jail sorted descending. Added JailBanCount/BansByJailResponse types to types/ban.ts, dashboardBansByJail constant to api/endpoints.ts, fetchBansByJail() to api/dashboard.ts, and the useJailDistribution hook at hooks/useJailDistribution.ts. Component handles loading (Spinner), error (MessageBar), and empty states inline. tsc --noEmit and ESLint pass with zero warnings.

Create frontend/src/components/JailDistributionChart.tsx. This component renders a horizontal bar chart showing the distribution of bans across jails.

Why this is useful and not covered by existing views: The current Jails page shows configuration details and live counters per jail, but does not provide a visual comparison of which jails are catching the most threats within a selectable time window. An admin reviewing the dashboard benefits from an at-a-glance answer to: "Which services are being attacked most frequently right now?" — this is fundamentally different from the country-based charts (which answer "where") and from the ban trend (which answers "when"). The jail distribution answers "what service is targeted" and helps prioritise hardening efforts.

Data source: A new useJailDistribution hook calling GET /api/dashboard/bans/by-jail.

Visual requirements:

  • Horizontal <BarChart> from Recharts.
  • Y-axis: jail names.
  • X-axis: ban count.
  • Colour-coded bars from the chart theme.
  • Tooltip with jail name and exact count.
  • Responsive.

Acceptance criteria:

  • Renders one bar per jail, sorted descending.
  • Responsive.
  • tsc --noEmit, ESLint clean.

Task 5.3 — Integrate the jail distribution chart into DashboardPage

Status: done

Added a full-width "Jail Distribution" section card to DashboardPage below the "Top Countries" section (2-column country charts on row 1, jail chart full-width on row 2). The section renders <JailDistributionChart timeRange={timeRange} origin={originFilter} />, sharing the same state already used by the other charts. Loading, error, and empty states are handled inside JailDistributionChart itself. tsc --noEmit and ESLint pass with zero warnings.

Add the JailDistributionChart as a third chart card alongside the two country charts, or in a second chart row below them if space is constrained.

Layout decision:

  • If three cards fit side-by-side at the standard breakpoint, place all three in one row.
  • Otherwise, use a 2-column + 1-column stacked layout (pie + bar on row 1, jail chart full-width on row 2). Choose whichever looks cleaner.

Acceptance criteria:

  • The jail distribution chart renders on the dashboard.
  • Shares time-range and origin-filter controls with the other charts.
  • Loading/error states handled.
  • Responsive layout.
  • tsc --noEmit and ESLint pass.

Stage 6 — Polish and Final Review

Task 6.1 — Ensure consistent loading, error, and empty states across all charts

Status: done

Review all four chart components and ensure:

  1. Loading state: Each shows a Fluent UI <Spinner> centred in its card while data is fetching.
  2. Error state: Each shows a Fluent UI <MessageBar intent="error"> with a retry button.
  3. Empty state: When the data set has zero bans, each chart shows a friendly message (e.g. "No bans in this time range") instead of an empty or broken chart.

Extract a small shared wrapper if three or more charts duplicate the same loading/error/empty pattern (e.g. ChartCard or ChartStateWrapper).

Acceptance criteria:

  • All charts handle loading, error, and empty states consistently.
  • No broken or blank chart renders when data is unavailable.
  • tsc --noEmit and ESLint pass.

Task 6.2 — Write frontend tests for chart components

Status: done

Add tests for each chart component to confirm:

  • Correct number of rendered slices/bars given known test data.
  • "Other" aggregation logic in the pie chart.
  • Top-N truncation in the bar chart.
  • Hook re-fetch on prop change.
  • Loading and error states render the expected UI.

Follow the project's existing frontend test setup and conventions.

Acceptance criteria:

  • Each chart component has at least one happy-path and one edge-case test.
  • Tests pass.
  • ESLint clean.

Task 6.3 — Full build and lint check

Status: done

Run the complete quality-assurance pipeline:

  1. Backend: ruff check, mypy --strict, pytest with coverage.
  2. Frontend: tsc --noEmit, eslint, npm run build.
  3. Fix any warnings or errors introduced during stages 16.
  4. Verify overall test coverage remains ≥ 80 %.

Acceptance criteria:

  • Zero lint warnings/errors on both backend and frontend.
  • All tests pass.
  • Build artifacts generated successfully.