Phase 1: Control Service skeleton (Go, SQLite, session login, Docker, Caddy)

- Go service with its own multi-stage Dockerfile and docker-compose.yml; runs
  as container webrestrict-control on port 8791.
- SQLite schema for the full AGENTS.md §7 data model via versioned migrations
  (PRAGMA user_version); audit log is append-only.
- Configuration from the process environment; the real env file is gitignored
  and a template is committed. No secrets in the repo.
- Separate session login (signed HMAC cookie + CSRF) for Sam.
- GET /health; HTMX + server-rendered dashboard styled with the Family Home Lab
  tokens.css (copied) and Inter fonts; dark mode; phone-friendly.
- Caddy vhost for webrestrict.lab.audasmedia.com.au applied on .35 (Let's
  Encrypt cert issued); route recorded in deploy/caddy/webrestrict.caddy.
- Unit tests for config, auth and store.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-10-13 14:11:38 +11:00
parent 5368225f23
commit c83c9df0a3
29 changed files with 2119 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
{{define "dashboard.html"}}<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<title>{{.Title}} — Internet Use Control</title>
<link rel="stylesheet" href="/static/tokens.css">
<link rel="stylesheet" href="/static/app.css">
<script src="/static/htmx.min.js" defer></script>
</head>
<body class="app">
<nav class="nav-bar">
<a class="nav-bar__brand" href="/">
<span class="nav-bar__mark" aria-hidden="true"></span>
Internet Use Control
</a>
<div class="nav-bar__right">
<span class="nav-bar__user">{{.Username}}</span>
<form method="post" action="/logout" class="nav-bar__logout">
<input type="hidden" name="csrf" value="{{.CSRF}}">
<button class="button-utility" type="submit">Sign out</button>
</form>
</div>
</nav>
<main class="container">
<header class="page-head">
<h1 class="heading-1">Dashboard</h1>
<p class="page-head__sub">Phase 1 skeleton — the Control Service is up and answering.</p>
</header>
{{template "status.html" .}}
<section class="section" aria-labelledby="devices-title">
<div class="section__head">
<h2 id="devices-title" class="heading-2">Devices</h2>
<span class="category-pill"><span class="dot dot--faint"></span>0 registered</span>
</div>
<div class="empty-state">
<p class="empty-state__title">No devices yet</p>
<p class="empty-state__body">Device registration arrives in Phase 2. Until then the
dashboard, session login and <code>/health</code> are the working pieces.</p>
</div>
</section>
</main>
<footer class="app-foot">
<span>Control Service · {{.Version}} · {{.AppEnv}}</span>
<span class="app-foot__sep">·</span>
<span>{{year}} Family Home Lab</span>
</footer>
</body>
</html>{{end}}

View File

@@ -0,0 +1,40 @@
{{define "login.html"}}<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<title>Sign in — Internet Use Control</title>
<link rel="stylesheet" href="/static/tokens.css">
<link rel="stylesheet" href="/static/app.css">
</head>
<body class="login-page">
<main class="login-band">
<div class="login-band__inner">
<p class="login-eyebrow">Internet Use Control</p>
<h1 class="login-headline">Good to see you,<br>Sam.</h1>
<p class="login-sub">Sign in to manage the kids' devices.</p>
</div>
<section class="auth-card" aria-labelledby="signin-title">
<h2 id="signin-title" class="auth-card__title">Sign in</h2>
{{if .Error}}<p class="auth-error" role="alert">{{.Error}}</p>{{end}}
<form method="post" action="/login" class="auth-form" autocomplete="on">
<input type="hidden" name="csrf" value="{{.CSRF}}">
<label class="field">
<span class="field__label">Username</span>
<input class="text-input" type="text" name="username" value="{{.Username}}"
autocomplete="username" autocapitalize="none" spellcheck="false" required>
</label>
<label class="field">
<span class="field__label">Password</span>
<input class="text-input" type="password" name="password"
autocomplete="current-password" required>
</label>
<button class="button-primary" type="submit">Sign in</button>
</form>
</section>
</main>
<footer class="login-foot">Family Home Lab · Control Service</footer>
</body>
</html>{{end}}

View File

@@ -0,0 +1,34 @@
{{define "status.html"}}<section class="card-grid" id="status" hx-get="/partials/status" hx-trigger="every 10s" hx-swap="outerHTML">
<article class="tool-card">
<div class="tool-card__icon tool-card__icon--green" aria-hidden="true">●</div>
<h3 class="tool-card__title">Service</h3>
<p class="tool-card__desc">
{{if .DBHealthy}}Running{{else}}Database error{{end}}
</p>
<span class="status-pill {{if .DBHealthy}}status-pill--on{{else}}status-pill--off{{end}}">
<span class="dot {{if .DBHealthy}}dot--on{{else}}dot--off{{end}}"></span>
{{if .DBHealthy}}online{{else}}offline{{end}}
</span>
</article>
<article class="tool-card">
<div class="tool-card__icon tool-card__icon--sky" aria-hidden="true">▤</div>
<h3 class="tool-card__title">Database</h3>
<p class="tool-card__desc">SQLite · source of truth</p>
<span class="status-pill status-pill--muted">{{if .DBHealthy}}ok{{else}}error{{end}}</span>
</article>
<article class="tool-card">
<div class="tool-card__icon tool-card__icon--purple" aria-hidden="true">⏱</div>
<h3 class="tool-card__title">Uptime</h3>
<p class="tool-card__desc">{{.ServerUp}}</p>
<span class="status-pill status-pill--muted">since boot</span>
</article>
<article class="tool-card">
<div class="tool-card__icon tool-card__icon--orange" aria-hidden="true">⌚</div>
<h3 class="tool-card__title">Server time</h3>
<p class="tool-card__desc">{{.ServerTime}}</p>
<span class="status-pill status-pill--muted">local</span>
</article>
</section>{{end}}