mavweb: unify UI — shared ui.css + nav partial across all pages
One theme (the PWA's dark palette) for dash/history/trace/notifications/ tools/passkey via static/ui.css; shared nav template with active-page highlight; tables wrapped in .scroll so they pan on phones; PWA nav no longer clips the RU/EN toggle; dash 'updated' timestamp fixed (selector matched nothing). AGENTS.md documents the local preview/screenshot recipe. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ASstMtsZWLSRcD1Tq8T68Q
This commit is contained in:
+41
-27
@@ -54,10 +54,23 @@ var traceHTML string
|
||||
//go:embed notifications.html
|
||||
var notificationsHTML string
|
||||
|
||||
// dashTmpl — the monitoring read surface, server-rendered from dash.html (no JS,
|
||||
// no client fetch); meta-refresh keeps it live. html/template escapes the user
|
||||
// text in facts/nudges. Read-only: browses the append-only store via CoreAPI,
|
||||
// never writes — the store IS the audit trail, this just shows it.
|
||||
// navHTML — the shared site nav, parsed into every server-rendered template
|
||||
// alongside ui.css so the pages read as one app. Invoke as
|
||||
// {{template "nav" "<active-page>"}}; the argument highlights the current link.
|
||||
const navHTML = `{{define "nav"}}<nav class=site>
|
||||
<a href=/ class="{{if eq . "voice"}}active{{end}}">voice</a>
|
||||
<a href=/dash class="{{if eq . "dash"}}active{{end}}">dash</a>
|
||||
<a href=/history class="{{if eq . "history"}}active{{end}}">history</a>
|
||||
<a href=/trace class="{{if eq . "trace"}}active{{end}}">trace</a>
|
||||
<a href=/notifications class="{{if eq . "notifications"}}active{{end}}">notifications</a>
|
||||
<a href=/tools class="{{if eq . "tools"}}active{{end}}">tools</a>
|
||||
<a href=/auth/passkey class="{{if eq . "passkey"}}active{{end}}">passkey</a>
|
||||
</nav>{{end}}`
|
||||
|
||||
// dashTmpl — the monitoring read surface, server-rendered from dash.html;
|
||||
// a small fetch loop refreshes the tables in place. html/template escapes the
|
||||
// user text in facts/nudges. Read-only: browses the append-only store via
|
||||
// CoreAPI, never writes — the store IS the audit trail, this just shows it.
|
||||
var dashTmpl = template.Must(template.New("dash").Funcs(template.FuncMap{
|
||||
"ago": func(t time.Time) string {
|
||||
// A zero timestamp (no presence signal yet, fresh DB) would make
|
||||
@@ -67,7 +80,7 @@ var dashTmpl = template.Must(template.New("dash").Funcs(template.FuncMap{
|
||||
}
|
||||
return time.Since(t).Round(time.Second).String() + " ago"
|
||||
},
|
||||
}).Parse(dashHTML))
|
||||
}).Parse(navHTML + dashHTML))
|
||||
|
||||
func noCache(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -352,21 +365,22 @@ func handleDash(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
|
||||
// utterances (they came from voice STT — untrusted text).
|
||||
var toolsTmpl = template.Must(template.New("tools").Funcs(template.FuncMap{
|
||||
"join": strings.Join,
|
||||
}).Parse(toolsHTML))
|
||||
}).Parse(navHTML + toolsHTML))
|
||||
|
||||
const toolsHTML = `<!doctype html><meta charset=utf-8><title>maven · tools</title>
|
||||
<style>body{font:15px system-ui;margin:2rem;max-width:52rem}h2{margin-top:2rem}
|
||||
table{border-collapse:collapse;width:100%}td,th{border:1px solid #ccc;padding:.4rem .6rem;text-align:left}
|
||||
input[type=text]{width:22rem}code{background:#f4f4f4;padding:.1rem .3rem}
|
||||
.d{color:#b00}.msg{background:#efe;border:1px solid #6c6;padding:.5rem;margin:1rem 0}</style>
|
||||
<h1>maven · tools</h1>
|
||||
<p><small>enabling requires step-up — <a href=/auth/passkey>assert a passkey</a> first.</small></p>
|
||||
{{if .Msg}}<div class=msg>{{.Msg}}</div>{{end}}
|
||||
const toolsHTML = `<!doctype html><meta charset=utf-8>
|
||||
<meta name=viewport content="width=device-width,initial-scale=1">
|
||||
<title>maven · tools</title>
|
||||
<link rel=stylesheet href=/ui.css>
|
||||
<style>input[type=text]{width:20rem}</style>
|
||||
{{template "nav" "tools"}}
|
||||
<h1>tools</h1>
|
||||
<p class=muted>enabling requires step-up — <a href=/auth/passkey>assert a passkey</a> first.</p>
|
||||
{{if .Msg}}<div class="msg msg-ok">{{.Msg}}</div>{{end}}
|
||||
<h2>proposed <small>({{len .Proposed}})</small></h2>
|
||||
{{if .Proposed}}<p>maven drafted these from acts she couldn't run. Fill the command (argv, space-separated) and enable.</p>
|
||||
<table><tr><th>name</th><th>scope</th><th>from utterance</th><th>enable as</th></tr>
|
||||
{{if .Proposed}}<p class=muted>maven drafted these from acts she couldn't run. Fill the command (argv, space-separated) and enable.</p>
|
||||
<div class=scroll><table><tr><th>name</th><th>scope</th><th>from utterance</th><th>enable as</th></tr>
|
||||
{{range .Proposed}}<tr>
|
||||
<td><code>{{.Name}}</code></td><td>{{.Scope}}</td><td>{{.Utterance}}</td>
|
||||
<td><code>{{.Name}}</code></td><td><span class=badge>{{.Scope}}</span></td><td>{{.Utterance}}</td>
|
||||
<td><form method=post action=/tools>
|
||||
<input type=hidden name=name value="{{.Name}}">
|
||||
<input type=hidden name=scope value="{{.Scope}}">
|
||||
@@ -374,23 +388,23 @@ input[type=text]{width:22rem}code{background:#f4f4f4;padding:.1rem .3rem}
|
||||
<input type=text name=cmd placeholder="systemctl restart" required>
|
||||
<label><input type=checkbox name=destructive> destructive</label>
|
||||
<button>enable</button></form></td>
|
||||
</tr>{{end}}</table>
|
||||
{{else}}<p>none pending.</p>{{end}}
|
||||
</tr>{{end}}</table></div>
|
||||
{{else}}<p class=muted>none pending.</p>{{end}}
|
||||
<h2>enabled <small>({{len .Enabled}})</small></h2>
|
||||
{{if .Enabled}}<table><tr><th>name</th><th>scope</th><th>command</th><th></th><th></th></tr>
|
||||
{{range .Enabled}}<tr><td><code>{{.Name}}</code></td><td>{{.Scope}}</td><td><code>{{join .Cmd " "}}</code></td>
|
||||
<td>{{if .Destructive}}<span class=d>destructive</span>{{end}}</td>
|
||||
{{if .Enabled}}<div class=scroll><table><tr><th>name</th><th>scope</th><th>command</th><th></th><th></th></tr>
|
||||
{{range .Enabled}}<tr><td><code>{{.Name}}</code></td><td><span class=badge>{{.Scope}}</span></td><td><code>{{join .Cmd " "}}</code></td>
|
||||
<td>{{if .Destructive}}<span class=red>destructive</span>{{end}}</td>
|
||||
<td><form method=post action=/tools style=display:inline>
|
||||
<input type=hidden name=name value="{{.Name}}">
|
||||
<input type=hidden name=scope value="{{.Scope}}">
|
||||
<input type=hidden name=action value=disable>
|
||||
<button>disable</button></form></td></tr>{{end}}</table>
|
||||
{{else}}<p>none enabled.</p>{{end}}
|
||||
<button>disable</button></form></td></tr>{{end}}</table></div>
|
||||
{{else}}<p class=muted>none enabled.</p>{{end}}
|
||||
`
|
||||
|
||||
var historyTmpl = template.Must(template.New("history").Parse(historyHTML))
|
||||
var historyTmpl = template.Must(template.New("history").Parse(navHTML + historyHTML))
|
||||
|
||||
var notificationsTmpl = template.Must(template.New("notifications").Parse(notificationsHTML))
|
||||
var notificationsTmpl = template.Must(template.New("notifications").Parse(navHTML + notificationsHTML))
|
||||
|
||||
var traceTmpl = template.Must(template.New("trace").Funcs(template.FuncMap{
|
||||
"ago": func(t time.Time) string {
|
||||
@@ -406,7 +420,7 @@ var traceTmpl = template.Must(template.New("trace").Funcs(template.FuncMap{
|
||||
return t.Format("15:04:05")
|
||||
},
|
||||
"join": strings.Join,
|
||||
}).Parse(traceHTML))
|
||||
}).Parse(navHTML + traceHTML))
|
||||
|
||||
func handleHistory(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
|
||||
if core == nil {
|
||||
|
||||
Reference in New Issue
Block a user