Restyle admin UI to dark theme, fix Docker /data volume permissions

- Rework the admin UI (topbar, sidebar, cards, tables, badges, stat
  tiles) into a dark theme with a light/dark toggle, matching the
  visual style requested for the project.
- Fix Dockerfile: chown /data to the distroless nonroot uid/gid before
  the VOLUME declaration so a freshly created named volume is seeded
  with writable permissions instead of root:root (was causing
  "unable to open database file" on first run).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QTXFrNP1imCW7jtfL5SZrw
This commit is contained in:
2026-08-17 10:37:51 -05:00
co-authored by Claude Sonnet 5
parent ba1f7c4c03
commit affdbfaa41
9 changed files with 483 additions and 67 deletions
+4 -1
View File
@@ -1,6 +1,9 @@
{{define "access_lists"}}{{template "header" .}}
<div class="d-flex justify-content-between align-items-center mb-3">
<input type="text" class="form-control w-auto" placeholder="Search access lists…" data-search-target="aclTable">
<div class="input-icon">
<i class="bi bi-search"></i>
<input type="text" class="form-control" style="width:20rem" placeholder="Search access lists…" data-search-target="aclTable">
</div>
<a href="/access-lists/new" class="btn btn-primary"><i class="bi bi-plus-lg"></i> Add Access List</a>
</div>
<div class="card">
+17 -4
View File
@@ -3,25 +3,38 @@
<div class="row g-3 mb-4">
<div class="col-sm-6 col-lg-3">
<div class="card stat-card h-100"><div class="card-body">
<div class="text-secondary small">Active Proxy Hosts</div>
<div class="d-flex align-items-start justify-content-between">
<div class="stat-label">Active Proxy Hosts</div>
<i class="bi bi-globe2 stat-icon"></i>
</div>
<div class="stat-value">{{$d.ActiveProxyHosts}}</div>
</div></div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card stat-card h-100"><div class="card-body">
<div class="text-secondary small">TCP / UDP Streams</div>
<div class="d-flex align-items-start justify-content-between">
<div class="stat-label">TCP / UDP Streams</div>
<i class="bi bi-arrow-left-right stat-icon"></i>
</div>
<div class="stat-value">{{$d.ActiveTCPStreams}} / {{$d.ActiveUDPStreams}}</div>
</div></div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card stat-card h-100"><div class="card-body">
<div class="text-secondary small">Requests / sec (1h avg)</div>
<div class="d-flex align-items-start justify-content-between">
<div class="stat-label">Requests / sec</div>
<i class="bi bi-speedometer2 stat-icon"></i>
</div>
<div class="stat-value">{{printf "%.2f" $d.RequestsPerSecond}}</div>
<div class="stat-sub">1h average</div>
</div></div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card stat-card h-100"><div class="card-body">
<div class="text-secondary small">Requests (24h)</div>
<div class="d-flex align-items-start justify-content-between">
<div class="stat-label">Requests (24h)</div>
<i class="bi bi-graph-up stat-icon"></i>
</div>
<div class="stat-value">{{$d.TotalRequestsToday}}</div>
</div></div>
</div>
+2 -2
View File
@@ -1,6 +1,6 @@
{{define "footer"}}
</main>
</div>
</div>
</main>
</div>
<script src="/static/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/app.js"></script>
+34 -10
View File
@@ -1,5 +1,5 @@
{{define "header"}}<!doctype html>
<html lang="en" data-bs-theme="light">
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@@ -7,22 +7,46 @@
<link rel="stylesheet" href="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-icons.min.css">
<link rel="stylesheet" href="/static/css/app.css">
<script>
(function () {
var saved = localStorage.getItem("vp-theme");
if (saved === "light" || saved === "dark") {
document.documentElement.setAttribute("data-bs-theme", saved);
}
})();
</script>
</head>
<body>
<header class="vp-topbar d-flex align-items-center justify-content-between">
<a href="/dashboard" class="vp-brand d-flex align-items-center gap-2">
<i class="bi bi-hexagon-fill"></i>
<span>vibeproxy</span>
</a>
<div class="d-flex align-items-center gap-2">
<button type="button" class="btn vp-icon-btn" title="Toggle theme" data-theme-toggle>
<i class="bi bi-moon-stars"></i>
</button>
<span class="vp-user-badge"><i class="bi bi-person-circle"></i> admin</span>
</div>
</header>
<div class="vp-shell">
<nav class="vp-sidebar d-flex flex-column">
<a href="/dashboard" class="vp-brand d-flex align-items-center gap-2">
<i class="bi bi-hexagon-fill"></i>
<span>vibeproxy</span>
</a>
<ul class="nav flex-column vp-nav">
<li class="nav-item"><a class="nav-link {{if eq .Active "dashboard"}}active{{end}}" href="/dashboard"><i class="bi bi-speedometer2"></i> Dashboard</a></li>
<li class="vp-nav-label">Proxy</li>
<li class="nav-item"><a class="nav-link {{if eq .Active "proxy_hosts"}}active{{end}}" href="/proxy-hosts"><i class="bi bi-globe2"></i> Proxy Hosts</a></li>
<li class="nav-item"><a class="nav-link {{if eq .Active "streams"}}active{{end}}" href="/streams"><i class="bi bi-arrow-left-right"></i> TCP/UDP Streams</a></li>
<li class="nav-item"><a class="nav-link {{if eq .Active "backends"}}active{{end}}" href="/backends"><i class="bi bi-hdd-network"></i> Backends</a></li>
<li class="vp-nav-label">Security</li>
<li class="nav-item"><a class="nav-link {{if eq .Active "certificates"}}active{{end}}" href="/certificates"><i class="bi bi-shield-lock"></i> Certificates</a></li>
<li class="nav-item"><a class="nav-link {{if eq .Active "access_lists"}}active{{end}}" href="/access-lists"><i class="bi bi-key"></i> Access Lists</a></li>
<li class="vp-nav-label">Operations</li>
<li class="nav-item"><a class="nav-link {{if eq .Active "logs"}}active{{end}}" href="/logs"><i class="bi bi-journal-text"></i> Logs</a></li>
<li class="vp-nav-label">Settings</li>
<li class="nav-item"><a class="nav-link {{if eq .Active "settings"}}active{{end}}" href="/settings"><i class="bi bi-gear"></i> Settings</a></li>
<li class="nav-item"><a class="nav-link {{if eq .Active "api"}}active{{end}}" href="/api-tokens"><i class="bi bi-code-slash"></i> API</a></li>
</ul>
@@ -30,11 +54,11 @@
<i class="bi bi-box-seam"></i> vibeproxy
</div>
</nav>
<div class="vp-main">
<header class="vp-topbar d-flex align-items-center justify-content-between">
<h1 class="h5 mb-0">{{.Title}}</h1>
</header>
<main class="vp-content">
<main class="vp-main">
<div class="vp-page-header d-flex align-items-center justify-content-between">
<h1 class="h4 mb-0">{{.Title}}</h1>
</div>
<div class="vp-content">
{{if .Flash}}
<div class="alert alert-{{if .FlashKind}}{{.FlashKind}}{{else}}success{{end}} alert-dismissible fade show" role="alert">
{{.Flash}}
+4 -1
View File
@@ -1,7 +1,10 @@
{{define "proxy_hosts"}}{{template "header" .}}
{{$d := .Data}}
<div class="d-flex justify-content-between align-items-center mb-3">
<input type="text" class="form-control w-auto" placeholder="Search proxy hosts…" data-search-target="hostsTable">
<div class="input-icon">
<i class="bi bi-search"></i>
<input type="text" class="form-control" style="width:20rem" placeholder="Search proxy hosts…" data-search-target="hostsTable">
</div>
<a href="/proxy-hosts/new" class="btn btn-primary"><i class="bi bi-plus-lg"></i> Add Proxy Host</a>
</div>
<div class="card">
+4 -1
View File
@@ -1,6 +1,9 @@
{{define "streams"}}{{template "header" .}}
<div class="d-flex justify-content-between align-items-center mb-3">
<input type="text" class="form-control w-auto" placeholder="Search streams…" data-search-target="streamsTable">
<div class="input-icon">
<i class="bi bi-search"></i>
<input type="text" class="form-control" style="width:20rem" placeholder="Search streams…" data-search-target="streamsTable">
</div>
<a href="/streams/new" class="btn btn-primary"><i class="bi bi-plus-lg"></i> Add Stream</a>
</div>
<div class="card">