feat: add minimal web dashboard with stats, top credentials, and sessions
Implements Phase 1.5 — an embedded web UI using Go templates, Pico CSS (dark theme), and htmx for auto-refreshing stats and active sessions. Adds read query methods to the Store interface (GetDashboardStats, GetTopUsernames, GetTopPasswords, GetTopIPs, GetRecentSessions) with implementations for both SQLite and MemoryStore. Introduces the internal/web package with server, handlers, templates, and tests. Web server is opt-in via [web] config section and runs alongside SSH with graceful shutdown. Bumps version to 0.2.0. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
93
internal/web/templates/dashboard.html
Normal file
93
internal/web/templates/dashboard.html
Normal file
@@ -0,0 +1,93 @@
|
||||
{{define "content"}}
|
||||
<section id="stats-section" hx-get="/fragments/stats" hx-trigger="every 30s" hx-swap="innerHTML">
|
||||
{{template "stats" .Stats}}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>Top Credentials & IPs</h3>
|
||||
<div class="top-grid">
|
||||
<article>
|
||||
<header>Top Usernames</header>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Username</th><th>Attempts</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .TopUsernames}}
|
||||
<tr><td>{{.Value}}</td><td>{{.Count}}</td></tr>
|
||||
{{else}}
|
||||
<tr><td colspan="2">No data</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</article>
|
||||
<article>
|
||||
<header>Top Passwords</header>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Password</th><th>Attempts</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .TopPasswords}}
|
||||
<tr><td>{{.Value}}</td><td>{{.Count}}</td></tr>
|
||||
{{else}}
|
||||
<tr><td colspan="2">No data</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</article>
|
||||
<article>
|
||||
<header>Top IPs</header>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>IP</th><th>Attempts</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .TopIPs}}
|
||||
<tr><td>{{.Value}}</td><td>{{.Count}}</td></tr>
|
||||
{{else}}
|
||||
<tr><td colspan="2">No data</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</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>Username</th>
|
||||
<th>Shell</th>
|
||||
<th>Connected</th>
|
||||
<th>Disconnected</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .RecentSessions}}
|
||||
<tr>
|
||||
<td><code>{{truncateID .ID}}</code></td>
|
||||
<td>{{.IP}}</td>
|
||||
<td>{{.Username}}</td>
|
||||
<td>{{.ShellName}}</td>
|
||||
<td>{{formatTime .ConnectedAt}}</td>
|
||||
<td>{{if .DisconnectedAt}}{{formatTime (derefTime .DisconnectedAt)}}{{else}}<mark>active</mark>{{end}}</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="6">No sessions</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
{{end}}
|
||||
26
internal/web/templates/fragments/active_sessions.html
Normal file
26
internal/web/templates/fragments/active_sessions.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{{define "active_sessions"}}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>IP</th>
|
||||
<th>Username</th>
|
||||
<th>Shell</th>
|
||||
<th>Connected</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .}}
|
||||
<tr>
|
||||
<td><code>{{truncateID .ID}}</code></td>
|
||||
<td>{{.IP}}</td>
|
||||
<td>{{.Username}}</td>
|
||||
<td>{{.ShellName}}</td>
|
||||
<td>{{formatTime .ConnectedAt}}</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="5">No active sessions</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{end}}
|
||||
20
internal/web/templates/fragments/stats.html
Normal file
20
internal/web/templates/fragments/stats.html
Normal file
@@ -0,0 +1,20 @@
|
||||
{{define "stats"}}
|
||||
<div class="stats-grid">
|
||||
<article class="stat-card">
|
||||
<h2>{{.TotalAttempts}}</h2>
|
||||
<p>Total Attempts</p>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<h2>{{.UniqueIPs}}</h2>
|
||||
<p>Unique IPs</p>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<h2>{{.TotalSessions}}</h2>
|
||||
<p>Total Sessions</p>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<h2>{{.ActiveSessions}}</h2>
|
||||
<p>Active Sessions</p>
|
||||
</article>
|
||||
</div>
|
||||
{{end}}
|
||||
56
internal/web/templates/layout.html
Normal file
56
internal/web/templates/layout.html
Normal file
@@ -0,0 +1,56 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Oubliette</title>
|
||||
<link rel="stylesheet" href="/static/pico.min.css">
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--pico-font-size: 16px;
|
||||
}
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
.stat-card {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
.stat-card h2 {
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 2rem;
|
||||
}
|
||||
.stat-card p {
|
||||
margin: 0;
|
||||
color: var(--pico-muted-color);
|
||||
}
|
||||
.top-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
nav h1 {
|
||||
margin: 0;
|
||||
}
|
||||
nav small {
|
||||
color: var(--pico-muted-color);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="container">
|
||||
<ul>
|
||||
<li><h1>Oubliette</h1></li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li><small>SSH Honeypot Dashboard</small></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<main class="container">
|
||||
{{block "content" .}}{{end}}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user