Files
krawl.es/src/templates/jinja2/dashboard/partials/ip_insight.html
Lorenzo Venerandi d9ae55c0aa 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.
2026-02-28 17:43:50 +01:00

280 lines
12 KiB
HTML

{# HTMX fragment: IP Insight - full IP detail view for inline display #}
<div class="ip-insight-content" id="ip-insight-content">
{# Page header #}
<div class="ip-page-header">
<h1>
<span class="ip-address-title">{{ ip_address }}</span>
{% if stats.category %}
<span class="category-badge category-{{ stats.category | lower | replace('_', '-') }}">
{{ stats.category | replace('_', ' ') | title }}
</span>
{% endif %}
</h1>
{% if stats.city or stats.country %}
<p class="ip-location-subtitle">
{{ stats.city | default('') }}{% if stats.city and stats.country %}, {% endif %}{{ stats.country | default(stats.country_code | default('')) }}
</p>
{% endif %}
</div>
{# Main content grid #}
<div class="ip-page-grid">
{# Left column: IP Info + Map #}
<div class="ip-page-left">
{# IP Information Card #}
<div class="table-container ip-info-card">
<h2>IP Information</h2>
<div class="ip-info-grid">
<div class="ip-info-section">
<h3>Activity</h3>
<div class="stat-row">
<span class="stat-label-sm">Total Requests:</span>
<span class="stat-value-sm">{{ stats.total_requests | default('N/A') }}</span>
</div>
<div class="stat-row">
<span class="stat-label-sm">First Seen:</span>
<span class="stat-value-sm">{{ stats.first_seen | format_ts }}</span>
</div>
<div class="stat-row">
<span class="stat-label-sm">Last Seen:</span>
<span class="stat-value-sm">{{ stats.last_seen | format_ts }}</span>
</div>
{% if stats.last_analysis %}
<div class="stat-row">
<span class="stat-label-sm">Last Analysis:</span>
<span class="stat-value-sm">{{ stats.last_analysis | format_ts }}</span>
</div>
{% endif %}
</div>
<div class="ip-info-section">
<h3>Location</h3>
{% if stats.city %}
<div class="stat-row">
<span class="stat-label-sm">City:</span>
<span class="stat-value-sm">{{ stats.city | e }}</span>
</div>
{% endif %}
{% if stats.region_name %}
<div class="stat-row">
<span class="stat-label-sm">Region:</span>
<span class="stat-value-sm">{{ stats.region_name | e }}</span>
</div>
{% endif %}
{% if stats.country %}
<div class="stat-row">
<span class="stat-label-sm">Country:</span>
<span class="stat-value-sm">{{ stats.country | e }} ({{ stats.country_code | default('') | e }})</span>
</div>
{% endif %}
{% if stats.timezone %}
<div class="stat-row">
<span class="stat-label-sm">Timezone:</span>
<span class="stat-value-sm">{{ stats.timezone | e }}</span>
</div>
{% endif %}
</div>
<div class="ip-info-section">
<h3>Network</h3>
{% if stats.isp %}
<div class="stat-row">
<span class="stat-label-sm">ISP:</span>
<span class="stat-value-sm">{{ stats.isp | e }}</span>
</div>
{% endif %}
{% if stats.asn_org %}
<div class="stat-row">
<span class="stat-label-sm">Organization:</span>
<span class="stat-value-sm">{{ stats.asn_org | e }}</span>
</div>
{% endif %}
{% if stats.asn %}
<div class="stat-row">
<span class="stat-label-sm">ASN:</span>
<span class="stat-value-sm">AS{{ stats.asn }}</span>
</div>
{% endif %}
{% if stats.reverse_dns %}
<div class="stat-row">
<span class="stat-label-sm">Reverse DNS:</span>
<span class="stat-value-sm" style="word-break: break-all;">{{ stats.reverse_dns | e }}</span>
</div>
{% endif %}
</div>
<div class="ip-info-section">
<h3>Flags & Reputation</h3>
{% set flags = [] %}
{% if stats.is_proxy %}{% set _ = flags.append('Proxy') %}{% endif %}
{% if stats.is_hosting %}{% set _ = flags.append('Hosting') %}{% endif %}
{% if flags %}
<div class="stat-row">
<span class="stat-label-sm">Flags:</span>
<span class="stat-value-sm">
{% for flag in flags %}
<span class="ip-flag">{{ flag }}</span>
{% endfor %}
</span>
</div>
{% endif %}
{% if stats.reputation_score is not none %}
<div class="stat-row">
<span class="stat-label-sm">Reputation:</span>
<span class="stat-value-sm reputation-score {% if stats.reputation_score <= 30 %}bad{% elif stats.reputation_score <= 60 %}medium{% else %}good{% endif %}">
{{ stats.reputation_score }}/100
</span>
</div>
{% endif %}
{% if stats.blocklist_memberships %}
<div class="stat-row" style="flex-direction: column; align-items: flex-start; gap: 8px;">
<span class="stat-label-sm">Listed On:</span>
<div style="display: flex; flex-wrap: wrap; gap: 6px;">
{% for bl in stats.blocklist_memberships %}
<span class="reputation-badge">{{ bl | e }}</span>
{% endfor %}
</div>
</div>
{% else %}
<div class="stat-row">
<span class="stat-label-sm">Blocklists:</span>
<span class="reputation-clean">Clean</span>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{# Right column: Radar Chart + Timeline #}
<div class="ip-page-right">
{# Category Analysis Card #}
{% if stats.category_scores %}
<div class="table-container">
<h2>Category Analysis</h2>
<div class="radar-chart-container">
<div class="radar-chart" id="insight-radar-chart"></div>
</div>
</div>
{% endif %}
{# Behavior Timeline #}
{% if stats.category_history %}
<div class="table-container">
<h2>Behavior Timeline</h2>
<div class="timeline">
{% for entry in stats.category_history %}
<div class="timeline-item">
<div class="timeline-marker {{ entry.new_category | default('unknown') | replace('_', '-') }}"></div>
<div>
<strong style="color: #e6edf3;">{{ entry.new_category | default('unknown') | replace('_', ' ') | title }}</strong>
{% if entry.old_category %}
<span style="color: #8b949e;"> from {{ entry.old_category | replace('_', ' ') | title }}</span>
{% endif %}
<br><span style="color: #8b949e; font-size: 11px;">{{ entry.timestamp | format_ts }}</span>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>
{# Single IP Map - full width #}
<div class="table-container" style="margin-top: 20px;">
<h2>Location</h2>
<div id="insight-ip-map" style="height: 300px; border-radius: 6px; border: 1px solid #30363d;"></div>
</div>
{# Access History table #}
<div class="table-container alert-section" style="margin-top: 20px;">
<h2>Access History</h2>
<div class="htmx-container"
hx-get="{{ dashboard_path }}/htmx/access-logs?page=1&ip_filter={{ ip_address }}"
hx-trigger="revealed"
hx-swap="innerHTML">
<div class="htmx-indicator">Loading...</div>
</div>
</div>
</div>
{# Inline script for initializing map and chart after HTMX swap #}
<script>
(function() {
// Initialize radar chart
{% if stats.category_scores %}
const scores = {{ stats.category_scores | tojson }};
const container = document.getElementById('insight-radar-chart');
if (container && typeof generateRadarChart === 'function') {
container.innerHTML = generateRadarChart(scores, 220, true);
}
{% endif %}
// Initialize single IP map
{% if stats.latitude and stats.longitude %}
setTimeout(function() {
const mapContainer = document.getElementById('insight-ip-map');
if (!mapContainer || typeof L === 'undefined') return;
// Clean up any existing map instance
if (mapContainer._leaflet_id) {
mapContainer._leaflet_id = null;
}
mapContainer.innerHTML = '';
const lat = {{ stats.latitude }};
const lng = {{ stats.longitude }};
const category = '{{ stats.category | default("unknown") | lower }}';
const categoryColors = {
attacker: '#f85149',
bad_crawler: '#f0883e',
good_crawler: '#3fb950',
regular_user: '#58a6ff',
unknown: '#8b949e'
};
const map = L.map('insight-ip-map', {
center: [lat, lng],
zoom: 6,
zoomControl: true,
scrollWheelZoom: true
});
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
attribution: '&copy; CartoDB | &copy; OpenStreetMap contributors',
maxZoom: 19,
subdomains: 'abcd'
}).addTo(map);
const color = categoryColors[category] || '#8b949e';
const markerHtml = `
<div style="
width: 24px;
height: 24px;
background: ${color};
border: 3px solid #fff;
border-radius: 50%;
box-shadow: 0 0 12px ${color}, 0 0 24px ${color}80;
"></div>
`;
const icon = L.divIcon({
html: markerHtml,
iconSize: [24, 24],
className: 'single-ip-marker'
});
L.marker([lat, lng], { icon: icon }).addTo(map);
}, 100);
{% else %}
const mapContainer = document.getElementById('insight-ip-map');
if (mapContainer) {
mapContainer.innerHTML = '<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #8b949e;">Location data not available</div>';
}
{% endif %}
})();
</script>