From d9ae55c0aa1e6a7b4e455792d19a6af658a5a651 Mon Sep 17 00:00:00 2001 From: Lorenzo Venerandi Date: Sat, 28 Feb 2026 17:43:50 +0100 Subject: [PATCH] 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 Attacks + + IP Insight +
{# ==================== 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 #} +
+ + ← Back to Dashboard + +
+ {# 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);