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:
2026-02-14 20:59:12 +01:00
parent 85e79c97ac
commit 96c8476f77
20 changed files with 1104 additions and 2 deletions

View File

@@ -2,18 +2,22 @@ package main
import (
"context"
"errors"
"flag"
"log/slog"
"net/http"
"os"
"os/signal"
"sync"
"syscall"
"git.t-juice.club/torjus/oubliette/internal/config"
"git.t-juice.club/torjus/oubliette/internal/server"
"git.t-juice.club/torjus/oubliette/internal/storage"
"git.t-juice.club/torjus/oubliette/internal/web"
)
const Version = "0.1.0"
const Version = "0.2.0"
func main() {
configPath := flag.String("config", "oubliette.toml", "path to config file")
@@ -65,10 +69,44 @@ func main() {
os.Exit(1)
}
var wg sync.WaitGroup
// Start web server if enabled.
if cfg.Web.Enabled {
webHandler, err := web.NewServer(store, logger.With("component", "web"))
if err != nil {
logger.Error("failed to create web server", "err", err)
os.Exit(1)
}
httpServer := &http.Server{
Addr: cfg.Web.ListenAddr,
Handler: webHandler,
}
wg.Add(1)
go func() {
defer wg.Done()
logger.Info("web server listening", "addr", cfg.Web.ListenAddr)
if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
logger.Error("web server error", "err", err)
}
}()
// Graceful shutdown on context cancellation.
go func() {
<-ctx.Done()
if err := httpServer.Shutdown(context.Background()); err != nil {
logger.Error("web server shutdown error", "err", err)
}
}()
}
if err := srv.ListenAndServe(ctx); err != nil {
logger.Error("server error", "err", err)
os.Exit(1)
}
wg.Wait()
logger.Info("server stopped")
}