Add origin field and filter for ban sources (Tasks 1 & 2)

- 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
This commit is contained in:
2026-03-07 20:03:43 +01:00
parent 706d2e1df8
commit 53d664de4f
28 changed files with 1637 additions and 103 deletions

View File

@@ -0,0 +1,99 @@
/**
* Map color utilities for World Map visualization.
*
* Provides color interpolation logic that maps ban counts to colors based on
* configurable thresholds. Countries with zero bans remain transparent;
* non-zero counts are interpolated through green → yellow → red color stops.
*/
/**
* Interpolate a value between two numbers.
*
* @param start - Start value
* @param end - End value
* @param t - Interpolation factor in [0, 1]
* @returns The interpolated value
*/
function lerp(start: number, end: number, t: number): number {
return start + (end - start) * t;
}
/**
* Convert RGB values to hex color string.
*
* @param r - Red component (0-255)
* @param g - Green component (0-255)
* @param b - Blue component (0-255)
* @returns Hex color string in format "#RRGGBB"
*/
function rgbToHex(r: number, g: number, b: number): string {
const toHex = (n: number): string => {
const hex = Math.round(Math.max(0, Math.min(255, n))).toString(16);
return hex.length === 1 ? "0" + hex : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
/**
* Compute the fill color for a country based on its ban count.
*
* Returns "transparent" for zero bans. For non-zero counts, interpolates
* through color stops:
* - 1 to threshold_low: light green → full green
* - threshold_low to threshold_medium: green → yellow
* - threshold_medium to threshold_high: yellow → red
* - threshold_high or more: solid red
*
* @param banCount - Number of bans for the country
* @param thresholdLow - Ban count for green coloring (default: 20)
* @param thresholdMedium - Ban count for yellow coloring (default: 50)
* @param thresholdHigh - Ban count for red coloring (default: 100)
* @returns Hex color string or "transparent"
*/
export function getBanCountColor(
banCount: number,
thresholdLow: number = 20,
thresholdMedium: number = 50,
thresholdHigh: number = 100,
): string {
// Zero bans → transparent (no fill)
if (banCount === 0) {
return "transparent";
}
// Color stops
const lightGreen = { r: 144, g: 238, b: 144 }; // #90EE90
const green = { r: 0, g: 128, b: 0 }; // #008000
const yellow = { r: 255, g: 255, b: 0 }; // #FFFF00
const red = { r: 220, g: 20, b: 60 }; // #DC143C (crimson)
// 1 to threshold_low: interpolate light green → green
if (banCount <= thresholdLow) {
const t = (banCount - 1) / (thresholdLow - 1);
const r = lerp(lightGreen.r, green.r, t);
const g = lerp(lightGreen.g, green.g, t);
const b = lerp(lightGreen.b, green.b, t);
return rgbToHex(r, g, b);
}
// threshold_low to threshold_medium: interpolate green → yellow
if (banCount <= thresholdMedium) {
const t = (banCount - thresholdLow) / (thresholdMedium - thresholdLow);
const r = lerp(green.r, yellow.r, t);
const g = lerp(green.g, yellow.g, t);
const b = lerp(green.b, yellow.b, t);
return rgbToHex(r, g, b);
}
// threshold_medium to threshold_high: interpolate yellow → red
if (banCount <= thresholdHigh) {
const t = (banCount - thresholdMedium) / (thresholdHigh - thresholdMedium);
const r = lerp(yellow.r, red.r, t);
const g = lerp(yellow.g, red.g, t);
const b = lerp(yellow.b, red.b, t);
return rgbToHex(r, g, b);
}
// threshold_high or more: solid red
return rgbToHex(red.r, red.g, red.b);
}