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 #}
+
+
+
+
Select an IP address from any table to view detailed insights.
+
+
+
+
+
+
{# 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 #}
+
- {% 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 #}
+
+
+
+ {# Right column: Radar Chart + Timeline #}
+
+ {# Category Analysis Card #}
+ {% if stats.category_scores %}
+
+ {% 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 #}
+
+ {# Raw Request Modal #}
+
{% 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 }} |
+
+
+ |
- |
+ |
|
{% 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 }} |
+
+
+ |
- |
+ |
|
{% 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 }} |
+
+
+ |
- |
+ |
|
{% 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 #}
+
+
+ {# 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 %}
+
+ {% 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 #}
+
+
+ {# Access History table #}
+
+
+
+{# 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) }} |
+
+
+ |
- |
+ |
|
{% 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 }} |
+
+
+ |
- |
+ |
|
{% 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);