mavweb: move the shell partial out of Go into shell.html (V-409)
The eight pages were already embedded .html files. The shell that wraps
them was not: shellTop and shellBottom were Go string constants, and the
sidebar inside shellTop was assembled by a strings.Builder writing
`<div class=sidebar-section>` a fragment at a time. That builder is the
markup-in-Go the review complained about.
shell.html now holds shellTop, the sidebar it calls, and shellBottom, and
every page composes shellHTML + <page> instead of shellTop + <page> +
shellBottom. Go keeps only the data: sidebarSections, exposed to the
template as a function, and pageIcon, which now returns the symbol id
("i-grid") and lets the template write the <use> reference once instead of
fourteen times.
sidebarActive was dead — nothing called it.
Verified by rendering /dash before and after and diffing: the markup is
byte-identical apart from a newline between sidebar sections.
This commit is contained in:
+42
-119
@@ -75,6 +75,14 @@ var morningHTML string
|
|||||||
//go:embed events.html
|
//go:embed events.html
|
||||||
var eventsHTML string
|
var eventsHTML string
|
||||||
|
|
||||||
|
// shellHTML — the shell partial every page is wrapped in: "shellTop", the
|
||||||
|
// "sidebar" it calls, and "shellBottom". It used to be two Go string constants
|
||||||
|
// with the sidebar assembled by a strings.Builder, which is the one piece of
|
||||||
|
// markup that was still concatenated in Go.
|
||||||
|
//
|
||||||
|
//go:embed shell.html
|
||||||
|
var shellHTML string
|
||||||
|
|
||||||
// ── Ethos Workstation Shell ──
|
// ── Ethos Workstation Shell ──
|
||||||
//
|
//
|
||||||
// Two template pieces that wrap every page:
|
// Two template pieces that wrap every page:
|
||||||
@@ -135,74 +143,40 @@ var sidebarSections = []struct {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func sidebarActive(url, key string, activeKey string) string {
|
// pageIcon returns the ethos-icons.svg symbol id for the given page. The
|
||||||
if key == activeKey {
|
// sidebar template wraps it in the <use> reference.
|
||||||
return `class="active"`
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// sidebarHTML renders the sidebar navigation given the active page key.
|
|
||||||
func sidebarHTML(active string) template.HTML {
|
|
||||||
var b strings.Builder
|
|
||||||
for _, sec := range sidebarSections {
|
|
||||||
b.WriteString(`<div class=sidebar-section>`)
|
|
||||||
b.WriteString(`<div class=sidebar-label>`)
|
|
||||||
b.WriteString(sec.Label)
|
|
||||||
b.WriteString(`</div>`)
|
|
||||||
for _, p := range sec.Pages {
|
|
||||||
cls := ""
|
|
||||||
if p.Key == active {
|
|
||||||
cls = ` class="active"`
|
|
||||||
}
|
|
||||||
b.WriteString(`<a href="`)
|
|
||||||
b.WriteString(p.URL)
|
|
||||||
b.WriteString(`"`)
|
|
||||||
b.WriteString(cls)
|
|
||||||
b.WriteString(`><span class=icon>`)
|
|
||||||
b.WriteString(pageIcon(p.Key))
|
|
||||||
b.WriteString(`</span><span>`)
|
|
||||||
b.WriteString(p.Label)
|
|
||||||
b.WriteString(`</span></a>`)
|
|
||||||
}
|
|
||||||
b.WriteString(`</div>`)
|
|
||||||
}
|
|
||||||
return template.HTML(b.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// pageIcon returns an ethos-icons.svg <use> reference for the given page.
|
|
||||||
func pageIcon(key string) string {
|
func pageIcon(key string) string {
|
||||||
switch key {
|
switch key {
|
||||||
case "dash":
|
case "dash":
|
||||||
return `<svg class=icon width="14" height="14"><use href="/ethos-icons.svg#i-grid"/></svg>`
|
return "i-grid"
|
||||||
case "history":
|
case "history":
|
||||||
return `<svg class=icon width="14" height="14"><use href="/ethos-icons.svg#i-clock"/></svg>`
|
return "i-clock"
|
||||||
case "trace":
|
case "trace":
|
||||||
return `<svg class=icon width="14" height="14"><use href="/ethos-icons.svg#i-wave"/></svg>`
|
return "i-wave"
|
||||||
case "notifications":
|
case "notifications":
|
||||||
return `<svg class=icon width="14" height="14"><use href="/ethos-icons.svg#i-bell"/></svg>`
|
return "i-bell"
|
||||||
case "tasks":
|
case "tasks":
|
||||||
return `<svg class=icon width="14" height="14"><use href="/ethos-icons.svg#i-grid"/></svg>`
|
return "i-grid"
|
||||||
case "reminders":
|
case "reminders":
|
||||||
return `<svg class=icon width="14" height="14"><use href="/ethos-icons.svg#i-calendar"/></svg>`
|
return "i-calendar"
|
||||||
case "routines":
|
case "routines":
|
||||||
return `<svg class=icon width="14" height="14"><use href="/ethos-icons.svg#i-repeat"/></svg>`
|
return "i-repeat"
|
||||||
case "morning":
|
case "morning":
|
||||||
return `<svg class=icon width="14" height="14"><use href="/ethos-icons.svg#i-calendar"/></svg>`
|
return "i-calendar"
|
||||||
case "chat":
|
case "chat":
|
||||||
return `<svg class=icon width="14" height="14"><use href="/ethos-icons.svg#i-message"/></svg>`
|
return "i-message"
|
||||||
case "voice":
|
case "voice":
|
||||||
return `<svg class=icon width="14" height="14"><use href="/ethos-icons.svg#i-mic"/></svg>`
|
return "i-mic"
|
||||||
case "ecosystem":
|
case "ecosystem":
|
||||||
return `<svg class=icon width="14" height="14"><use href="/ethos-icons.svg#i-grid"/></svg>`
|
return "i-grid"
|
||||||
case "tools":
|
case "tools":
|
||||||
return `<svg class=icon width="14" height="14"><use href="/ethos-icons.svg#i-settings"/></svg>`
|
return "i-settings"
|
||||||
case "models":
|
case "models":
|
||||||
return `<svg class=icon width="14" height="14"><use href="/ethos-icons.svg#i-wave"/></svg>`
|
return "i-wave"
|
||||||
case "passkey":
|
case "passkey":
|
||||||
return `<svg class=icon width="14" height="14"><use href="/ethos-icons.svg#i-lock"/></svg>`
|
return "i-lock"
|
||||||
default:
|
default:
|
||||||
return `<svg class=icon width="14" height="14"><use href="/ethos-icons.svg#i-search"/></svg>`
|
return "i-search"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,63 +216,12 @@ func pageTitle(key string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// shellTopHTML opens the shell and renders the top bar + sidebar.
|
|
||||||
// Usage: {{template "shellTop" "<page-key>"}}
|
|
||||||
const shellTopHTML = `{{define "shellTop"}}<!doctype html><meta charset=utf-8>
|
|
||||||
<meta name=viewport content="width=device-width,initial-scale=1,viewport-fit=cover">
|
|
||||||
<meta name=theme-color content="#14110D">
|
|
||||||
<link rel=manifest href=/manifest.json>
|
|
||||||
<title>maven · {{pageTitle .}}</title>
|
|
||||||
<link rel=stylesheet href=/ui.css>
|
|
||||||
<div class=shell data-app=maven>
|
|
||||||
<header class=topbar>
|
|
||||||
<div class=breadcrumbs>
|
|
||||||
<span class=current>{{pageTitle .}}</span>
|
|
||||||
</div>
|
|
||||||
<div class=topbar-actions>
|
|
||||||
<div class=search-trigger onclick="window.__openSearch()" role=button tabindex=0>
|
|
||||||
<svg class=icon width="13" height="13"><use href="/ethos-icons.svg#i-search"/></svg>
|
|
||||||
Search
|
|
||||||
<span class=kbd-hint>Ctrl+/</span>
|
|
||||||
</div>
|
|
||||||
<button class=icon-btn onclick="window.__openPalette()" title="Command Palette (Ctrl+K)" aria-label="Command Palette">
|
|
||||||
<svg class=icon width="15" height="15"><use href="/ethos-icons.svg#i-grid"/></svg>
|
|
||||||
</button>
|
|
||||||
<span class=conn-status>
|
|
||||||
<span class="dot online" id=connDot></span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div class=shell-body>
|
|
||||||
<aside class=sidebar>
|
|
||||||
{{sidebarHTML .}}
|
|
||||||
</aside>
|
|
||||||
<main class=content>
|
|
||||||
{{end}}`
|
|
||||||
|
|
||||||
// shellBottomHTML closes the content area, inspector, and shell.
|
|
||||||
// Usage: {{template "shellBottom"}}
|
|
||||||
const shellBottomHTML = `{{define "shellBottom"}}
|
|
||||||
</main>
|
|
||||||
<aside class=inspector id=inspector>
|
|
||||||
<div class=inspector-inner>
|
|
||||||
<div class=inspector-header>
|
|
||||||
<span id=inspectorTitle>Details</span>
|
|
||||||
<button class=inspector-close onclick="closeInspector()" aria-label="Close inspector">×</button>
|
|
||||||
</div>
|
|
||||||
<div class=inspector-body id=inspectorBody></div>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script src=/mavweb.js></script>
|
|
||||||
{{end}}`
|
|
||||||
|
|
||||||
// shellFuncs returns the FuncMap shared by every server-rendered page template.
|
// shellFuncs returns the FuncMap shared by every server-rendered page template.
|
||||||
func shellFuncs() template.FuncMap {
|
func shellFuncs() template.FuncMap {
|
||||||
return template.FuncMap{
|
return template.FuncMap{
|
||||||
"pageTitle": pageTitle,
|
"pageTitle": pageTitle,
|
||||||
"sidebarHTML": sidebarHTML,
|
"pageIcon": pageIcon,
|
||||||
|
"sidebarSections": func() any { return sidebarSections },
|
||||||
"ago": func(t time.Time) string {
|
"ago": func(t time.Time) string {
|
||||||
if t.IsZero() {
|
if t.IsZero() {
|
||||||
return "never"
|
return "never"
|
||||||
@@ -313,21 +236,21 @@ func shellFuncs() template.FuncMap {
|
|||||||
// a small fetch loop refreshes the tables in place. html/template escapes the
|
// 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
|
// 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.
|
// CoreAPI, never writes — the store IS the audit trail, this just shows it.
|
||||||
var dashTmpl = template.Must(template.New("dash").Funcs(shellFuncs()).Parse(shellTopHTML + dashHTML + shellBottomHTML))
|
var dashTmpl = template.Must(template.New("dash").Funcs(shellFuncs()).Parse(shellHTML + dashHTML))
|
||||||
|
|
||||||
// ecosystemTmpl — read-only view of the Nexus/Praxis/Hexis siblings, whose only
|
// ecosystemTmpl — read-only view of the Nexus/Praxis/Hexis siblings, whose only
|
||||||
// human surface is here (they ship no web UI of their own).
|
// human surface is here (they ship no web UI of their own).
|
||||||
var ecosystemTmpl = template.Must(template.New("ecosystem").Funcs(shellFuncs()).Parse(shellTopHTML + ecosystemHTML + shellBottomHTML))
|
var ecosystemTmpl = template.Must(template.New("ecosystem").Funcs(shellFuncs()).Parse(shellHTML + ecosystemHTML))
|
||||||
|
|
||||||
// eventsTmpl — the unified intake journal (Vikunja #283), read-only. Same
|
// eventsTmpl — the unified intake journal (Vikunja #283), read-only. Same
|
||||||
// shape as trace.html and morning.html: server-rendered, refreshed on reload.
|
// shape as trace.html and morning.html: server-rendered, refreshed on reload.
|
||||||
var eventsTmpl = template.Must(template.New("events").Funcs(shellFuncs()).Parse(shellTopHTML + eventsHTML + shellBottomHTML))
|
var eventsTmpl = template.Must(template.New("events").Funcs(shellFuncs()).Parse(shellHTML + eventsHTML))
|
||||||
|
|
||||||
// morningTmpl — read-only view of today's checklist state per configured
|
// morningTmpl — read-only view of today's checklist state per configured
|
||||||
// morning routine (internal/morning). Same shape as trace.html: a plain
|
// morning routine (internal/morning). Same shape as trace.html: a plain
|
||||||
// server-rendered page, refreshed on reload — no live-update loop, since
|
// server-rendered page, refreshed on reload — no live-update loop, since
|
||||||
// checklist state changes on the scale of minutes, not seconds.
|
// checklist state changes on the scale of minutes, not seconds.
|
||||||
var morningTmpl = template.Must(template.New("morning").Funcs(shellFuncs()).Parse(shellTopHTML + morningHTML + shellBottomHTML))
|
var morningTmpl = template.Must(template.New("morning").Funcs(shellFuncs()).Parse(shellHTML + morningHTML))
|
||||||
|
|
||||||
func noCache(h http.Handler) http.Handler {
|
func noCache(h http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -747,7 +670,7 @@ var toolsTmpl = template.Must(template.New("tools").Funcs(func() template.FuncMa
|
|||||||
m := shellFuncs()
|
m := shellFuncs()
|
||||||
m["join"] = strings.Join
|
m["join"] = strings.Join
|
||||||
return m
|
return m
|
||||||
}()).Parse(shellTopHTML + toolsHTML + shellBottomHTML))
|
}()).Parse(shellHTML + toolsHTML))
|
||||||
|
|
||||||
const toolsHTML = `{{template "shellTop" "tools"}}
|
const toolsHTML = `{{template "shellTop" "tools"}}
|
||||||
<h1>Tools</h1>
|
<h1>Tools</h1>
|
||||||
@@ -835,19 +758,19 @@ const routinesHTML = `{{template "shellTop" "routines"}}
|
|||||||
</section>
|
</section>
|
||||||
{{template "shellBottom"}}`
|
{{template "shellBottom"}}`
|
||||||
|
|
||||||
var historyTmpl = template.Must(template.New("history").Funcs(shellFuncs()).Parse(shellTopHTML + historyHTML + shellBottomHTML))
|
var historyTmpl = template.Must(template.New("history").Funcs(shellFuncs()).Parse(shellHTML + historyHTML))
|
||||||
|
|
||||||
var notificationsTmpl = template.Must(template.New("notifications").Funcs(shellFuncs()).Parse(shellTopHTML + notificationsHTML + shellBottomHTML))
|
var notificationsTmpl = template.Must(template.New("notifications").Funcs(shellFuncs()).Parse(shellHTML + notificationsHTML))
|
||||||
|
|
||||||
var remindersTmpl = template.Must(template.New("reminders").Funcs(shellFuncs()).Parse(shellTopHTML + remindersHTML + shellBottomHTML))
|
var remindersTmpl = template.Must(template.New("reminders").Funcs(shellFuncs()).Parse(shellHTML + remindersHTML))
|
||||||
|
|
||||||
var passkeyTmpl = template.Must(template.New("passkey").Funcs(shellFuncs()).Parse(shellTopHTML + passkeyPageHTML + shellBottomHTML))
|
var passkeyTmpl = template.Must(template.New("passkey").Funcs(shellFuncs()).Parse(shellHTML + passkeyPageHTML))
|
||||||
|
|
||||||
var voiceTmpl = template.Must(template.New("voice").Funcs(shellFuncs()).Parse(shellTopHTML + voiceHTML + shellBottomHTML))
|
var voiceTmpl = template.Must(template.New("voice").Funcs(shellFuncs()).Parse(shellHTML + voiceHTML))
|
||||||
|
|
||||||
var tasksTmpl = template.Must(template.New("tasks").Funcs(shellFuncs()).Parse(shellTopHTML + tasksHTML + shellBottomHTML))
|
var tasksTmpl = template.Must(template.New("tasks").Funcs(shellFuncs()).Parse(shellHTML + tasksHTML))
|
||||||
|
|
||||||
var routinesTmpl = template.Must(template.New("routines").Funcs(shellFuncs()).Parse(shellTopHTML + routinesHTML + shellBottomHTML))
|
var routinesTmpl = template.Must(template.New("routines").Funcs(shellFuncs()).Parse(shellHTML + routinesHTML))
|
||||||
|
|
||||||
var traceTmpl = template.Must(template.New("trace").Funcs(func() template.FuncMap {
|
var traceTmpl = template.Must(template.New("trace").Funcs(func() template.FuncMap {
|
||||||
m := shellFuncs()
|
m := shellFuncs()
|
||||||
@@ -859,7 +782,7 @@ var traceTmpl = template.Must(template.New("trace").Funcs(func() template.FuncMa
|
|||||||
}
|
}
|
||||||
m["join"] = strings.Join
|
m["join"] = strings.Join
|
||||||
return m
|
return m
|
||||||
}()).Parse(shellTopHTML + traceHTML + shellBottomHTML))
|
}()).Parse(shellHTML + traceHTML))
|
||||||
|
|
||||||
func handleHistory(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
|
func handleHistory(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
|
||||||
if core == nil {
|
if core == nil {
|
||||||
@@ -1649,7 +1572,7 @@ func handlePTT(w http.ResponseWriter, r *http.Request, voiceAddr string, session
|
|||||||
|
|
||||||
// chatTmpl — plain text conversation interface. No JS: form POSTs to /api/chat
|
// chatTmpl — plain text conversation interface. No JS: form POSTs to /api/chat
|
||||||
// and the handler redirects back to /chat with the response.
|
// and the handler redirects back to /chat with the response.
|
||||||
var chatTmpl = template.Must(template.New("chat").Funcs(shellFuncs()).Parse(shellTopHTML + chatPageHTML + shellBottomHTML))
|
var chatTmpl = template.Must(template.New("chat").Funcs(shellFuncs()).Parse(shellHTML + chatPageHTML))
|
||||||
|
|
||||||
const chatPageHTML = `{{template "shellTop" "chat"}}
|
const chatPageHTML = `{{template "shellTop" "chat"}}
|
||||||
<h1>Chat</h1>
|
<h1>Chat</h1>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ type modelController interface {
|
|||||||
SwapModel(ctx context.Context, req ipc.SwapModelReq) (ipc.SwapModelResp, error)
|
SwapModel(ctx context.Context, req ipc.SwapModelReq) (ipc.SwapModelResp, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
var modelsTmpl = template.Must(template.New("models").Funcs(shellFuncs()).Parse(shellTopHTML + modelsHTML + shellBottomHTML))
|
var modelsTmpl = template.Must(template.New("models").Funcs(shellFuncs()).Parse(shellHTML + modelsHTML))
|
||||||
|
|
||||||
const modelsHTML = `{{template "shellTop" "models"}}
|
const modelsHTML = `{{template "shellTop" "models"}}
|
||||||
<h1>Resident model</h1>
|
<h1>Resident model</h1>
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
{{define "shellTop"}}<!doctype html><meta charset=utf-8>
|
||||||
|
<meta name=viewport content="width=device-width,initial-scale=1,viewport-fit=cover">
|
||||||
|
<meta name=theme-color content="#14110D">
|
||||||
|
<link rel=manifest href=/manifest.json>
|
||||||
|
<title>maven · {{pageTitle .}}</title>
|
||||||
|
<link rel=stylesheet href=/ui.css>
|
||||||
|
<div class=shell data-app=maven>
|
||||||
|
<header class=topbar>
|
||||||
|
<div class=breadcrumbs>
|
||||||
|
<span class=current>{{pageTitle .}}</span>
|
||||||
|
</div>
|
||||||
|
<div class=topbar-actions>
|
||||||
|
<div class=search-trigger onclick="window.__openSearch()" role=button tabindex=0>
|
||||||
|
<svg class=icon width="13" height="13"><use href="/ethos-icons.svg#i-search"/></svg>
|
||||||
|
Search
|
||||||
|
<span class=kbd-hint>Ctrl+/</span>
|
||||||
|
</div>
|
||||||
|
<button class=icon-btn onclick="window.__openPalette()" title="Command Palette (Ctrl+K)" aria-label="Command Palette">
|
||||||
|
<svg class=icon width="15" height="15"><use href="/ethos-icons.svg#i-grid"/></svg>
|
||||||
|
</button>
|
||||||
|
<span class=conn-status>
|
||||||
|
<span class="dot online" id=connDot></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class=shell-body>
|
||||||
|
<aside class=sidebar>
|
||||||
|
{{template "sidebar" .}}
|
||||||
|
</aside>
|
||||||
|
<main class=content>
|
||||||
|
{{end}}{{/* sidebar — the section list, with the active page's link marked. The dot
|
||||||
|
argument is the page key the page passed to shellTop. */}}{{define "sidebar"}}{{$active := .}}{{range sidebarSections}}<div class=sidebar-section><div class=sidebar-label>{{.Label}}</div>
|
||||||
|
{{- range .Pages}}<a href="{{.URL}}"{{if eq .Key $active}} class="active"{{end}}><span class=icon><svg class=icon width="14" height="14"><use href="/ethos-icons.svg#{{pageIcon .Key}}"/></svg></span><span>{{.Label}}</span></a>
|
||||||
|
{{- end}}</div>
|
||||||
|
{{end}}{{end}}{{define "shellBottom"}}
|
||||||
|
</main>
|
||||||
|
<aside class=inspector id=inspector>
|
||||||
|
<div class=inspector-inner>
|
||||||
|
<div class=inspector-header>
|
||||||
|
<span id=inspectorTitle>Details</span>
|
||||||
|
<button class=inspector-close onclick="closeInspector()" aria-label="Close inspector">×</button>
|
||||||
|
</div>
|
||||||
|
<div class=inspector-body id=inspectorBody></div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src=/mavweb.js></script>
|
||||||
|
{{end}}
|
||||||
Reference in New Issue
Block a user