diff --git a/src/templates/jinja2/dashboard/index.html b/src/templates/jinja2/dashboard/index.html index 7defadf..9e7fee4 100644 --- a/src/templates/jinja2/dashboard/index.html +++ b/src/templates/jinja2/dashboard/index.html @@ -59,7 +59,8 @@ IP Insight - IP Banlist + Tracked IPs + IP Banlist {# Lock icon (not authenticated) #} @@ -197,6 +198,11 @@ + {# ==================== TRACKED IPS TAB (protected, loaded via HTMX with server-side auth) ==================== #} +
+
+
+ {# ==================== IP BANLIST TAB (protected, loaded via HTMX with server-side auth) ==================== #}
diff --git a/src/templates/jinja2/dashboard/partials/_ip_detail.html b/src/templates/jinja2/dashboard/partials/_ip_detail.html index dbdf879..bd5d106 100644 --- a/src/templates/jinja2/dashboard/partials/_ip_detail.html +++ b/src/templates/jinja2/dashboard/partials/_ip_detail.html @@ -12,7 +12,7 @@ {% endif %} - {# Ban/Unban actions — visible only when authenticated #} + {# Ban/Unban + Track/Untrack actions — visible only when authenticated #}
{% if stats.city or stats.country %} diff --git a/src/templates/jinja2/dashboard/partials/tracked_ips_panel.html b/src/templates/jinja2/dashboard/partials/tracked_ips_panel.html new file mode 100644 index 0000000..e671c38 --- /dev/null +++ b/src/templates/jinja2/dashboard/partials/tracked_ips_panel.html @@ -0,0 +1,89 @@ +{# IP Tracking management panel #} +
+ + {# Track IP form #} +
+

Tracked IPs

+

+ Track an IP address to monitor its activity. You can also track IPs from the IP Insight page. +

+
+
+ + +
+ +
+

+
+ + {# Tracked IPs list #} +
+

Currently Tracked

+
+
Loading...
+
+
+
+ + diff --git a/src/templates/jinja2/dashboard/partials/tracked_ips_table.html b/src/templates/jinja2/dashboard/partials/tracked_ips_table.html new file mode 100644 index 0000000..da88f66 --- /dev/null +++ b/src/templates/jinja2/dashboard/partials/tracked_ips_table.html @@ -0,0 +1,55 @@ +{# HTMX fragment: Tracked IPs list #} +
+ Page {{ pagination.page }}/{{ pagination.total_pages }} — {{ pagination.total }} tracked +
+ + +
+
+ + + + + + + + + + + + + + + {% for ip in items %} + + + + + + + + + + + {% else %} + + {% endfor %} + +
#IP AddressCategoryTotal RequestsLocationLast SeenTracked Since
{{ loop.index + (pagination.page - 1) * pagination.page_size }}{{ ip.ip | e }}{{ ip.category | default('unknown') | replace('_', ' ') | title }}{{ ip.total_requests }}{{ ip.city | default('') | e }}{% if ip.city and ip.country_code %}, {% endif %}{{ ip.country_code | default('N/A') | e }}{{ ip.last_seen | format_ts }}{{ ip.tracked_since | format_ts }} + + +
No tracked IPs
diff --git a/src/templates/static/css/dashboard.css b/src/templates/static/css/dashboard.css index 45a3fab..1207e4b 100644 --- a/src/templates/static/css/dashboard.css +++ b/src/templates/static/css/dashboard.css @@ -1338,7 +1338,7 @@ tbody { display: inline-flex; align-items: center; justify-content: center; - padding: 4px; + padding: 6px; background: none; border: none; border-radius: 4px; @@ -1346,12 +1346,12 @@ tbody { transition: color 0.2s, background 0.2s; } .ban-icon-btn svg { - width: 18px; - height: 18px; + width: 24px; + height: 24px; fill: currentColor; } .ban-icon-btn .material-symbols-outlined { - font-size: 20px; + font-size: 26px; } .ban-icon-unban { color: #3fb950; @@ -1392,6 +1392,53 @@ tbody { opacity: 1; } +/* IP Tracking buttons */ +.track-form-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 10px 18px; + background: rgba(88, 166, 255, 0.15); + color: #58a6ff; + border: 1px solid rgba(88, 166, 255, 0.3); + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; +} +.track-form-btn:hover:not(:disabled) { + background: rgba(88, 166, 255, 0.3); + border-color: rgba(88, 166, 255, 0.5); +} +.track-form-btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} +.track-form-btn .material-symbols-outlined { + font-size: 18px; +} +.track-icon-track { + color: #58a6ff; +} +.track-icon-track:hover { + background: rgba(88, 166, 255, 0.15); +} +.track-icon-untrack { + color: #8b949e; +} +.track-icon-untrack:hover { + color: #c9d1d9; + background: rgba(139, 148, 158, 0.15); +} +.track-icon-inspect { + color: #d2a8ff; +} +.track-icon-inspect:hover { + background: rgba(210, 168, 255, 0.15); +} + /* Custom confirm/alert modal */ .krawl-modal-overlay { position: fixed; diff --git a/src/templates/static/js/dashboard.js b/src/templates/static/js/dashboard.js index a1e104e..1ead4a3 100644 --- a/src/templates/static/js/dashboard.js +++ b/src/templates/static/js/dashboard.js @@ -48,6 +48,8 @@ document.addEventListener('alpine:init', () => { this.switchToAttacks(); } else if (h === 'banlist') { if (this.authenticated) this.switchToBanlist(); + } else if (h === 'tracked-ips') { + if (this.authenticated) this.switchToTrackedIps(); } else if (h !== 'ip-insight') { if (this.tab !== 'ip-insight') { this.switchToOverview(); @@ -91,6 +93,21 @@ document.addEventListener('alpine:init', () => { }); }, + switchToTrackedIps() { + if (!this.authenticated) return; + this.tab = 'tracked-ips'; + window.location.hash = '#tracked-ips'; + this.$nextTick(() => { + const container = document.getElementById('tracked-ips-htmx-container'); + if (container && typeof htmx !== 'undefined') { + htmx.ajax('GET', `${this.dashboardPath}/htmx/tracked-ips`, { + target: '#tracked-ips-htmx-container', + swap: 'innerHTML' + }); + } + }); + }, + async logout() { try { await fetch(`${this.dashboardPath}/api/auth/logout`, { @@ -99,7 +116,7 @@ document.addEventListener('alpine:init', () => { }); } catch {} this.authenticated = false; - if (this.tab === 'banlist') this.switchToOverview(); + if (this.tab === 'banlist' || this.tab === 'tracked-ips') this.switchToOverview(); }, promptAuth() { @@ -357,6 +374,43 @@ window.ipBanAction = async function(ip, action) { } }; +// Global track action for IP insight page (auth-gated) +window.ipTrackAction = async function(ip, action) { + const data = getAlpineData('[x-data="dashboardApp()"]'); + if (!data || !data.authenticated) { + if (data && typeof data.promptAuth === 'function') data.promptAuth(); + return; + } + const safeIp = escapeHtml(ip); + const label = action === 'track' ? 'track' : 'untrack'; + const confirmed = await krawlModal.confirm(`Are you sure you want to ${label} IP ${safeIp}?`); + if (!confirmed) return; + try { + const resp = await fetch(`${window.__DASHBOARD_PATH__}/api/track-ip`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'same-origin', + body: JSON.stringify({ ip, action }), + }); + const result = await resp.json().catch(() => ({})); + if (resp.ok) { + krawlModal.success(escapeHtml(result.message || `${label} successful for ${ip}`)); + // Refresh tracked IPs list if visible + const container = document.getElementById('tracked-ips-container'); + if (container && typeof htmx !== 'undefined') { + htmx.ajax('GET', `${window.__DASHBOARD_PATH__}/htmx/tracked-ips/list?page=1`, { + target: '#tracked-ips-container', + swap: 'innerHTML' + }); + } + } else { + krawlModal.error(escapeHtml(result.error || `Failed to ${label} IP ${ip}`)); + } + } catch { + krawlModal.error('Request failed'); + } +}; + // Show/hide ban action buttons based on auth state function updateBanActionVisibility(authenticated) { document.querySelectorAll('.ip-ban-actions').forEach(el => {