c600b426f3
V-409. cmd/mavweb/main.go went from 1868 lines to 265, split along seams that were already there: shell, pages, tasks, routines, the voice proxy, and one file per write surface. The last two markup constants moved into passkey.html and models.html, so no page markup lives in Go any more. Four hand-written shapes became one function each. parsePage and renderPage had 13 copies, requireCore had 12, stepUpGate had 6. The route table lost about 20 identical closures. The parallel title and icon switches merged into one pageChrome table, so a page can no longer have a title and no icon. The server struct that would hold core, session and voiceAddr was left undone. Four test files call the handlers with their full parameter lists, and those signatures are contract. Found and filed V-578: /events has no chrome entry, so its tab is lowercase and its icon falls through to the default. Conflict resolved by hand. V-564 grew /trace a turn-decisions table in main.go while this branch was moving handleTrace to pages.go. The split file wins and V-564's read plus traceData were ported onto it, so the page keeps both tables. --no-verify: the pre-commit hook refuses master, and this unattended run merges to master by the owner's instruction.
206 lines
6.1 KiB
Go
206 lines
6.1 KiB
Go
package main
|
|
|
|
import (
|
|
"cmp"
|
|
_ "embed"
|
|
"html/template"
|
|
"log"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/kami/maven/internal/ipc"
|
|
)
|
|
|
|
// The read-only pages: dash, history, trace, morning, events, and the voice
|
|
// page mounted at "/". Each is GET-only, reads through CoreAPI and renders.
|
|
// The write surfaces live next to their own handlers (tasks.go, tools.go,
|
|
// routines.go, chat.go).
|
|
|
|
//go:embed dash.html
|
|
var dashHTML string
|
|
|
|
//go:embed history.html
|
|
var historyHTML string
|
|
|
|
//go:embed trace.html
|
|
var traceHTML string
|
|
|
|
//go:embed morning.html
|
|
var morningHTML string
|
|
|
|
//go:embed events.html
|
|
var eventsHTML string
|
|
|
|
//go:embed voice.html
|
|
var voiceHTML string
|
|
|
|
//go:embed ecosystem.html
|
|
var ecosystemHTML string
|
|
|
|
// 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 = parsePage("dash", dashHTML, nil)
|
|
|
|
var historyTmpl = parsePage("history", historyHTML, nil)
|
|
|
|
var traceTmpl = parsePage("trace", traceHTML, template.FuncMap{
|
|
"fmtTime": func(t *time.Time) string {
|
|
if t == nil || t.IsZero() {
|
|
return "—"
|
|
}
|
|
return t.Format("15:04:05")
|
|
},
|
|
"join": strings.Join,
|
|
})
|
|
|
|
// 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 = parsePage("morning", morningHTML, nil)
|
|
|
|
// eventsTmpl — the unified intake journal (Vikunja #283), read-only. Same
|
|
// shape as trace.html and morning.html: server-rendered, refreshed on reload.
|
|
var eventsTmpl = parsePage("events", eventsHTML, nil)
|
|
|
|
var voiceTmpl = parsePage("voice", voiceHTML, nil)
|
|
|
|
// 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 = parsePage("ecosystem", ecosystemHTML, nil)
|
|
|
|
func handleDash(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
|
|
if !requireCore(w, core, "dash") {
|
|
return
|
|
}
|
|
ctx := r.Context()
|
|
pres, err1 := core.Presence(ctx)
|
|
facts, err2 := core.RecentFacts(ctx, 50)
|
|
nudges, err3 := core.RecentNudges(ctx, 50)
|
|
notes, err4 := core.RecentNotes(ctx, 50)
|
|
if err := cmp.Or(err1, err2, err3, err4); err != nil {
|
|
log.Printf("dash: %v", err)
|
|
http.Error(w, "core read failed", http.StatusBadGateway)
|
|
return
|
|
}
|
|
renderPage(w, dashTmpl, struct {
|
|
Presence ipc.Presence
|
|
Facts []ipc.Fact
|
|
Nudges []ipc.Nudge
|
|
Notes []ipc.Note
|
|
}{pres, facts, nudges, notes})
|
|
}
|
|
|
|
func handleHistory(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
|
|
if !requireCore(w, core, "history") {
|
|
return
|
|
}
|
|
facts, err := core.RecentFacts(r.Context(), 200)
|
|
if err != nil {
|
|
log.Printf("history: %v", err)
|
|
http.Error(w, "core read failed", http.StatusBadGateway)
|
|
return
|
|
}
|
|
renderPage(w, historyTmpl, struct {
|
|
Facts []ipc.Fact
|
|
}{facts})
|
|
}
|
|
|
|
func handleTrace(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
|
|
if !requireCore(w, core, "trace") {
|
|
return
|
|
}
|
|
trace, err := core.TickTrace(r.Context())
|
|
if err != nil {
|
|
log.Printf("trace: %v", err)
|
|
http.Error(w, "core read failed", http.StatusBadGateway)
|
|
return
|
|
}
|
|
// The turn records share this page rather than getting one of their own
|
|
// (V-564). Both answer the same question, who won and who lost and why, and
|
|
// one is about nudges while the other is about utterances. A read failure
|
|
// here is not fatal to the page. The rule trace above it still renders, and
|
|
// a daemon too old to know the method is the ordinary case during a rolling
|
|
// deploy.
|
|
turns, err := core.TurnDecisions(r.Context(), 25)
|
|
if err != nil {
|
|
log.Printf("trace: turn decisions: %v", err)
|
|
}
|
|
renderPage(w, traceTmpl, traceData{Tick: trace, Turns: turns})
|
|
}
|
|
|
|
// traceData — what trace.html renders: the last tick's rule arbitration and the
|
|
// last turns' claim arbitration.
|
|
type traceData struct {
|
|
Tick ipc.TickTrace
|
|
Turns []ipc.TurnDecision
|
|
}
|
|
|
|
// morningView — what /morning renders: today's plan on top, the checklist
|
|
// state under it. PlanErr is set instead of Plan when the core could not build
|
|
// a plan, so the page says so rather than showing an empty day.
|
|
type morningView struct {
|
|
Plan *ipc.DayPlan
|
|
PlanErr string
|
|
Routines []ipc.MorningRoutineStatus
|
|
}
|
|
|
|
func handleMorning(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
|
|
if !requireCore(w, core, "morning") {
|
|
return
|
|
}
|
|
ctx := r.Context()
|
|
status, err := core.MorningStatus(ctx)
|
|
if err != nil {
|
|
log.Printf("morning: %v", err)
|
|
http.Error(w, "core read failed", http.StatusBadGateway)
|
|
return
|
|
}
|
|
view := morningView{Routines: status}
|
|
// The day plan (#128) shows on this page because it is the same question at
|
|
// a different scale. A plan read that fails must not take the checklist
|
|
// down with it — the page degrades to what it had before.
|
|
plan, err := core.DayPlan(ctx)
|
|
if err != nil {
|
|
log.Printf("morning: day plan: %v", err)
|
|
view.PlanErr = err.Error()
|
|
} else {
|
|
view.Plan = &plan
|
|
}
|
|
renderPage(w, morningTmpl, view)
|
|
}
|
|
|
|
// eventsView — what /events renders. Err is set instead of Events when the
|
|
// core could not serve the journal, so the page says why rather than showing an
|
|
// empty intake and implying nothing arrived.
|
|
type eventsView struct {
|
|
Events []ipc.IntakeEvent
|
|
Err string
|
|
}
|
|
|
|
// eventsPageLimit — how many envelopes the page shows. The ring holds more; a
|
|
// page is for scanning what just happened, not for archaeology.
|
|
const eventsPageLimit = 200
|
|
|
|
func handleEvents(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
|
|
if !requireCore(w, core, "intake journal") {
|
|
return
|
|
}
|
|
var view eventsView
|
|
evs, err := core.RecentEvents(r.Context(), eventsPageLimit)
|
|
if err != nil {
|
|
log.Printf("events: %v", err)
|
|
view.Err = err.Error()
|
|
} else {
|
|
view.Events = evs
|
|
}
|
|
renderPage(w, eventsTmpl, view)
|
|
}
|
|
|
|
func handleVoice(w http.ResponseWriter, r *http.Request) {
|
|
renderPage(w, voiceTmpl, nil)
|
|
}
|