From 486d02fbd4c7a1b839f2bcbdb10c947aa8751ba9 Mon Sep 17 00:00:00 2001 From: Lorenzo Venerandi Date: Sun, 22 Feb 2026 17:33:57 +0100 Subject: [PATCH 1/8] feat: add deployment strategy type as Recreate in multiple deployment files --- helm/templates/deployment.yaml | 2 ++ kubernetes/krawl-all-in-one-deploy.yaml | 2 ++ kubernetes/manifests/deployment.yaml | 2 ++ 3 files changed, 6 insertions(+) diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index f24261c..11328f4 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -8,6 +8,8 @@ spec: {{- if not .Values.autoscaling.enabled }} replicas: {{ .Values.replicaCount }} {{- end }} + strategy: + type: Recreate selector: matchLabels: {{- include "krawl.selectorLabels" . | nindent 6 }} diff --git a/kubernetes/krawl-all-in-one-deploy.yaml b/kubernetes/krawl-all-in-one-deploy.yaml index 767c080..99e30fc 100644 --- a/kubernetes/krawl-all-in-one-deploy.yaml +++ b/kubernetes/krawl-all-in-one-deploy.yaml @@ -154,6 +154,8 @@ metadata: app.kubernetes.io/version: "1.0.0" spec: replicas: 1 + strategy: + type: Recreate selector: matchLabels: app.kubernetes.io/name: krawl diff --git a/kubernetes/manifests/deployment.yaml b/kubernetes/manifests/deployment.yaml index 4c87a73..aff7469 100644 --- a/kubernetes/manifests/deployment.yaml +++ b/kubernetes/manifests/deployment.yaml @@ -10,6 +10,8 @@ metadata: app.kubernetes.io/version: "1.0.0" spec: replicas: 1 + strategy: + type: Recreate selector: matchLabels: app.kubernetes.io/name: krawl From 75722051d69e3258457a59fac3995454e5d0090b Mon Sep 17 00:00:00 2001 From: carnivuth Date: Sun, 22 Feb 2026 21:53:13 +0100 Subject: [PATCH 2/8] added first version of single ip page breakdiwn --- src/database.py | 63 +++++++++++++++++++ src/routes/api.py | 1 - src/routes/dashboard.py | 31 +++++++++ src/routes/htmx.py | 36 +++++++++++ src/templates/jinja2/dashboard/ip.html | 30 +++++++++ .../partials/access_by_ip_table.html | 63 +++++++++++++++++++ 6 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 src/templates/jinja2/dashboard/ip.html create mode 100644 src/templates/jinja2/dashboard/partials/access_by_ip_table.html diff --git a/src/database.py b/src/database.py index 9daca49..ba34cb5 100644 --- a/src/database.py +++ b/src/database.py @@ -850,6 +850,69 @@ class DatabaseManager: except Exception as e: session.rollback() raise + + def get_access_logs_paginated( + self, + page: int = 1, + page_size: int = 25, + ip_filter: Optional[str] = None, + suspicious_only: bool = False, + since_minutes: Optional[int] = None, + ) -> Dict[str, Any]: + """ + Retrieve access logs with pagination and optional filtering. + + Args: + page: Page to retrieve + page_size: Number of records for page + ip_filter: Filter by IP address + suspicious_only: Only return suspicious requests + since_minutes: Only return logs from the last N minutes + + Returns: + List of access log dictionaries + """ + session = self.session + try: + offset = (page - 1) * page_size + query = session.query(AccessLog).order_by(AccessLog.timestamp.desc()) + + if ip_filter: + query = query.filter(AccessLog.ip == sanitize_ip(ip_filter)) + if suspicious_only: + query = query.filter(AccessLog.is_suspicious == True) + if since_minutes is not None: + cutoff_time = datetime.now() - timedelta(minutes=since_minutes) + query = query.filter(AccessLog.timestamp >= cutoff_time) + + logs = query.offset(offset).limit(page_size).all() + # Get total count of attackers + total_access_logs = ( + session.query(AccessLog).filter(AccessLog.ip == sanitize_ip(ip_filter)).count() + ) + total_pages = (total_access_logs + page_size - 1) // page_size + + return { + "access_logs": [ + { + "id": log.id, + "ip": log.ip, + "path": log.path, + "user_agent": log.user_agent, + "method": log.method, + "is_suspicious": log.is_suspicious, + "is_honeypot_trigger": log.is_honeypot_trigger, + "timestamp": log.timestamp.isoformat(), + "attack_types": [d.attack_type for d in log.attack_detections], + } + for log in logs ], + "pagination": { + "page": page, + "page_size": page_size, + "total_logs": total_access_logs, + "total_pages": total_pages, + }, + } finally: self.close_session() diff --git a/src/routes/api.py b/src/routes/api.py index 02b52dc..a4e6a7a 100644 --- a/src/routes/api.py +++ b/src/routes/api.py @@ -7,7 +7,6 @@ All endpoints are prefixed with the secret dashboard path. """ import os -import json from fastapi import APIRouter, Request, Response, Query from fastapi.responses import JSONResponse, PlainTextResponse diff --git a/src/routes/dashboard.py b/src/routes/dashboard.py index 6f5773b..c8c482e 100644 --- a/src/routes/dashboard.py +++ b/src/routes/dashboard.py @@ -6,6 +6,8 @@ Renders the main dashboard page with server-side data for initial load. """ from fastapi import APIRouter, Request +from fastapi.responses import JSONResponse +from logger import get_app_logger from dependencies import get_db, get_templates @@ -37,3 +39,32 @@ async def dashboard_page(request: Request): "suspicious_activities": suspicious, }, ) + +@router.get("/ip/{ip_address:path}") +async def ip_page(ip_address: str, request: Request): + db = get_db() + try: + stats = db.get_ip_stats_by_ip(ip_address) + config = request.app.state.config + dashboard_path = "/" + config.dashboard_secret_path.lstrip("/") + + if stats: + + templates = get_templates() + return templates.TemplateResponse( + "dashboard/ip.html", + { + "request": request, + "dashboard_path": dashboard_path, + "stats": stats, + "ip_address": ip_address + }, + ) + else: + return JSONResponse( + content={"error": "IP not found"}, + ) + except Exception as e: + get_app_logger().error(f"Error fetching IP stats: {e}") + return JSONResponse(content={"error": str(e)}) + diff --git a/src/routes/htmx.py b/src/routes/htmx.py index 4013ce5..ddf08f6 100644 --- a/src/routes/htmx.py +++ b/src/routes/htmx.py @@ -167,6 +167,42 @@ async def htmx_attackers( ) +# ── Access logs by ip ──────────────────────────────────────────────────────── + + +@router.get("/htmx/access-logs") +async def htmx_access_logs_by_ip( + request: Request, + page: int = Query(1), + sort_by: str = Query("total_requests"), + sort_order: str = Query("desc"), + ip_filter: str = Query("ip_filter"), +): + db = get_db() + result = db.get_access_logs_paginated( + page=max(1, page),page_size=25, ip_filter=ip_filter + ) + + # Normalize pagination key (DB returns total_attackers, template expects total) + pagination = result["pagination"] + if "total_access_logs" in pagination and "total" not in pagination: + pagination["total"] = pagination["total_access_logs"] + + templates = get_templates() + return templates.TemplateResponse( + "dashboard/partials/access_by_ip_table.html", + { + "request": request, + "dashboard_path": _dashboard_path(request), + "items": result["access_logs"], + "pagination": pagination, + "sort_by": sort_by, + "sort_order": sort_order, + "ip_filter": ip_filter, + }, + ) + + # ── Credentials ────────────────────────────────────────────────────── diff --git a/src/templates/jinja2/dashboard/ip.html b/src/templates/jinja2/dashboard/ip.html new file mode 100644 index 0000000..c9d73c7 --- /dev/null +++ b/src/templates/jinja2/dashboard/ip.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} + +{% block content %} +
+ + {# GitHub logo #} + + +

Krawl {{ ip_address }} analysis

+ + + {% include "dashboard/partials/map_section.html" %} + {% include "dashboard/partials/ip_detail.html" %} + + {# Attack Types table #} +
+

{{ip_address}} Access History

+
+
Loading...
+
+
+ +
+{% endblock %} diff --git a/src/templates/jinja2/dashboard/partials/access_by_ip_table.html b/src/templates/jinja2/dashboard/partials/access_by_ip_table.html new file mode 100644 index 0000000..69de4a8 --- /dev/null +++ b/src/templates/jinja2/dashboard/partials/access_by_ip_table.html @@ -0,0 +1,63 @@ +{# HTMX fragment: Detected Access logs by ip table #} +
+ Page {{ pagination.page }}/{{ pagination.total_pages }} — {{ pagination.total }} total +
+ + +
+
+ + + + + + + + + + + + {% for log in items %} + + + + + + + + + + + {% else %} + + {% endfor %} + +
#PathUser-Agent + Time + Actions
{{ loop.index + (pagination.page - 1) * pagination.page_size }} +
+ {{ log.path | e }} + {% if log.path | length > 30 %} +
{{ log.path | e }}
+ {% endif %} +
+
{{ (log.user_agent | default(''))[:50] | e }}{{ log.timestamp | format_ts }} + {% if log.log_id %} + + {% endif %} +
No logs detected
From f7416518fe72293ab13ea53cbde206d5005c98a5 Mon Sep 17 00:00:00 2001 From: carnivuth Date: Sun, 22 Feb 2026 21:53:18 +0100 Subject: [PATCH 3/8] added first version of single ip page breakdiwn --- src/database.py | 31 +++++++++++++++++-------------- src/routes/dashboard.py | 4 ++-- src/routes/htmx.py | 2 +- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/database.py b/src/database.py index ba34cb5..179dab0 100644 --- a/src/database.py +++ b/src/database.py @@ -888,31 +888,34 @@ class DatabaseManager: logs = query.offset(offset).limit(page_size).all() # Get total count of attackers total_access_logs = ( - session.query(AccessLog).filter(AccessLog.ip == sanitize_ip(ip_filter)).count() + session.query(AccessLog) + .filter(AccessLog.ip == sanitize_ip(ip_filter)) + .count() ) total_pages = (total_access_logs + page_size - 1) // page_size return { - "access_logs": [ + "access_logs": [ { - "id": log.id, - "ip": log.ip, - "path": log.path, - "user_agent": log.user_agent, - "method": log.method, - "is_suspicious": log.is_suspicious, - "is_honeypot_trigger": log.is_honeypot_trigger, - "timestamp": log.timestamp.isoformat(), - "attack_types": [d.attack_type for d in log.attack_detections], - } - for log in logs ], + "id": log.id, + "ip": log.ip, + "path": log.path, + "user_agent": log.user_agent, + "method": log.method, + "is_suspicious": log.is_suspicious, + "is_honeypot_trigger": log.is_honeypot_trigger, + "timestamp": log.timestamp.isoformat(), + "attack_types": [d.attack_type for d in log.attack_detections], + } + for log in logs + ], "pagination": { "page": page, "page_size": page_size, "total_logs": total_access_logs, "total_pages": total_pages, }, - } + } finally: self.close_session() diff --git a/src/routes/dashboard.py b/src/routes/dashboard.py index c8c482e..da6846b 100644 --- a/src/routes/dashboard.py +++ b/src/routes/dashboard.py @@ -40,6 +40,7 @@ async def dashboard_page(request: Request): }, ) + @router.get("/ip/{ip_address:path}") async def ip_page(ip_address: str, request: Request): db = get_db() @@ -57,7 +58,7 @@ async def ip_page(ip_address: str, request: Request): "request": request, "dashboard_path": dashboard_path, "stats": stats, - "ip_address": ip_address + "ip_address": ip_address, }, ) else: @@ -67,4 +68,3 @@ async def ip_page(ip_address: str, request: Request): except Exception as e: get_app_logger().error(f"Error fetching IP stats: {e}") return JSONResponse(content={"error": str(e)}) - diff --git a/src/routes/htmx.py b/src/routes/htmx.py index ddf08f6..28de8cd 100644 --- a/src/routes/htmx.py +++ b/src/routes/htmx.py @@ -180,7 +180,7 @@ async def htmx_access_logs_by_ip( ): db = get_db() result = db.get_access_logs_paginated( - page=max(1, page),page_size=25, ip_filter=ip_filter + page=max(1, page), page_size=25, ip_filter=ip_filter ) # Normalize pagination key (DB returns total_attackers, template expects total) From 4194c4585b4dba330b6e23eec49ee06197739c53 Mon Sep 17 00:00:00 2001 From: Lorenzo Venerandi Date: Sat, 28 Feb 2026 15:35:48 +0100 Subject: [PATCH 4/8] fix: update container name to 'krawl' in deployment.yaml --- helm/templates/deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index 11328f4..3676817 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -31,7 +31,7 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} containers: - - name: {{ .Chart.Name }} + - name: krawl {{- with .Values.securityContext }} securityContext: {{- toYaml . | nindent 12 }} From ce713d8072b36546f972a826c4d30e0213779525 Mon Sep 17 00:00:00 2001 From: Lorenzo Venerandi Date: Sat, 28 Feb 2026 16:45:07 +0100 Subject: [PATCH 5/8] tweaked map markers --- src/templates/static/js/map.js | 45 ++++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/src/templates/static/js/map.js b/src/templates/static/js/map.js index 5181295..aaf613b 100644 --- a/src/templates/static/js/map.js +++ b/src/templates/static/js/map.js @@ -36,14 +36,45 @@ function createClusterIcon(cluster) { gradientStops.push(`${color} ${start.toFixed(1)}deg ${end.toFixed(1)}deg`); }); - const size = Math.max(32, Math.min(50, 32 + Math.log2(total) * 5)); - const inner = size - 10; - const offset = 5; // (size - inner) / 2 + const size = Math.max(20, Math.min(44, 20 + Math.log2(total) * 4)); + const centerSize = size - 8; + const centerOffset = 4; + const ringWidth = 4; + const radius = (size / 2) - (ringWidth / 2); + const cx = size / 2; + const cy = size / 2; + const gapDeg = 8; + + // Build SVG arc segments with gaps - glow layer first, then sharp layer + let glowSegments = ''; + let segments = ''; + let currentAngle = -90; + sorted.forEach(([cat, count], idx) => { + const sliceDeg = (count / total) * 360; + if (sliceDeg < gapDeg) return; + const startAngle = currentAngle + (gapDeg / 2); + const endAngle = currentAngle + sliceDeg - (gapDeg / 2); + const startRad = (startAngle * Math.PI) / 180; + const endRad = (endAngle * Math.PI) / 180; + const x1 = cx + radius * Math.cos(startRad); + const y1 = cy + radius * Math.sin(startRad); + const x2 = cx + radius * Math.cos(endRad); + const y2 = cy + radius * Math.sin(endRad); + const largeArc = (endAngle - startAngle) > 180 ? 1 : 0; + const color = categoryColors[cat] || '#8b949e'; + // Glow layer - subtle + glowSegments += ``; + // Sharp layer + segments += ``; + currentAngle += sliceDeg; + }); return L.divIcon({ html: `
` + - `
` + - `
${total}
` + + `` + + `` + + `${glowSegments}${segments}` + + `
${total}
` + `
`, className: 'ip-cluster-icon', iconSize: L.point(size, size) @@ -180,11 +211,11 @@ function buildMapMarkers(ips) { // Single cluster group with custom pie-chart icons clusterGroup = L.markerClusterGroup({ - maxClusterRadius: 20, + maxClusterRadius: 35, spiderfyOnMaxZoom: true, showCoverageOnHover: false, zoomToBoundsOnClick: true, - disableClusteringAtZoom: 10, + disableClusteringAtZoom: 8, iconCreateFunction: createClusterIcon }); From d9ae55c0aa1e6a7b4e455792d19a6af658a5a651 Mon Sep 17 00:00:00 2001 From: Lorenzo Venerandi Date: Sat, 28 Feb 2026 17:43:50 +0100 Subject: [PATCH 6/8] feat: add IP insight feature with detailed view and actions - Updated various tables to include "Actions" column with inspect buttons for IP insights. - Created a new IP insight template for displaying detailed information about an IP address. - Implemented JavaScript functions to handle opening the IP insight view and loading data via HTMX. - Enhanced map markers to include inspect buttons for quick access to IP insights. - Added styles for the new IP insight page and buttons to maintain UI consistency. --- src/database.py | 2 + src/routes/dashboard.py | 4 + src/routes/htmx.py | 30 +- src/templates/jinja2/dashboard/index.html | 16 + src/templates/jinja2/dashboard/ip.html | 294 ++++++++++++++- .../partials/access_by_ip_table.html | 4 +- .../partials/attack_types_table.html | 5 +- .../dashboard/partials/attackers_table.html | 10 +- .../dashboard/partials/credentials_table.html | 10 +- .../dashboard/partials/honeypot_table.html | 10 +- .../jinja2/dashboard/partials/ip_insight.html | 279 ++++++++++++++ .../dashboard/partials/suspicious_table.html | 10 +- .../dashboard/partials/top_ips_table.html | 10 +- src/templates/static/css/dashboard.css | 346 ++++++++++++++++++ src/templates/static/js/dashboard.js | 49 ++- src/templates/static/js/map.js | 14 + 16 files changed, 1070 insertions(+), 23 deletions(-) create mode 100644 src/templates/jinja2/dashboard/partials/ip_insight.html diff --git a/src/database.py b/src/database.py index 179dab0..0e5b40b 100644 --- a/src/database.py +++ b/src/database.py @@ -1084,6 +1084,8 @@ class DatabaseManager: "region": stat.region, "region_name": stat.region_name, "timezone": stat.timezone, + "latitude": stat.latitude, + "longitude": stat.longitude, "isp": stat.isp, "reverse": stat.reverse, "asn": stat.asn, diff --git a/src/routes/dashboard.py b/src/routes/dashboard.py index da6846b..0e93873 100644 --- a/src/routes/dashboard.py +++ b/src/routes/dashboard.py @@ -50,6 +50,10 @@ async def ip_page(ip_address: str, request: Request): dashboard_path = "/" + config.dashboard_secret_path.lstrip("/") if stats: + # Transform fields for template compatibility + list_on = stats.get("list_on") or {} + stats["blocklist_memberships"] = list(list_on.keys()) if list_on else [] + stats["reverse_dns"] = stats.get("reverse") templates = get_templates() return templates.TemplateResponse( diff --git a/src/routes/htmx.py b/src/routes/htmx.py index 28de8cd..ef2d5c1 100644 --- a/src/routes/htmx.py +++ b/src/routes/htmx.py @@ -58,7 +58,7 @@ async def htmx_top_ips( ): db = get_db() result = db.get_top_ips_paginated( - page=max(1, page), page_size=5, sort_by=sort_by, sort_order=sort_order + page=max(1, page), page_size=8, sort_by=sort_by, sort_order=sort_order ) templates = get_templates() @@ -316,6 +316,34 @@ async def htmx_patterns( ) +# ── IP Insight (full IP page as partial) ───────────────────────────── + + +@router.get("/htmx/ip-insight/{ip_address:path}") +async def htmx_ip_insight(ip_address: str, request: Request): + db = get_db() + stats = db.get_ip_stats_by_ip(ip_address) + + if not stats: + stats = {"ip": ip_address, "total_requests": "N/A"} + + # Transform fields for template compatibility + list_on = stats.get("list_on") or {} + stats["blocklist_memberships"] = list(list_on.keys()) if list_on else [] + stats["reverse_dns"] = stats.get("reverse") + + templates = get_templates() + return templates.TemplateResponse( + "dashboard/partials/ip_insight.html", + { + "request": request, + "dashboard_path": _dashboard_path(request), + "stats": stats, + "ip_address": ip_address, + }, + ) + + # ── IP Detail ──────────────────────────────────────────────────────── diff --git a/src/templates/jinja2/dashboard/index.html b/src/templates/jinja2/dashboard/index.html index 5ec70f7..a0dbf8c 100644 --- a/src/templates/jinja2/dashboard/index.html +++ b/src/templates/jinja2/dashboard/index.html @@ -35,6 +35,9 @@ {# ==================== OVERVIEW TAB ==================== #} @@ -147,6 +150,19 @@ + {# ==================== IP INSIGHT TAB ==================== #} +
+ {# IP Insight content - loaded via HTMX when IP is selected #} +
+ +
+
+
+ {# Raw request modal - Alpine.js #} {% include "dashboard/partials/raw_request_modal.html" %} diff --git a/src/templates/jinja2/dashboard/ip.html b/src/templates/jinja2/dashboard/ip.html index c9d73c7..8a0e70f 100644 --- a/src/templates/jinja2/dashboard/ip.html +++ b/src/templates/jinja2/dashboard/ip.html @@ -9,22 +9,302 @@ Krawl -

Krawl {{ ip_address }} analysis

+ {# Back to dashboard link #} + + {# Page header #} +
+

+ {{ ip_address }} + {% if stats.category %} + + {{ stats.category | replace('_', ' ') | title }} + + {% endif %} +

+ {% if stats.city or stats.country %} +

+ {{ stats.city | default('') }}{% if stats.city and stats.country %}, {% endif %}{{ stats.country | default(stats.country_code | default('')) }} +

+ {% endif %} +
- {% include "dashboard/partials/map_section.html" %} - {% include "dashboard/partials/ip_detail.html" %} + {# Main content grid #} +
+ {# Left column: IP Info + Map #} +
+ {# IP Information Card #} +
+

IP Information

+
+
+

Activity

+
+ Total Requests: + {{ stats.total_requests | default('N/A') }} +
+
+ First Seen: + {{ stats.first_seen | format_ts }} +
+
+ Last Seen: + {{ stats.last_seen | format_ts }} +
+ {% if stats.last_analysis %} +
+ Last Analysis: + {{ stats.last_analysis | format_ts }} +
+ {% endif %} +
- {# Attack Types table #} -
-

{{ip_address}} Access History

+
+

Location

+ {% if stats.city %} +
+ City: + {{ stats.city | e }} +
+ {% endif %} + {% if stats.region_name %} +
+ Region: + {{ stats.region_name | e }} +
+ {% endif %} + {% if stats.country %} +
+ Country: + {{ stats.country | e }} ({{ stats.country_code | default('') | e }}) +
+ {% endif %} + {% if stats.timezone %} +
+ Timezone: + {{ stats.timezone | e }} +
+ {% endif %} +
+ +
+

Network

+ {% if stats.isp %} +
+ ISP: + {{ stats.isp | e }} +
+ {% endif %} + {% if stats.asn_org %} +
+ Organization: + {{ stats.asn_org | e }} +
+ {% endif %} + {% if stats.asn %} +
+ ASN: + AS{{ stats.asn }} +
+ {% endif %} + {% if stats.reverse_dns %} +
+ Reverse DNS: + {{ stats.reverse_dns | e }} +
+ {% endif %} +
+ +
+

Flags & Reputation

+ {% set flags = [] %} + {% if stats.is_proxy %}{% set _ = flags.append('Proxy') %}{% endif %} + {% if stats.is_hosting %}{% set _ = flags.append('Hosting') %}{% endif %} + {% if flags %} +
+ Flags: + + {% for flag in flags %} + {{ flag }} + {% endfor %} + +
+ {% endif %} + {% if stats.reputation_score is not none %} +
+ Reputation: + + {{ stats.reputation_score }}/100 + +
+ {% endif %} + {% if stats.blocklist_memberships %} +
+ Listed On: +
+ {% for bl in stats.blocklist_memberships %} + {{ bl | e }} + {% endfor %} +
+
+ {% else %} +
+ Blocklists: + Clean +
+ {% endif %} +
+
+
+ + {# Single IP Map #} +
+

Location

+
+
+
+ + {# Right column: Radar Chart + Timeline #} +
+ {# Category Analysis Card #} + {% if stats.category_scores %} +
+

Category Analysis

+
+
+
+
+ {% endif %} + + {# Behavior Timeline #} + {% if stats.category_history %} +
+

Behavior Timeline

+
+ {% for entry in stats.category_history %} +
+
+
+ {{ entry.new_category | default('unknown') | replace('_', ' ') | title }} + {% if entry.old_category %} + from {{ entry.old_category | replace('_', ' ') | title }} + {% endif %} +
{{ entry.timestamp | format_ts }} +
+
+ {% endfor %} +
+
+ {% endif %} +
+
+ + {# Access History table #} +
+

Access History

Loading...
+ {# Raw Request Modal #} +
+
+
+

Raw Request

+ × +
+
+

+            
+ +
+
{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/src/templates/jinja2/dashboard/partials/access_by_ip_table.html b/src/templates/jinja2/dashboard/partials/access_by_ip_table.html index 69de4a8..34306bc 100644 --- a/src/templates/jinja2/dashboard/partials/access_by_ip_table.html +++ b/src/templates/jinja2/dashboard/partials/access_by_ip_table.html @@ -44,8 +44,8 @@ {{ (log.user_agent | default(''))[:50] | e }} {{ log.timestamp | format_ts }} - {% if log.log_id %} - + {% if log.id %} + {% endif %} diff --git a/src/templates/jinja2/dashboard/partials/attack_types_table.html b/src/templates/jinja2/dashboard/partials/attack_types_table.html index 8a74572..9d8bb30 100644 --- a/src/templates/jinja2/dashboard/partials/attack_types_table.html +++ b/src/templates/jinja2/dashboard/partials/attack_types_table.html @@ -60,10 +60,13 @@ {{ (attack.user_agent | default(''))[:50] | e }} {{ attack.timestamp | format_ts }} - + {% if attack.log_id %} {% endif %} + diff --git a/src/templates/jinja2/dashboard/partials/attackers_table.html b/src/templates/jinja2/dashboard/partials/attackers_table.html index 632137d..4e8a987 100644 --- a/src/templates/jinja2/dashboard/partials/attackers_table.html +++ b/src/templates/jinja2/dashboard/partials/attackers_table.html @@ -28,6 +28,7 @@ First Seen Last Seen Location + Actions @@ -45,16 +46,21 @@ {{ ip.first_seen | format_ts }} {{ ip.last_seen | format_ts }} {{ ip.city | default('') | e }}{% if ip.city and ip.country_code %}, {% endif %}{{ ip.country_code | default('N/A') | e }} + + + - +
Loading stats...
{% else %} - No attackers found + No attackers found {% endfor %} diff --git a/src/templates/jinja2/dashboard/partials/credentials_table.html b/src/templates/jinja2/dashboard/partials/credentials_table.html index ccfb364..92af527 100644 --- a/src/templates/jinja2/dashboard/partials/credentials_table.html +++ b/src/templates/jinja2/dashboard/partials/credentials_table.html @@ -28,6 +28,7 @@ hx-swap="innerHTML"> Time + Actions @@ -45,16 +46,21 @@ {{ cred.password | default('N/A') | e }} {{ cred.path | default('') | e }} {{ cred.timestamp | format_ts }} + + + - +
Loading stats...
{% else %} - No credentials captured + No credentials captured {% endfor %} diff --git a/src/templates/jinja2/dashboard/partials/honeypot_table.html b/src/templates/jinja2/dashboard/partials/honeypot_table.html index 35676fc..f7cd1da 100644 --- a/src/templates/jinja2/dashboard/partials/honeypot_table.html +++ b/src/templates/jinja2/dashboard/partials/honeypot_table.html @@ -25,6 +25,7 @@ hx-swap="innerHTML"> Honeypot Triggers + Actions @@ -39,16 +40,21 @@ {{ item.ip | e }} {{ item.count }} + + + - +
Loading stats...
{% else %} - No data + No data {% endfor %} diff --git a/src/templates/jinja2/dashboard/partials/ip_insight.html b/src/templates/jinja2/dashboard/partials/ip_insight.html new file mode 100644 index 0000000..ae82f61 --- /dev/null +++ b/src/templates/jinja2/dashboard/partials/ip_insight.html @@ -0,0 +1,279 @@ +{# HTMX fragment: IP Insight - full IP detail view for inline display #} +
+ {# Page header #} +
+

+ {{ ip_address }} + {% if stats.category %} + + {{ stats.category | replace('_', ' ') | title }} + + {% endif %} +

+ {% if stats.city or stats.country %} +

+ {{ stats.city | default('') }}{% if stats.city and stats.country %}, {% endif %}{{ stats.country | default(stats.country_code | default('')) }} +

+ {% endif %} +
+ + {# Main content grid #} +
+ {# Left column: IP Info + Map #} +
+ {# IP Information Card #} +
+

IP Information

+
+
+

Activity

+
+ Total Requests: + {{ stats.total_requests | default('N/A') }} +
+
+ First Seen: + {{ stats.first_seen | format_ts }} +
+
+ Last Seen: + {{ stats.last_seen | format_ts }} +
+ {% if stats.last_analysis %} +
+ Last Analysis: + {{ stats.last_analysis | format_ts }} +
+ {% endif %} +
+ +
+

Location

+ {% if stats.city %} +
+ City: + {{ stats.city | e }} +
+ {% endif %} + {% if stats.region_name %} +
+ Region: + {{ stats.region_name | e }} +
+ {% endif %} + {% if stats.country %} +
+ Country: + {{ stats.country | e }} ({{ stats.country_code | default('') | e }}) +
+ {% endif %} + {% if stats.timezone %} +
+ Timezone: + {{ stats.timezone | e }} +
+ {% endif %} +
+ +
+

Network

+ {% if stats.isp %} +
+ ISP: + {{ stats.isp | e }} +
+ {% endif %} + {% if stats.asn_org %} +
+ Organization: + {{ stats.asn_org | e }} +
+ {% endif %} + {% if stats.asn %} +
+ ASN: + AS{{ stats.asn }} +
+ {% endif %} + {% if stats.reverse_dns %} +
+ Reverse DNS: + {{ stats.reverse_dns | e }} +
+ {% endif %} +
+ +
+

Flags & Reputation

+ {% set flags = [] %} + {% if stats.is_proxy %}{% set _ = flags.append('Proxy') %}{% endif %} + {% if stats.is_hosting %}{% set _ = flags.append('Hosting') %}{% endif %} + {% if flags %} +
+ Flags: + + {% for flag in flags %} + {{ flag }} + {% endfor %} + +
+ {% endif %} + {% if stats.reputation_score is not none %} +
+ Reputation: + + {{ stats.reputation_score }}/100 + +
+ {% endif %} + {% if stats.blocklist_memberships %} +
+ Listed On: +
+ {% for bl in stats.blocklist_memberships %} + {{ bl | e }} + {% endfor %} +
+
+ {% else %} +
+ Blocklists: + Clean +
+ {% endif %} +
+
+
+ +
+ + {# Right column: Radar Chart + Timeline #} +
+ {# Category Analysis Card #} + {% if stats.category_scores %} +
+

Category Analysis

+
+
+
+
+ {% endif %} + + {# Behavior Timeline #} + {% if stats.category_history %} +
+

Behavior Timeline

+
+ {% for entry in stats.category_history %} +
+
+
+ {{ entry.new_category | default('unknown') | replace('_', ' ') | title }} + {% if entry.old_category %} + from {{ entry.old_category | replace('_', ' ') | title }} + {% endif %} +
{{ entry.timestamp | format_ts }} +
+
+ {% endfor %} +
+
+ {% endif %} +
+
+ + {# Single IP Map - full width #} +
+

Location

+
+
+ + {# Access History table #} +
+

Access History

+
+
Loading...
+
+
+
+ +{# Inline script for initializing map and chart after HTMX swap #} + diff --git a/src/templates/jinja2/dashboard/partials/suspicious_table.html b/src/templates/jinja2/dashboard/partials/suspicious_table.html index 72a0480..172e6c8 100644 --- a/src/templates/jinja2/dashboard/partials/suspicious_table.html +++ b/src/templates/jinja2/dashboard/partials/suspicious_table.html @@ -8,6 +8,7 @@ Path User-Agent Time + Actions @@ -23,16 +24,21 @@ {{ activity.path | e }} {{ (activity.user_agent | default(''))[:80] | e }} {{ activity.timestamp | format_ts(time_only=True) }} + + + - +
Loading stats...
{% else %} - No suspicious activity detected + No suspicious activity detected {% endfor %} diff --git a/src/templates/jinja2/dashboard/partials/top_ips_table.html b/src/templates/jinja2/dashboard/partials/top_ips_table.html index 84b335f..d014668 100644 --- a/src/templates/jinja2/dashboard/partials/top_ips_table.html +++ b/src/templates/jinja2/dashboard/partials/top_ips_table.html @@ -25,6 +25,7 @@ hx-swap="innerHTML"> Access Count + Actions @@ -39,16 +40,21 @@ {{ item.ip | e }} {{ item.count }} + + + - +
Loading stats...
{% else %} - No data + No data {% endfor %} diff --git a/src/templates/static/css/dashboard.css b/src/templates/static/css/dashboard.css index c7cd3a5..9ed0186 100644 --- a/src/templates/static/css/dashboard.css +++ b/src/templates/static/css/dashboard.css @@ -474,6 +474,15 @@ tbody { color: #58a6ff; border-bottom-color: #58a6ff; } +.tab-button.disabled { + color: #484f58; + cursor: not-allowed; + opacity: 0.6; +} +.tab-button.disabled:hover { + color: #484f58; + background: transparent; +} .tab-content { display: none; } @@ -1253,3 +1262,340 @@ tbody { [x-cloak] { display: none !important; } + +/* ======================================== + Single IP Page Styles + ======================================== */ + +.ip-page-header { + text-align: center; + margin-bottom: 30px; + padding-top: 20px; +} + +.ip-page-header h1 { + display: flex; + align-items: center; + justify-content: center; + gap: 15px; + flex-wrap: wrap; + margin-bottom: 8px; +} + +.ip-address-title { + font-family: 'SF Mono', Monaco, Consolas, monospace; + font-size: 32px; + color: #58a6ff; +} + +.ip-location-subtitle { + color: #8b949e; + font-size: 16px; + margin: 0; +} + +.ip-page-grid { + display: grid; + grid-template-columns: 2fr 1fr; + gap: 20px; + margin-bottom: 20px; +} + +.ip-page-left, +.ip-page-right { + display: flex; + flex-direction: column; + gap: 20px; +} + +.ip-info-card h2 { + margin-bottom: 20px; +} + +.ip-info-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 20px; +} + +.ip-info-section { + background: #0d1117; + border: 1px solid #21262d; + border-radius: 6px; + padding: 15px; +} + +.ip-info-section h3 { + color: #58a6ff; + font-size: 13px; + font-weight: 600; + text-transform: uppercase; + margin: 0 0 12px 0; + padding-bottom: 8px; + border-bottom: 1px solid #21262d; +} + +.ip-flag { + display: inline-block; + padding: 2px 8px; + background: #f0883e1a; + color: #f0883e; + border: 1px solid #f0883e4d; + border-radius: 4px; + font-size: 11px; + font-weight: 600; + margin-right: 4px; +} + +.reputation-score { + font-weight: 700; + padding: 2px 8px; + border-radius: 4px; +} + +.reputation-score.bad { + background: #f851491a; + color: #f85149; +} + +.reputation-score.medium { + background: #f0883e1a; + color: #f0883e; +} + +.reputation-score.good { + background: #3fb9501a; + color: #3fb950; +} + +.radar-chart-container { + display: flex; + justify-content: center; + padding: 20px 0; +} + +/* Single IP page: radar chart with legend on the right */ +.ip-page-right .radar-chart-container { + padding: 10px 0; + justify-content: flex-start; +} + +/* Target the wrapper div injected by generateRadarChart inside radar chart containers */ +.ip-page-right #ip-radar-chart > div, +.ip-page-right #insight-radar-chart > div { + display: flex !important; + flex-direction: row !important; + align-items: center !important; + gap: 15px; +} + +.ip-page-right #ip-radar-chart > div > svg, +.ip-page-right #insight-radar-chart > div > svg { + flex-shrink: 0; +} + +.ip-page-right #ip-radar-chart .radar-legend, +.ip-page-right #insight-radar-chart .radar-legend { + margin-top: 0; + text-align: left; +} + +/* Single IP page: limit timeline height to match map */ +.ip-page-right .timeline { + max-height: 250px; + overflow-y: auto; + padding-right: 10px; +} + +/* Dark theme scrollbar for timeline */ +.ip-page-right .timeline::-webkit-scrollbar { + width: 6px; +} + +.ip-page-right .timeline::-webkit-scrollbar-track { + background: #21262d; + border-radius: 3px; +} + +.ip-page-right .timeline::-webkit-scrollbar-thumb { + background: #484f58; + border-radius: 3px; +} + +.ip-page-right .timeline::-webkit-scrollbar-thumb:hover { + background: #6e7681; +} + +.single-ip-marker { + background: none !important; + border: none !important; +} + +/* Mobile responsiveness for IP page */ +@media (max-width: 1024px) { + .ip-page-grid { + grid-template-columns: 1fr; + } + + .ip-page-right { + order: -1; + } + + .ip-info-grid { + grid-template-columns: 1fr; + } + + /* On mobile, stack legend below chart again */ + .ip-page-right #ip-radar-chart > div, + .ip-page-right #insight-radar-chart > div { + flex-direction: column !important; + } + + .ip-page-right #ip-radar-chart .radar-legend, + .ip-page-right #insight-radar-chart .radar-legend { + margin-top: 10px; + text-align: center; + } + + /* On mobile, remove timeline height limit */ + .ip-page-right .timeline { + max-height: none; + } +} + +@media (max-width: 768px) { + .ip-address-title { + font-size: 24px; + } + + .ip-page-header h1 { + flex-direction: column; + gap: 10px; + } + + .ip-info-section { + padding: 12px; + } + + .ip-info-section h3 { + font-size: 12px; + } +} + +@media (max-width: 480px) { + .ip-address-title { + font-size: 18px; + } + + .ip-location-subtitle { + font-size: 14px; + } +} + +/* ======================================== + IP Lookup Panel + ======================================== */ + +.ip-lookup-panel { + background: #161b22; + border: 1px solid #30363d; + border-radius: 8px; + padding: 20px; + margin-bottom: 30px; + display: flex; + justify-content: center; +} + +.ip-lookup-form { + display: flex; + gap: 12px; + width: 100%; + max-width: 500px; +} + +.ip-lookup-input { + flex: 1; + padding: 12px 16px; + background: #0d1117; + border: 1px solid #30363d; + border-radius: 6px; + color: #c9d1d9; + font-size: 14px; + font-family: 'SF Mono', Monaco, Consolas, monospace; + outline: none; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.ip-lookup-input:focus { + border-color: #58a6ff; + box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.15); +} + +.ip-lookup-input::placeholder { + color: #6e7681; +} + +.ip-lookup-btn { + padding: 12px 24px; + background: #238636; + color: #ffffff; + border: 1px solid #2ea043; + border-radius: 6px; + font-weight: 600; + font-size: 14px; + cursor: pointer; + transition: background 0.2s; + white-space: nowrap; +} + +.ip-lookup-btn:hover { + background: #2ea043; +} + +.ip-lookup-btn:active { + background: #1f7a2f; +} + +@media (max-width: 480px) { + .ip-lookup-form { + flex-direction: column; + } + + .ip-lookup-btn { + width: 100%; + } +} + +/* ======================================== + Inspect Button + ======================================== */ + +.inspect-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 4px; + padding: 4px 10px; + background: #21262d; + color: #58a6ff; + border: 1px solid #30363d; + border-radius: 4px; + font-size: 11px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + text-decoration: none; + white-space: nowrap; +} + +.inspect-btn:hover { + background: #30363d; + border-color: #58a6ff; + color: #79c0ff; +} + +.inspect-btn svg { + width: 12px; + height: 12px; + fill: currentColor; +} diff --git a/src/templates/static/js/dashboard.js b/src/templates/static/js/dashboard.js index b74a51d..eec56ca 100644 --- a/src/templates/static/js/dashboard.js +++ b/src/templates/static/js/dashboard.js @@ -17,19 +17,26 @@ document.addEventListener('alpine:init', () => { // Chart state chartLoaded: false, + // IP Insight state + insightIp: null, + init() { // Handle hash-based tab routing const hash = window.location.hash.slice(1); if (hash === 'ip-stats' || hash === 'attacks') { this.switchToAttacks(); } + // ip-insight tab is only accessible via lens buttons, not direct hash navigation window.addEventListener('hashchange', () => { const h = window.location.hash.slice(1); if (h === 'ip-stats' || h === 'attacks') { this.switchToAttacks(); - } else { - this.switchToOverview(); + } else if (h !== 'ip-insight') { + // Don't switch away from ip-insight via hash if already there + if (this.tab !== 'ip-insight') { + this.switchToOverview(); + } } }); }, @@ -60,6 +67,31 @@ document.addEventListener('alpine:init', () => { window.location.hash = '#overview'; }, + switchToIpInsight() { + // Only allow switching if an IP is selected + if (!this.insightIp) return; + this.tab = 'ip-insight'; + window.location.hash = '#ip-insight'; + }, + + openIpInsight(ip) { + // Set the IP and load the insight content + this.insightIp = ip; + this.tab = 'ip-insight'; + window.location.hash = '#ip-insight'; + + // Load IP insight content via HTMX + this.$nextTick(() => { + const container = document.getElementById('ip-insight-htmx-container'); + if (container && typeof htmx !== 'undefined') { + htmx.ajax('GET', `${this.dashboardPath}/htmx/ip-insight/${encodeURIComponent(ip)}`, { + target: '#ip-insight-htmx-container', + swap: 'innerHTML' + }); + } + }); + }, + async viewRawRequest(logId) { try { const resp = await fetch( @@ -110,6 +142,19 @@ document.addEventListener('alpine:init', () => { })); }); +// Global function for opening IP Insight (used by map popups) +window.openIpInsight = function(ip) { + // Find the Alpine component and call openIpInsight + const container = document.querySelector('[x-data="dashboardApp()"]'); + if (container) { + // Try Alpine 3.x API first, then fall back to older API + const data = Alpine.$data ? Alpine.$data(container) : (container._x_dataStack && container._x_dataStack[0]); + if (data && typeof data.openIpInsight === 'function') { + data.openIpInsight(ip); + } + } +}; + // Utility function for formatting timestamps (used by map popups) function formatTimestamp(isoTimestamp) { if (!isoTimestamp) return 'N/A'; diff --git a/src/templates/static/js/map.js b/src/templates/static/js/map.js index aaf613b..aa4e5ab 100644 --- a/src/templates/static/js/map.js +++ b/src/templates/static/js/map.js @@ -340,6 +340,15 @@ function buildMapMarkers(ips) { `; } + // Add inspect button + popupContent += ` +
+ +
+ `; + popupContent += ''; marker.setPopupContent(popupContent); } catch (err) { @@ -363,6 +372,11 @@ function buildMapMarkers(ips) {
Failed to load chart: ${err.message}
+
+ +
`; marker.setPopupContent(errorPopup); From 3d8178ff0eff31f3e481697ffb24ae88dc41f8a7 Mon Sep 17 00:00:00 2001 From: Lorenzo Venerandi Date: Sat, 28 Feb 2026 18:04:26 +0100 Subject: [PATCH 7/8] feat: enhance dashboard with IP category display and improved data tables --- src/database.py | 11 ++++-- src/templates/jinja2/dashboard/index.html | 34 +++++++++---------- .../partials/access_by_ip_table.html | 6 ++-- .../partials/attack_types_table.html | 2 +- .../dashboard/partials/attackers_table.html | 2 +- .../dashboard/partials/credentials_table.html | 2 +- .../dashboard/partials/honeypot_table.html | 2 +- .../dashboard/partials/suspicious_table.html | 2 +- .../dashboard/partials/top_ips_table.html | 12 +++++-- src/templates/static/js/dashboard.js | 8 +---- 10 files changed, 43 insertions(+), 38 deletions(-) diff --git a/src/database.py b/src/database.py index 0e5b40b..ffd2592 100644 --- a/src/database.py +++ b/src/database.py @@ -1755,14 +1755,19 @@ class DatabaseManager: offset = (page - 1) * page_size results = ( - session.query(AccessLog.ip, func.count(AccessLog.id).label("count")) - .group_by(AccessLog.ip) + session.query( + AccessLog.ip, + func.count(AccessLog.id).label("count"), + IpStats.category, + ) + .outerjoin(IpStats, AccessLog.ip == IpStats.ip) + .group_by(AccessLog.ip, IpStats.category) .all() ) # Filter out local/private IPs and server IP, then sort filtered = [ - {"ip": row.ip, "count": row.count} + {"ip": row.ip, "count": row.count, "category": row.category or "unknown"} for row in results if is_valid_public_ip(row.ip, server_ip) ] diff --git a/src/templates/jinja2/dashboard/index.html b/src/templates/jinja2/dashboard/index.html index a0dbf8c..d4cb7d3 100644 --- a/src/templates/jinja2/dashboard/index.html +++ b/src/templates/jinja2/dashboard/index.html @@ -41,21 +41,10 @@ {# ==================== OVERVIEW TAB ==================== #} -
+
- {# Suspicious Activity - server-rendered #} - {% include "dashboard/partials/suspicious_table.html" %} - - {# Honeypot Triggers - HTMX loaded #} -
-

Honeypot Triggers by IP

-
-
Loading...
-
-
+ {# Map section #} + {% include "dashboard/partials/map_section.html" %} {# Top IPs + Top User-Agents side by side #}
@@ -89,14 +78,14 @@
Loading...
+ + {# Suspicious Activity - server-rendered #} + {% include "dashboard/partials/suspicious_table.html" %}
{# ==================== ATTACKS TAB ==================== #}
- {# Map section #} - {% include "dashboard/partials/map_section.html" %} - {# Attackers table - HTMX loaded #}

Attackers by Total Requests

@@ -119,6 +108,17 @@
+ {# Honeypot Triggers - HTMX loaded #} +
+

Honeypot Triggers by IP

+
+
Loading...
+
+
+ {# Attack Types table #}

Detected Attack Types

diff --git a/src/templates/jinja2/dashboard/partials/access_by_ip_table.html b/src/templates/jinja2/dashboard/partials/access_by_ip_table.html index 34306bc..5e7bd6c 100644 --- a/src/templates/jinja2/dashboard/partials/access_by_ip_table.html +++ b/src/templates/jinja2/dashboard/partials/access_by_ip_table.html @@ -26,7 +26,7 @@ hx-swap="innerHTML"> Time - Actions + @@ -50,14 +50,14 @@ - +
Loading stats...
{% else %} - No logs detected + No logs detected {% endfor %} diff --git a/src/templates/jinja2/dashboard/partials/attack_types_table.html b/src/templates/jinja2/dashboard/partials/attack_types_table.html index 9d8bb30..9a2451a 100644 --- a/src/templates/jinja2/dashboard/partials/attack_types_table.html +++ b/src/templates/jinja2/dashboard/partials/attack_types_table.html @@ -28,7 +28,7 @@ hx-swap="innerHTML"> Time - Actions + diff --git a/src/templates/jinja2/dashboard/partials/attackers_table.html b/src/templates/jinja2/dashboard/partials/attackers_table.html index 4e8a987..23c61e8 100644 --- a/src/templates/jinja2/dashboard/partials/attackers_table.html +++ b/src/templates/jinja2/dashboard/partials/attackers_table.html @@ -28,7 +28,7 @@ First Seen Last Seen Location - Actions + diff --git a/src/templates/jinja2/dashboard/partials/credentials_table.html b/src/templates/jinja2/dashboard/partials/credentials_table.html index 92af527..491b5b2 100644 --- a/src/templates/jinja2/dashboard/partials/credentials_table.html +++ b/src/templates/jinja2/dashboard/partials/credentials_table.html @@ -28,7 +28,7 @@ hx-swap="innerHTML"> Time - Actions + diff --git a/src/templates/jinja2/dashboard/partials/honeypot_table.html b/src/templates/jinja2/dashboard/partials/honeypot_table.html index f7cd1da..3215bc7 100644 --- a/src/templates/jinja2/dashboard/partials/honeypot_table.html +++ b/src/templates/jinja2/dashboard/partials/honeypot_table.html @@ -25,7 +25,7 @@ hx-swap="innerHTML"> Honeypot Triggers - Actions + diff --git a/src/templates/jinja2/dashboard/partials/suspicious_table.html b/src/templates/jinja2/dashboard/partials/suspicious_table.html index 172e6c8..c10502c 100644 --- a/src/templates/jinja2/dashboard/partials/suspicious_table.html +++ b/src/templates/jinja2/dashboard/partials/suspicious_table.html @@ -8,7 +8,7 @@ Path User-Agent Time - Actions + diff --git a/src/templates/jinja2/dashboard/partials/top_ips_table.html b/src/templates/jinja2/dashboard/partials/top_ips_table.html index d014668..afcf26b 100644 --- a/src/templates/jinja2/dashboard/partials/top_ips_table.html +++ b/src/templates/jinja2/dashboard/partials/top_ips_table.html @@ -19,13 +19,14 @@ # IP Address + Category Access Count - Actions + @@ -39,6 +40,11 @@ @click="toggleIpDetail($event)"> {{ item.ip | e }} + + {% set cat = item.category | default('unknown') %} + {% set cat_colors = {'attacker': '#f85149', 'good_crawler': '#3fb950', 'bad_crawler': '#f0883e', 'regular_user': '#58a6ff', 'unknown': '#8b949e'} %} + + {{ item.count }}
{# ==================== ATTACKS TAB ==================== #}