diff --git a/src/dependencies.py b/src/dependencies.py index a713738..e1f908f 100644 --- a/src/dependencies.py +++ b/src/dependencies.py @@ -30,7 +30,7 @@ def get_templates() -> Jinja2Templates: return _templates -def _format_ts(value): +def _format_ts(value, time_only=False): """Custom Jinja2 filter for formatting ISO timestamps.""" if not value: return "N/A" @@ -39,6 +39,8 @@ def _format_ts(value): value = datetime.fromisoformat(value) except (ValueError, TypeError): return value + if time_only: + return value.strftime("%H:%M:%S") if value.date() == datetime.now().date(): return value.strftime("%H:%M:%S") return value.strftime("%m/%d/%Y %H:%M:%S") diff --git a/src/templates/jinja2/dashboard/index.html b/src/templates/jinja2/dashboard/index.html index 97e7ce1..fef46c6 100644 --- a/src/templates/jinja2/dashboard/index.html +++ b/src/templates/jinja2/dashboard/index.html @@ -47,7 +47,7 @@ hx-target="#search-results-container" hx-swap="innerHTML" hx-indicator="#search-spinner" /> - +
diff --git a/src/templates/jinja2/dashboard/ip.html b/src/templates/jinja2/dashboard/ip.html index 8a0e70f..371b771 100644 --- a/src/templates/jinja2/dashboard/ip.html +++ b/src/templates/jinja2/dashboard/ip.html @@ -64,23 +64,11 @@
-

Location

- {% if stats.city %} +

Geo & Network

+ {% if stats.city or stats.country %}
- 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 }}) + Location: + {{ stats.city | default('') | e }}{% if stats.city and stats.country %}, {% endif %}{{ stats.country | default(stats.country_code | default('')) | e }}
{% endif %} {% if stats.timezone %} @@ -89,10 +77,6 @@ {{ stats.timezone | e }}
{% endif %} - - -
-

Network

{% if stats.isp %}
ISP: @@ -120,7 +104,7 @@
-

Flags & Reputation

+

Reputation

{% set flags = [] %} {% if stats.is_proxy %}{% set _ = flags.append('Proxy') %}{% endif %} {% if stats.is_hosting %}{% set _ = flags.append('Hosting') %}{% endif %} @@ -136,16 +120,16 @@ {% endif %} {% if stats.reputation_score is not none %}
- Reputation: + Score: {{ stats.reputation_score }}/100
{% endif %} {% if stats.blocklist_memberships %} -
+
Listed On: -
+
{% for bl in stats.blocklist_memberships %} {{ bl | e }} {% endfor %} @@ -184,7 +168,7 @@ {% if stats.category_history %}

Behavior Timeline

-
+
{% for entry in stats.category_history %}
@@ -240,7 +224,7 @@ const scores = {{ stats.category_scores | tojson }}; const container = document.getElementById('ip-radar-chart'); if (container && typeof generateRadarChart === 'function') { - container.innerHTML = generateRadarChart(scores, 220, true); + container.innerHTML = generateRadarChart(scores, 220, true, 'side'); } {% endif %} diff --git a/src/templates/jinja2/dashboard/partials/ip_insight.html b/src/templates/jinja2/dashboard/partials/ip_insight.html index ae82f61..8c8ab13 100644 --- a/src/templates/jinja2/dashboard/partials/ip_insight.html +++ b/src/templates/jinja2/dashboard/partials/ip_insight.html @@ -48,23 +48,11 @@
-

Location

- {% if stats.city %} +

Geo & Network

+ {% if stats.city or stats.country %}
- 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 }}) + Location: + {{ stats.city | default('') | e }}{% if stats.city and stats.country %}, {% endif %}{{ stats.country | default(stats.country_code | default('')) | e }}
{% endif %} {% if stats.timezone %} @@ -73,10 +61,6 @@ {{ stats.timezone | e }}
{% endif %} -
- -
-

Network

{% if stats.isp %}
ISP: @@ -104,7 +88,7 @@
-

Flags & Reputation

+

Reputation

{% set flags = [] %} {% if stats.is_proxy %}{% set _ = flags.append('Proxy') %}{% endif %} {% if stats.is_hosting %}{% set _ = flags.append('Hosting') %}{% endif %} @@ -120,16 +104,16 @@ {% endif %} {% if stats.reputation_score is not none %}
- Reputation: + Score: {{ stats.reputation_score }}/100
{% endif %} {% if stats.blocklist_memberships %} -
+
Listed On: -
+
{% for bl in stats.blocklist_memberships %} {{ bl | e }} {% endfor %} @@ -163,7 +147,7 @@ {% if stats.category_history %}

Behavior Timeline

-
+
{% for entry in stats.category_history %}
@@ -208,7 +192,7 @@ const scores = {{ stats.category_scores | tojson }}; const container = document.getElementById('insight-radar-chart'); if (container && typeof generateRadarChart === 'function') { - container.innerHTML = generateRadarChart(scores, 220, true); + container.innerHTML = generateRadarChart(scores, 220, true, 'side'); } {% endif %} diff --git a/src/templates/jinja2/dashboard/partials/search_results.html b/src/templates/jinja2/dashboard/partials/search_results.html index a1e4046..1ae0d41 100644 --- a/src/templates/jinja2/dashboard/partials/search_results.html +++ b/src/templates/jinja2/dashboard/partials/search_results.html @@ -24,6 +24,7 @@ Location ISP / ASN Last Seen + @@ -40,7 +41,7 @@ {{ ip.total_requests }} {% if ip.category %} - + {{ ip.category | e }} {% else %} @@ -50,9 +51,14 @@ {{ ip.city | default('') | e }}{% if ip.city and ip.country_code %}, {% endif %}{{ ip.country_code | default('N/A') | e }} {{ ip.isp | default(ip.asn_org | default('N/A')) | e }} {{ ip.last_seen | format_ts }} + + + - +
Loading stats...
diff --git a/src/templates/jinja2/dashboard/partials/top_ua_table.html b/src/templates/jinja2/dashboard/partials/top_ua_table.html index dc38e90..2026005 100644 --- a/src/templates/jinja2/dashboard/partials/top_ua_table.html +++ b/src/templates/jinja2/dashboard/partials/top_ua_table.html @@ -31,7 +31,7 @@ {% for item in items %} {{ loop.index + (pagination.page - 1) * pagination.page_size }} - {{ item.user_agent | e }} + {{ item.user_agent | e }} {{ item.count }} {% else %} diff --git a/src/templates/static/css/dashboard.css b/src/templates/static/css/dashboard.css index a4dae5f..2a58750 100644 --- a/src/templates/static/css/dashboard.css +++ b/src/templates/static/css/dashboard.css @@ -274,14 +274,15 @@ tbody { overflow: visible; } .radar-legend { - margin-top: 10px; + margin-top: 0; font-size: 11px; + flex-shrink: 0; } .radar-legend-item { display: flex; align-items: center; gap: 6px; - margin: 3px 0; + margin: 4px 0; } .radar-legend-color { width: 12px; @@ -445,6 +446,119 @@ tbody { .timeline-marker.bad-crawler { background: #f0883e; } .timeline-marker.regular-user { background: #58a6ff; } .timeline-marker.unknown { background: #8b949e; } + +/* ── IP Insight Page Layout ─────────────────────── */ +.ip-insight-content { + animation: fadeIn 0.3s ease-in; +} +.ip-page-header { + margin-bottom: 24px; +} +.ip-page-header h1 { + display: flex; + align-items: center; + gap: 12px; + margin: 0 0 4px 0; +} +.ip-address-title { + font-size: 28px; + font-weight: 700; + color: #e6edf3; + font-family: monospace; +} +.ip-location-subtitle { + color: #8b949e; + font-size: 14px; + margin: 4px 0 0 0; +} +.ip-page-grid { + display: grid; + grid-template-columns: 3fr 2fr; + gap: 20px; + align-items: start; +} +.ip-page-left, +.ip-page-right { + display: flex; + flex-direction: column; + gap: 20px; +} +.ip-info-card h2 { + margin-top: 0; +} +.ip-info-grid { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 0; +} +.ip-info-section { + padding: 14px 16px; + border-right: 1px solid #21262d; +} +.ip-info-section:last-child { + border-right: none; +} +.ip-info-section h3 { + color: #58a6ff; + font-size: 13px; + font-weight: 600; + margin: 0 0 10px 0; + padding-bottom: 6px; + border-bottom: 1px solid #21262d; +} +.ip-info-section .stat-row { + padding: 3px 0; + font-size: 13px; +} +.blocklist-badges { + display: flex; + flex-wrap: wrap; + gap: 5px; + max-height: 120px; + overflow-y: auto; +} +.ip-flag { + display: inline-block; + background: #1c2128; + border: 1px solid #30363d; + border-radius: 4px; + padding: 2px 8px; + font-size: 11px; + color: #f0883e; + margin-right: 4px; +} +.reputation-score { + font-weight: 700; +} +.reputation-score.bad { color: #f85149; } +.reputation-score.medium { color: #f0883e; } +.reputation-score.good { color: #3fb950; } +.radar-chart-container { + display: flex; + align-items: center; + justify-content: center; + padding: 10px 0; +} +.ip-timeline-scroll { + max-height: 280px; + overflow-y: auto; +} +@media (max-width: 900px) { + .ip-page-grid { + grid-template-columns: 1fr; + } + .ip-info-grid { + grid-template-columns: 1fr; + } + .ip-info-section { + border-right: none; + border-bottom: 1px solid #21262d; + } + .ip-info-section:last-child { + border-bottom: none; + } +} + .tabs-container { border-bottom: 1px solid #30363d; margin-bottom: 30px; @@ -1222,6 +1336,27 @@ tbody { background: #30363d; border-color: #58a6ff; } +.inspect-btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 4px; + background: none; + border: none; + color: #8b949e; + cursor: pointer; + border-radius: 4px; + transition: color 0.2s, background 0.2s; +} +.inspect-btn svg { + width: 16px; + height: 16px; + fill: currentColor; +} +.inspect-btn:hover { + color: #58a6ff; + background: rgba(88, 166, 255, 0.1); +} .pagination-btn { padding: 6px 14px; background: #21262d; @@ -1313,12 +1448,15 @@ tbody { .search-spinner { position: absolute; right: 14px; - font-size: 18px; - color: #58a6ff; - animation: spin 0.8s linear infinite; + width: 16px; + height: 16px; + padding: 0; + border: 2px solid #30363d; + border-top-color: #58a6ff; + border-radius: 50%; + animation: spin 0.6s linear infinite; } @keyframes spin { - from { transform: rotate(0deg); } to { transform: rotate(360deg); } } diff --git a/src/templates/static/js/map.js b/src/templates/static/js/map.js index aa4e5ab..1350bb9 100644 --- a/src/templates/static/js/map.js +++ b/src/templates/static/js/map.js @@ -315,8 +315,13 @@ function buildMapMarkers(ips) { let popupContent = `
-
+
${ip.ip} + +
+
${categoryLabels[category]} @@ -340,23 +345,19 @@ function buildMapMarkers(ips) { `; } - // Add inspect button - popupContent += ` -
- -
- `; - popupContent += '
'; marker.setPopupContent(popupContent); } catch (err) { console.error('Error fetching IP stats:', err); const errorPopup = `
-
+
${ip.ip} + +
+
${categoryLabels[category]} @@ -372,11 +373,6 @@ function buildMapMarkers(ips) {
Failed to load chart: ${err.message}
-
- -
`; marker.setPopupContent(errorPopup); diff --git a/src/templates/static/js/radar.js b/src/templates/static/js/radar.js index f531046..fbe4974 100644 --- a/src/templates/static/js/radar.js +++ b/src/templates/static/js/radar.js @@ -11,11 +11,13 @@ * @param {Object} categoryScores - Object with keys: attacker, good_crawler, bad_crawler, regular_user, unknown * @param {number} [size=200] - Width/height of the SVG in pixels * @param {boolean} [showLegend=true] - Whether to show the legend below the chart + * @param {string} [legendPosition='below'] - 'below' or 'side' (side = legend to the right of the chart) * @returns {string} HTML string containing the SVG radar chart */ -function generateRadarChart(categoryScores, size, showLegend) { +function generateRadarChart(categoryScores, size, showLegend, legendPosition) { size = size || 200; if (showLegend === undefined) showLegend = true; + legendPosition = legendPosition || 'below'; if (!categoryScores || Object.keys(categoryScores).length === 0) { return '
No category data available
'; @@ -55,7 +57,8 @@ function generateRadarChart(categoryScores, size, showLegend) { const cx = 100, cy = 100, maxRadius = 75; - let html = '
'; + const flexDir = legendPosition === 'side' ? 'row' : 'column'; + let html = `
`; html += ``; // Draw concentric circles (grid)