added search bar feature, refactored the dashboard

This commit is contained in:
BlessedRebuS
2026-02-28 18:43:09 +01:00
parent e87564f694
commit 62bb091926
15 changed files with 478 additions and 18 deletions

View File

@@ -8,6 +8,7 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css" crossorigin="anonymous" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.5.3/MarkerCluster.css" crossorigin="anonymous" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.5.3/MarkerCluster.Default.css" crossorigin="anonymous" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Google+Sans+Flex:wght@400;500;700;900&display=swap" />
<link rel="stylesheet" href="{{ dashboard_path }}/static/css/dashboard.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js" crossorigin="anonymous" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.5.3/leaflet.markercluster.js" crossorigin="anonymous" defer></script>

View File

@@ -31,6 +31,27 @@
{# Stats cards - server-rendered #}
{% include "dashboard/partials/stats_cards.html" %}
{# Search bar #}
<div class="search-bar-container">
<div class="search-bar">
<svg class="search-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clip-rule="evenodd"/>
</svg>
<input id="search-input"
type="search"
name="q"
placeholder="Search attacks, IPs, patterns, locations..."
autocomplete="off"
hx-get="{{ dashboard_path }}/htmx/search"
hx-trigger="input changed delay:300ms, search"
hx-target="#search-results-container"
hx-swap="innerHTML"
hx-indicator="#search-spinner" />
<span id="search-spinner" class="htmx-indicator search-spinner">&#8635;</span>
</div>
<div id="search-results-container"></div>
</div>
{# Tab navigation - Alpine.js #}
<div class="tabs-container">
<a class="tab-button" :class="{ active: tab === 'overview' }" @click.prevent="switchToOverview()" href="#overview">Overview</a>

View File

@@ -74,7 +74,7 @@
</td>
</tr>
{% else %}
<tr><td colspan="7" style="text-align: center;">No attacks detected</td></tr>
<tr><td colspan="7" class="empty-state">No attacks detected</td></tr>
{% endfor %}
</tbody>
</table>

View File

@@ -62,7 +62,7 @@
</td>
</tr>
{% else %}
<tr><td colspan="6" style="text-align: center;">No attackers found</td></tr>
<tr><td colspan="6" class="empty-state">No attackers found</td></tr>
{% endfor %}
</tbody>
</table>

View File

@@ -54,7 +54,7 @@
</td>
</tr>
{% else %}
<tr><td colspan="6" style="text-align: center;">No credentials captured</td></tr>
<tr><td colspan="6" class="empty-state">No credentials captured</td></tr>
{% endfor %}
</tbody>
</table>

View File

@@ -48,7 +48,7 @@
</td>
</tr>
{% else %}
<tr><td colspan="3" style="text-align: center;">No data</td></tr>
<tr><td colspan="3" class="empty-state">No data</td></tr>
{% endfor %}
</tbody>
</table>

View File

@@ -37,7 +37,7 @@
<td>{{ pattern.count }}</td>
</tr>
{% else %}
<tr><td colspan="3" style="text-align: center;">No patterns found</td></tr>
<tr><td colspan="3" class="empty-state">No patterns found</td></tr>
{% endfor %}
</tbody>
</table>

View File

@@ -0,0 +1,158 @@
{# HTMX fragment: Search results for attacks and IPs #}
<div class="search-results">
<div class="search-results-header">
<span class="search-results-summary">
Found <strong>{{ pagination.total_attacks }}</strong> attack{{ 's' if pagination.total_attacks != 1 else '' }}
and <strong>{{ pagination.total_ips }}</strong> IP{{ 's' if pagination.total_ips != 1 else '' }}
for &ldquo;<em>{{ query | e }}</em>&rdquo;
</span>
<button class="search-close-btn" onclick="document.getElementById('search-input').value=''; document.getElementById('search-results-container').innerHTML='';">&times;</button>
</div>
{# ── Matching IPs ─────────────────────────────────── #}
{% if ips %}
<div class="search-section">
<h3 class="search-section-title">Matching IPs</h3>
<table>
<thead>
<tr>
<th>#</th>
<th>IP Address</th>
<th>Requests</th>
<th>Category</th>
<th>Location</th>
<th>ISP / ASN</th>
<th>Last Seen</th>
</tr>
</thead>
<tbody>
{% for ip in ips %}
<tr class="ip-row" data-ip="{{ ip.ip | e }}">
<td class="rank">{{ loop.index + (pagination.page - 1) * pagination.page_size }}</td>
<td class="ip-clickable"
hx-get="{{ dashboard_path }}/htmx/ip-detail/{{ ip.ip | e }}"
hx-target="next .ip-stats-row .ip-stats-dropdown"
hx-swap="innerHTML"
@click="toggleIpDetail($event)">
{{ ip.ip | e }}
</td>
<td>{{ ip.total_requests }}</td>
<td>
{% if ip.category %}
<span class="category-badge category-{{ ip.category | default('unknown') | replace(' ', '-') | lower }}">
{{ ip.category | e }}
</span>
{% else %}
<span class="category-badge category-unknown">unknown</span>
{% endif %}
</td>
<td>{{ ip.city | default('') | e }}{% if ip.city and ip.country_code %}, {% endif %}{{ ip.country_code | default('N/A') | e }}</td>
<td>{{ ip.isp | default(ip.asn_org | default('N/A')) | e }}</td>
<td>{{ ip.last_seen | format_ts }}</td>
</tr>
<tr class="ip-stats-row" style="display: none;">
<td colspan="7" class="ip-stats-cell">
<div class="ip-stats-dropdown">
<div class="loading">Loading stats...</div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{# ── Matching Attacks ─────────────────────────────── #}
{% if attacks %}
<div class="search-section">
<h3 class="search-section-title">Matching Attacks</h3>
<table>
<thead>
<tr>
<th>#</th>
<th>IP Address</th>
<th>Path</th>
<th>Attack Types</th>
<th>User-Agent</th>
<th>Time</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for attack in attacks %}
<tr class="ip-row" data-ip="{{ attack.ip | e }}">
<td class="rank">{{ loop.index + (pagination.page - 1) * pagination.page_size }}</td>
<td class="ip-clickable"
hx-get="{{ dashboard_path }}/htmx/ip-detail/{{ attack.ip | e }}"
hx-target="next .ip-stats-row .ip-stats-dropdown"
hx-swap="innerHTML"
@click="toggleIpDetail($event)">
{{ attack.ip | e }}
</td>
<td>
<div class="path-cell-container">
<span class="path-truncated">{{ attack.path | e }}</span>
{% if attack.path | length > 30 %}
<div class="path-tooltip">{{ attack.path | e }}</div>
{% endif %}
</div>
</td>
<td>
<div class="attack-types-cell">
{% set types_str = attack.attack_types | join(', ') %}
<span class="attack-types-truncated">{{ types_str | e }}</span>
{% if types_str | length > 30 %}
<div class="attack-types-tooltip">{{ types_str | e }}</div>
{% endif %}
</div>
</td>
<td>{{ (attack.user_agent | default(''))[:50] | e }}</td>
<td>{{ attack.timestamp | format_ts }}</td>
<td>
{% if attack.log_id %}
<button class="view-btn" @click="viewRawRequest({{ attack.log_id }})">View Request</button>
{% endif %}
</td>
</tr>
<tr class="ip-stats-row" style="display: none;">
<td colspan="7" class="ip-stats-cell">
<div class="ip-stats-dropdown">
<div class="loading">Loading stats...</div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{# ── Pagination ───────────────────────────────────── #}
{% if pagination.total_pages > 1 %}
<div class="search-pagination">
<span class="pagination-info">Page {{ pagination.page }}/{{ pagination.total_pages }}</span>
<div style="display: flex; gap: 8px;">
<button class="pagination-btn"
hx-get="{{ dashboard_path }}/htmx/search?q={{ query | urlencode }}&page={{ pagination.page - 1 }}"
hx-target="#search-results-container"
hx-swap="innerHTML"
{% if pagination.page <= 1 %}disabled{% endif %}>Prev</button>
<button class="pagination-btn"
hx-get="{{ dashboard_path }}/htmx/search?q={{ query | urlencode }}&page={{ pagination.page + 1 }}"
hx-target="#search-results-container"
hx-swap="innerHTML"
{% if pagination.page >= pagination.total_pages %}disabled{% endif %}>Next</button>
</div>
</div>
{% endif %}
{# ── No results ───────────────────────────────────── #}
{% if not attacks and not ips %}
<div class="search-no-results">
No results found for &ldquo;<em>{{ query | e }}</em>&rdquo;
</div>
{% endif %}
</div>

View File

@@ -32,7 +32,7 @@
</td>
</tr>
{% else %}
<tr><td colspan="4" style="text-align:center;">No suspicious activity detected</td></tr>
<tr><td colspan="4" class="empty-state">No suspicious activity detected</td></tr>
{% endfor %}
</tbody>
</table>

View File

@@ -48,7 +48,7 @@
</td>
</tr>
{% else %}
<tr><td colspan="3" style="text-align: center;">No data</td></tr>
<tr><td colspan="3" class="empty-state">No data</td></tr>
{% endfor %}
</tbody>
</table>

View File

@@ -35,7 +35,7 @@
<td>{{ item.count }}</td>
</tr>
{% else %}
<tr><td colspan="3" style="text-align: center;">No data</td></tr>
<tr><td colspan="3" class="empty-state">No data</td></tr>
{% endfor %}
</tbody>
</table>

View File

@@ -31,11 +31,11 @@
{% for item in items %}
<tr>
<td class="rank">{{ loop.index + (pagination.page - 1) * pagination.page_size }}</td>
<td>{{ item.user_agent | e }}</td>
<td style="font-size: 11px; word-break: break-all; max-width: 400px;">{{ item.user_agent | e }}</td>
<td>{{ item.count }}</td>
</tr>
{% else %}
<tr><td colspan="3" style="text-align: center;">No data</td></tr>
<tr><td colspan="3" class="empty-state">No data</td></tr>
{% endfor %}
</tbody>
</table>