2026-02-22 21:53:13 +01:00
|
|
|
{% extends "base.html" %}
|
|
|
|
|
|
|
|
|
|
{% block content %}
|
|
|
|
|
<div class="container" x-data="dashboardApp()" x-init="init()">
|
|
|
|
|
|
|
|
|
|
{# GitHub logo #}
|
|
|
|
|
<a href="https://github.com/BlessedRebuS/Krawl" target="_blank" class="github-logo">
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>
|
|
|
|
|
<span class="github-logo-text">Krawl</span>
|
|
|
|
|
</a>
|
|
|
|
|
|
2026-02-28 17:43:50 +01:00
|
|
|
{# Back to dashboard link #}
|
|
|
|
|
<div style="position: absolute; top: 0; right: 0;">
|
|
|
|
|
<a href="{{ dashboard_path }}/" class="download-btn" style="text-decoration: none;">
|
|
|
|
|
← Back to Dashboard
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{# 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">
|
2026-02-28 19:42:15 +01:00
|
|
|
<h3>Geo & Network</h3>
|
|
|
|
|
{% if stats.city or stats.country %}
|
2026-02-28 17:43:50 +01:00
|
|
|
<div class="stat-row">
|
2026-02-28 19:42:15 +01:00
|
|
|
<span class="stat-label-sm">Location:</span>
|
|
|
|
|
<span class="stat-value-sm">{{ stats.city | default('') | e }}{% if stats.city and stats.country %}, {% endif %}{{ stats.country | default(stats.country_code | default('')) | e }}</span>
|
2026-02-28 17:43:50 +01:00
|
|
|
</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 %}
|
|
|
|
|
{% 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>
|
2026-02-22 21:53:13 +01:00
|
|
|
|
2026-02-28 17:43:50 +01:00
|
|
|
<div class="ip-info-section">
|
2026-02-28 19:42:15 +01:00
|
|
|
<h3>Reputation</h3>
|
2026-02-28 17:43:50 +01:00
|
|
|
{% 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">
|
2026-02-28 19:42:15 +01:00
|
|
|
<span class="stat-label-sm">Score:</span>
|
2026-02-28 17:43:50 +01:00
|
|
|
<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 %}
|
2026-02-28 19:42:15 +01:00
|
|
|
<div class="stat-row" style="flex-direction: column; align-items: flex-start; gap: 6px;">
|
2026-02-28 17:43:50 +01:00
|
|
|
<span class="stat-label-sm">Listed On:</span>
|
2026-02-28 19:42:15 +01:00
|
|
|
<div class="blocklist-badges">
|
2026-02-28 17:43:50 +01:00
|
|
|
{% 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>
|
2026-02-22 21:53:13 +01:00
|
|
|
|
2026-02-28 17:43:50 +01:00
|
|
|
{# Single IP Map #}
|
|
|
|
|
<div class="table-container">
|
|
|
|
|
<h2>Location</h2>
|
|
|
|
|
<div id="single-ip-map" style="height: 300px; border-radius: 6px; border: 1px solid #30363d;"></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="ip-radar-chart"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
|
|
{# Behavior Timeline #}
|
|
|
|
|
{% if stats.category_history %}
|
|
|
|
|
<div class="table-container">
|
|
|
|
|
<h2>Behavior Timeline</h2>
|
2026-02-28 19:42:15 +01:00
|
|
|
<div class="timeline ip-timeline-scroll">
|
2026-02-28 17:43:50 +01:00
|
|
|
{% 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>
|
2026-02-22 21:53:13 +01:00
|
|
|
|
2026-02-28 17:43:50 +01:00
|
|
|
{# Access History table #}
|
|
|
|
|
<div class="table-container alert-section" style="margin-top: 20px;">
|
|
|
|
|
<h2>Access History</h2>
|
2026-02-22 21:53:13 +01:00
|
|
|
<div class="htmx-container"
|
2026-02-28 17:43:50 +01:00
|
|
|
hx-get="{{ dashboard_path }}/htmx/access-logs?page=1&ip_filter={{ ip_address }}"
|
2026-02-22 21:53:13 +01:00
|
|
|
hx-trigger="revealed"
|
|
|
|
|
hx-swap="innerHTML">
|
|
|
|
|
<div class="htmx-indicator">Loading...</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-28 17:43:50 +01:00
|
|
|
{# Raw Request Modal #}
|
|
|
|
|
<div class="raw-request-modal" x-show="rawModal.show" @click.self="closeRawModal()" x-cloak>
|
|
|
|
|
<div class="raw-request-modal-content">
|
|
|
|
|
<div class="raw-request-modal-header">
|
|
|
|
|
<h3>Raw Request</h3>
|
|
|
|
|
<span class="raw-request-modal-close" @click="closeRawModal()">×</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="raw-request-modal-body">
|
|
|
|
|
<pre class="raw-request-content" x-text="rawModal.content"></pre>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="raw-request-modal-footer">
|
|
|
|
|
<button class="raw-request-download-btn" @click="downloadRawRequest()">Download</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-02-22 21:53:13 +01:00
|
|
|
</div>
|
|
|
|
|
{% endblock %}
|
2026-02-28 17:43:50 +01:00
|
|
|
|
|
|
|
|
{% block scripts %}
|
|
|
|
|
<script>
|
|
|
|
|
(function() {
|
|
|
|
|
// Initialize radar chart
|
|
|
|
|
{% if stats.category_scores %}
|
|
|
|
|
const scores = {{ stats.category_scores | tojson }};
|
|
|
|
|
const container = document.getElementById('ip-radar-chart');
|
|
|
|
|
if (container && typeof generateRadarChart === 'function') {
|
2026-02-28 19:42:15 +01:00
|
|
|
container.innerHTML = generateRadarChart(scores, 220, true, 'side');
|
2026-02-28 17:43:50 +01:00
|
|
|
}
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
|
|
|
|
// Initialize single IP map
|
|
|
|
|
{% if stats.latitude and stats.longitude %}
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
setTimeout(function() {
|
|
|
|
|
const mapContainer = document.getElementById('single-ip-map');
|
|
|
|
|
if (!mapContainer || typeof L === 'undefined') return;
|
|
|
|
|
|
|
|
|
|
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('single-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: '© CartoDB | © 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 %}
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
const mapContainer = document.getElementById('single-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>
|
|
|
|
|
{% endblock %}
|