diff --git a/cmd/mavweb/main.go b/cmd/mavweb/main.go
index 9d8368c..e6c8c0c 100644
--- a/cmd/mavweb/main.go
+++ b/cmd/mavweb/main.go
@@ -75,6 +75,14 @@ var morningHTML string
//go:embed events.html
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 ──
//
// Two template pieces that wrap every page:
@@ -135,74 +143,40 @@ var sidebarSections = []struct {
},
}
-func sidebarActive(url, key string, activeKey string) string {
- if key == activeKey {
- 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(`
`)
- }
- return template.HTML(b.String())
-}
-
-// pageIcon returns an ethos-icons.svg reference for the given page.
+// pageIcon returns the ethos-icons.svg symbol id for the given page. The
+// sidebar template wraps it in the reference.
func pageIcon(key string) string {
switch key {
case "dash":
- return ` `
+ return "i-grid"
case "history":
- return ` `
+ return "i-clock"
case "trace":
- return ` `
+ return "i-wave"
case "notifications":
- return ` `
+ return "i-bell"
case "tasks":
- return ` `
+ return "i-grid"
case "reminders":
- return ` `
+ return "i-calendar"
case "routines":
- return ` `
+ return "i-repeat"
case "morning":
- return ` `
+ return "i-calendar"
case "chat":
- return ` `
+ return "i-message"
case "voice":
- return ` `
+ return "i-mic"
case "ecosystem":
- return ` `
+ return "i-grid"
case "tools":
- return ` `
+ return "i-settings"
case "models":
- return ` `
+ return "i-wave"
case "passkey":
- return ` `
+ return "i-lock"
default:
- return ` `
+ 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" ""}}
-const shellTopHTML = `{{define "shellTop"}}
-
-
-
-maven · {{pageTitle .}}
-
-
-
-
- {{pageTitle .}}
-
-
-
-
- Search
- Ctrl+/
-
-
-
-
-
-
-
-
-
-
-
-
-{{end}}`
-
-// shellBottomHTML closes the content area, inspector, and shell.
-// Usage: {{template "shellBottom"}}
-const shellBottomHTML = `{{define "shellBottom"}}
-
-
-
-
-
-{{end}}`
-
// shellFuncs returns the FuncMap shared by every server-rendered page template.
func shellFuncs() template.FuncMap {
return template.FuncMap{
- "pageTitle": pageTitle,
- "sidebarHTML": sidebarHTML,
+ "pageTitle": pageTitle,
+ "pageIcon": pageIcon,
+ "sidebarSections": func() any { return sidebarSections },
"ago": func(t time.Time) string {
if t.IsZero() {
return "never"
@@ -313,21 +236,21 @@ func shellFuncs() template.FuncMap {
// 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(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
// 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
// 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
// morning routine (internal/morning). Same shape as trace.html: a plain
// server-rendered page, refreshed on reload — no live-update loop, since
// 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 {
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["join"] = strings.Join
return m
-}()).Parse(shellTopHTML + toolsHTML + shellBottomHTML))
+}()).Parse(shellHTML + toolsHTML))
const toolsHTML = `{{template "shellTop" "tools"}}
Tools
@@ -835,19 +758,19 @@ const routinesHTML = `{{template "shellTop" "routines"}}
{{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 {
m := shellFuncs()
@@ -859,7 +782,7 @@ var traceTmpl = template.Must(template.New("trace").Funcs(func() template.FuncMa
}
m["join"] = strings.Join
return m
-}()).Parse(shellTopHTML + traceHTML + shellBottomHTML))
+}()).Parse(shellHTML + traceHTML))
func handleHistory(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
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
// 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"}}
Chat
diff --git a/cmd/mavweb/models.go b/cmd/mavweb/models.go
index 28b598a..ded6af1 100644
--- a/cmd/mavweb/models.go
+++ b/cmd/mavweb/models.go
@@ -31,7 +31,7 @@ type modelController interface {
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"}}
Resident model
diff --git a/cmd/mavweb/shell.html b/cmd/mavweb/shell.html
new file mode 100644
index 0000000..6321ae8
--- /dev/null
+++ b/cmd/mavweb/shell.html
@@ -0,0 +1,49 @@
+{{define "shellTop"}}
+
+
+
+maven · {{pageTitle .}}
+
+
+
+
+ {{pageTitle .}}
+
+
+
+
+ Search
+ Ctrl+/
+
+
+
+
+
+
+
+
+
+
+
+
+{{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}}
+{{end}}{{end}}{{define "shellBottom"}}
+
+
+
+
+
+{{end}}
\ No newline at end of file