feat: add charts, world map, and filters to web dashboard
Add Chart.js line/bar charts for attack trends (attempts over time, hourly pattern), an SVG world map choropleth colored by attack origin country, and a collapsible filter form (date range, IP, country, username) that narrows both charts and top-N tables. New store methods: GetAttemptsOverTime, GetHourlyPattern, GetCountryStats, and filtered variants of dashboard stats/top-N queries. New JSON API endpoints at /api/charts/* and an htmx fragment at /fragments/dashboard-content for filtered table updates. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,94 @@
|
||||
{{template "stats" .Stats}}
|
||||
</section>
|
||||
|
||||
<details>
|
||||
<summary>Filters</summary>
|
||||
<form id="filter-form">
|
||||
<div class="grid">
|
||||
<label>Since <input type="date" name="since"></label>
|
||||
<label>Until <input type="date" name="until"></label>
|
||||
<label>IP <input type="text" name="ip" placeholder="10.0.0.1"></label>
|
||||
<label>Country <input type="text" name="country" placeholder="CN" maxlength="2"></label>
|
||||
<label>Username <input type="text" name="username" placeholder="root"></label>
|
||||
</div>
|
||||
<button type="submit">Apply</button>
|
||||
<button type="button" class="secondary" onclick="clearFilters()">Clear</button>
|
||||
</form>
|
||||
</details>
|
||||
|
||||
<section>
|
||||
<h3>Attack Trends</h3>
|
||||
<div class="grid">
|
||||
<article>
|
||||
<header>Attempts Over Time</header>
|
||||
<canvas id="chart-attempts"></canvas>
|
||||
</article>
|
||||
<article>
|
||||
<header>Hourly Pattern (UTC)</header>
|
||||
<canvas id="chart-hourly"></canvas>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>Attack Origins</h3>
|
||||
<article>
|
||||
<div id="world-map"></div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<div id="dashboard-content">
|
||||
{{template "dashboard_content" .}}
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h3>Active Sessions</h3>
|
||||
<div id="active-sessions" hx-get="/fragments/active-sessions" hx-trigger="every 10s" hx-swap="innerHTML">
|
||||
{{template "active_sessions" .ActiveSessions}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>Recent Sessions</h3>
|
||||
<table id="recent-sessions-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>IP</th>
|
||||
<th>Country</th>
|
||||
<th>Username</th>
|
||||
<th>Type</th>
|
||||
<th>Score</th>
|
||||
<th>Connected</th>
|
||||
<th>Disconnected</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .RecentSessions}}
|
||||
<tr>
|
||||
<td><a href="/sessions/{{.ID}}"><code>{{truncateID .ID}}</code></a>{{if gt .EventCount 0}} <mark>replay</mark>{{end}}</td>
|
||||
<td>{{.IP}}</td>
|
||||
<td>{{.Country}}</td>
|
||||
<td>{{.Username}}</td>
|
||||
<td>{{if .ExecCommand}}<mark>exec</mark>{{else}}{{.ShellName}}{{end}}</td>
|
||||
<td>{{if .HumanScore}}{{if gt (derefFloat .HumanScore) 0.6}}<mark>{{formatScore .HumanScore}}</mark>{{else}}{{formatScore .HumanScore}}{{end}}{{else}}-{{end}}</td>
|
||||
<td>{{formatTime .ConnectedAt}}</td>
|
||||
<td>{{if .DisconnectedAt}}{{formatTime (derefTime .DisconnectedAt)}}{{else}}<mark>active</mark>{{end}}</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="8">No sessions</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
{{define "scripts"}}
|
||||
<script src="/static/chart.min.js"></script>
|
||||
<script src="/static/dashboard.js"></script>
|
||||
{{end}}
|
||||
|
||||
{{define "dashboard_content"}}
|
||||
<section>
|
||||
<h3>Top Credentials & IPs</h3>
|
||||
<div class="top-grid">
|
||||
@@ -83,45 +171,4 @@
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>Active Sessions</h3>
|
||||
<div id="active-sessions" hx-get="/fragments/active-sessions" hx-trigger="every 10s" hx-swap="innerHTML">
|
||||
{{template "active_sessions" .ActiveSessions}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>Recent Sessions</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>IP</th>
|
||||
<th>Country</th>
|
||||
<th>Username</th>
|
||||
<th>Type</th>
|
||||
<th>Score</th>
|
||||
<th>Connected</th>
|
||||
<th>Disconnected</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .RecentSessions}}
|
||||
<tr>
|
||||
<td><a href="/sessions/{{.ID}}"><code>{{truncateID .ID}}</code></a>{{if gt .EventCount 0}} <mark>replay</mark>{{end}}</td>
|
||||
<td>{{.IP}}</td>
|
||||
<td>{{.Country}}</td>
|
||||
<td>{{.Username}}</td>
|
||||
<td>{{if .ExecCommand}}<mark>exec</mark>{{else}}{{.ShellName}}{{end}}</td>
|
||||
<td>{{if .HumanScore}}{{if gt (derefFloat .HumanScore) 0.6}}<mark>{{formatScore .HumanScore}}</mark>{{else}}{{formatScore .HumanScore}}{{end}}{{else}}-{{end}}</td>
|
||||
<td>{{formatTime .ConnectedAt}}</td>
|
||||
<td>{{if .DisconnectedAt}}{{formatTime (derefTime .DisconnectedAt)}}{{else}}<mark>active</mark>{{end}}</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="8">No sessions</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
}
|
||||
#world-map svg { width: 100%; height: auto; }
|
||||
#world-map svg path { fill: #2a2a3e; stroke: #555; stroke-width: 0.5; transition: fill 0.2s; }
|
||||
#world-map svg path:hover { stroke: #fff; stroke-width: 1; }
|
||||
nav h1 {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -56,5 +59,6 @@
|
||||
<main class="container">
|
||||
{{block "content" .}}{{end}}
|
||||
</main>
|
||||
{{block "scripts" .}}{{end}}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user