|
|
|
@@ -54,33 +54,210 @@ var traceHTML string
|
|
|
|
|
//go:embed notifications.html
|
|
|
|
|
var notificationsHTML string
|
|
|
|
|
|
|
|
|
|
// 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}}`
|
|
|
|
|
//go:embed reminders.html
|
|
|
|
|
var remindersHTML string
|
|
|
|
|
|
|
|
|
|
// ── Ethos Workstation Shell ──
|
|
|
|
|
//
|
|
|
|
|
// Two template pieces that wrap every page:
|
|
|
|
|
// {{template "shellTop" "<page-key>"}} ← opens <html>, topbar, sidebar, content
|
|
|
|
|
// {{template "shellBottom"}} ← closes content, inspector, </html>
|
|
|
|
|
//
|
|
|
|
|
// The page-key argument highlights the active sidebar link and sets breadcrumbs.
|
|
|
|
|
|
|
|
|
|
// sidebarSections maps sidebar section → page entries {label, url, icon}
|
|
|
|
|
var sidebarSections = []struct {
|
|
|
|
|
Label string
|
|
|
|
|
Pages []struct{ Label, URL, Key string }
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
Label: "Workspace",
|
|
|
|
|
Pages: []struct{ Label, URL, Key string }{
|
|
|
|
|
{Label: "Dashboard", URL: "/dash", Key: "dash"},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Label: "Infrastructure",
|
|
|
|
|
Pages: []struct{ Label, URL, Key string }{
|
|
|
|
|
{Label: "History", URL: "/history", Key: "history"},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Label: "Automation",
|
|
|
|
|
Pages: []struct{ Label, URL, Key string }{
|
|
|
|
|
{Label: "Rule Trace", URL: "/trace", Key: "trace"},
|
|
|
|
|
{Label: "Notifications", URL: "/notifications", Key: "notifications"},
|
|
|
|
|
{Label: "Reminders", URL: "/reminders", Key: "reminders"},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Label: "AI",
|
|
|
|
|
Pages: []struct{ Label, URL, Key string }{
|
|
|
|
|
{Label: "Voice", URL: "/", Key: "voice"},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Label: "Settings",
|
|
|
|
|
Pages: []struct{ Label, URL, Key string }{
|
|
|
|
|
{Label: "Tools", URL: "/tools", Key: "tools"},
|
|
|
|
|
{Label: "Passkey", URL: "/auth/passkey", Key: "passkey"},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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(`<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 a lucide-style inline SVG icon path for the given page.
|
|
|
|
|
func pageIcon(key string) string {
|
|
|
|
|
switch key {
|
|
|
|
|
case "dash":
|
|
|
|
|
return `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/></svg>`
|
|
|
|
|
case "history":
|
|
|
|
|
return `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>`
|
|
|
|
|
case "trace":
|
|
|
|
|
return `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>`
|
|
|
|
|
case "notifications":
|
|
|
|
|
return `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg>`
|
|
|
|
|
case "reminders":
|
|
|
|
|
return `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>`
|
|
|
|
|
case "voice":
|
|
|
|
|
return `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>`
|
|
|
|
|
case "tools":
|
|
|
|
|
return `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>`
|
|
|
|
|
case "passkey":
|
|
|
|
|
return `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>`
|
|
|
|
|
default:
|
|
|
|
|
return `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"/></svg>`
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// pageTitle returns the human-readable page title for the given key.
|
|
|
|
|
func pageTitle(key string) string {
|
|
|
|
|
switch key {
|
|
|
|
|
case "dash":
|
|
|
|
|
return "Dashboard"
|
|
|
|
|
case "history":
|
|
|
|
|
return "History"
|
|
|
|
|
case "trace":
|
|
|
|
|
return "Rule Trace"
|
|
|
|
|
case "notifications":
|
|
|
|
|
return "Notifications"
|
|
|
|
|
case "reminders":
|
|
|
|
|
return "Reminders"
|
|
|
|
|
case "voice":
|
|
|
|
|
return "Voice"
|
|
|
|
|
case "tools":
|
|
|
|
|
return "Tools"
|
|
|
|
|
case "passkey":
|
|
|
|
|
return "Passkey"
|
|
|
|
|
default:
|
|
|
|
|
return key
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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">
|
|
|
|
|
<title>maven · {{pageTitle .}}</title>
|
|
|
|
|
<link rel=stylesheet href=/ui.css>
|
|
|
|
|
<div class=shell>
|
|
|
|
|
<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 width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></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 width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 7 4 4 7 4"/><polyline points="17 4 20 4 20 7"/><polyline points="20 17 20 20 17 20"/><polyline points="7 20 4 20 4 17"/></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.
|
|
|
|
|
func shellFuncs() template.FuncMap {
|
|
|
|
|
return template.FuncMap{
|
|
|
|
|
"pageTitle": pageTitle,
|
|
|
|
|
"sidebarHTML": sidebarHTML,
|
|
|
|
|
"ago": func(t time.Time) string {
|
|
|
|
|
if t.IsZero() {
|
|
|
|
|
return "never"
|
|
|
|
|
}
|
|
|
|
|
return time.Since(t).Round(time.Second).String() + " ago"
|
|
|
|
|
},
|
|
|
|
|
"connected": func() bool { return true }, // if page renders, core was available
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
// time.Since saturate to ~292y (MaxInt64) and render as garbage.
|
|
|
|
|
if t.IsZero() {
|
|
|
|
|
return "never"
|
|
|
|
|
}
|
|
|
|
|
return time.Since(t).Round(time.Second).String() + " ago"
|
|
|
|
|
},
|
|
|
|
|
}).Parse(navHTML + dashHTML))
|
|
|
|
|
var dashTmpl = template.Must(template.New("dash").Funcs(shellFuncs()).Parse(shellTopHTML + dashHTML + shellBottomHTML))
|
|
|
|
|
|
|
|
|
|
func noCache(h http.Handler) http.Handler {
|
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
@@ -158,6 +335,9 @@ func main() {
|
|
|
|
|
mux.HandleFunc("/notifications", func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
handleNotifications(w, r, core)
|
|
|
|
|
})
|
|
|
|
|
mux.HandleFunc("/reminders", func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
handleReminders(w, r, core)
|
|
|
|
|
})
|
|
|
|
|
// ----- passkey (WebAuthn) endpoints -----
|
|
|
|
|
// Wired when both -core and a configured origin are present. The origin
|
|
|
|
|
// must match the browser's view of mavweb (e.g. https://maven.kvmx.ru).
|
|
|
|
@@ -367,22 +547,19 @@ func handleDash(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
|
|
|
|
|
// toolsTmpl — the enable surface. Server-rendered, no JS: a plain HTML form
|
|
|
|
|
// POSTs back to /tools to enable a proposal. html/template escapes tool names +
|
|
|
|
|
// utterances (they came from voice STT — untrusted text).
|
|
|
|
|
var toolsTmpl = template.Must(template.New("tools").Funcs(template.FuncMap{
|
|
|
|
|
"join": strings.Join,
|
|
|
|
|
}).Parse(navHTML + toolsHTML))
|
|
|
|
|
var toolsTmpl = template.Must(template.New("tools").Funcs(func() template.FuncMap {
|
|
|
|
|
m := shellFuncs()
|
|
|
|
|
m["join"] = strings.Join
|
|
|
|
|
return m
|
|
|
|
|
}()).Parse(shellTopHTML + toolsHTML + shellBottomHTML))
|
|
|
|
|
|
|
|
|
|
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>
|
|
|
|
|
{{template "nav" "tools"}}
|
|
|
|
|
<main class=page>
|
|
|
|
|
<h1>tools</h1>
|
|
|
|
|
<p class=muted>enabling requires step-up — <a href=/auth/passkey>assert a passkey</a> first.</p>
|
|
|
|
|
const toolsHTML = `{{template "shellTop" "tools"}}
|
|
|
|
|
<h1>Tools</h1>
|
|
|
|
|
<p class=hint>enabling requires step-up — <a href=/auth/passkey>assert a passkey</a> first.</p>
|
|
|
|
|
{{if .Msg}}<div class="msg msg-ok">{{.Msg}}</div>{{end}}
|
|
|
|
|
<section class=card>
|
|
|
|
|
<h2 class=card-title>proposed <span class=badge>{{len .Proposed}}</span></h2>
|
|
|
|
|
{{if .Proposed}}<p class=muted>maven drafted these from acts she couldn't run. Fill the command (argv, space-separated) and enable.</p>
|
|
|
|
|
{{if .Proposed}}<p class=hint>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><span class=badge>{{.Scope}}</span></td><td>{{.Utterance}}</td>
|
|
|
|
@@ -394,7 +571,11 @@ const toolsHTML = `<!doctype html><meta charset=utf-8>
|
|
|
|
|
<label><input type=checkbox name=destructive> destructive</label>
|
|
|
|
|
<button class=btn>enable</button></form></td>
|
|
|
|
|
</tr>{{end}}</table></div>
|
|
|
|
|
{{else}}<p class=muted>none pending.</p>{{end}}
|
|
|
|
|
{{else}}<div class=empty style=padding:var(--ethos-space-5)>
|
|
|
|
|
<svg class=icon width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
|
|
|
|
<div>no proposed tools</div>
|
|
|
|
|
<div class=hint>maven will propose tools here when she needs help running an action</div>
|
|
|
|
|
</div>{{end}}
|
|
|
|
|
</section>
|
|
|
|
|
<section class=card>
|
|
|
|
|
<h2 class=card-title>enabled <span class=badge>{{len .Enabled}}</span></h2>
|
|
|
|
@@ -406,32 +587,33 @@ const toolsHTML = `<!doctype html><meta charset=utf-8>
|
|
|
|
|
<input type=hidden name=scope value="{{.Scope}}">
|
|
|
|
|
<input type=hidden name=action value=disable>
|
|
|
|
|
<button class=btn>disable</button></form></td></tr>{{end}}</table></div>
|
|
|
|
|
{{else}}<p class=muted>none enabled.</p>{{end}}
|
|
|
|
|
{{else}}<div class=empty style=padding:var(--ethos-space-5)>
|
|
|
|
|
<svg class=icon width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>
|
|
|
|
|
<div>no tools enabled</div>
|
|
|
|
|
<div class=hint>enable proposed tools above, or ask maven to configure one</div>
|
|
|
|
|
</div>{{end}}
|
|
|
|
|
</section>
|
|
|
|
|
</main>
|
|
|
|
|
`
|
|
|
|
|
{{template "shellBottom"}}`
|
|
|
|
|
|
|
|
|
|
var historyTmpl = template.Must(template.New("history").Parse(navHTML + historyHTML))
|
|
|
|
|
var historyTmpl = template.Must(template.New("history").Funcs(shellFuncs()).Parse(shellTopHTML + historyHTML + shellBottomHTML))
|
|
|
|
|
|
|
|
|
|
var notificationsTmpl = template.Must(template.New("notifications").Parse(navHTML + notificationsHTML))
|
|
|
|
|
var notificationsTmpl = template.Must(template.New("notifications").Funcs(shellFuncs()).Parse(shellTopHTML + notificationsHTML + shellBottomHTML))
|
|
|
|
|
|
|
|
|
|
var passkeyTmpl = template.Must(template.New("passkey").Parse(navHTML + passkeyPageHTML))
|
|
|
|
|
var remindersTmpl = template.Must(template.New("reminders").Funcs(shellFuncs()).Parse(shellTopHTML + remindersHTML + shellBottomHTML))
|
|
|
|
|
|
|
|
|
|
var traceTmpl = template.Must(template.New("trace").Funcs(template.FuncMap{
|
|
|
|
|
"ago": func(t time.Time) string {
|
|
|
|
|
if t.IsZero() {
|
|
|
|
|
return "never"
|
|
|
|
|
}
|
|
|
|
|
return time.Since(t).Round(time.Second).String() + " ago"
|
|
|
|
|
},
|
|
|
|
|
"fmtTime": func(t *time.Time) string {
|
|
|
|
|
var passkeyTmpl = template.Must(template.New("passkey").Funcs(shellFuncs()).Parse(shellTopHTML + passkeyPageHTML + shellBottomHTML))
|
|
|
|
|
|
|
|
|
|
var traceTmpl = template.Must(template.New("trace").Funcs(func() template.FuncMap {
|
|
|
|
|
m := shellFuncs()
|
|
|
|
|
m["fmtTime"] = func(t *time.Time) string {
|
|
|
|
|
if t == nil || t.IsZero() {
|
|
|
|
|
return "—"
|
|
|
|
|
}
|
|
|
|
|
return t.Format("15:04:05")
|
|
|
|
|
},
|
|
|
|
|
"join": strings.Join,
|
|
|
|
|
}).Parse(navHTML + traceHTML))
|
|
|
|
|
}
|
|
|
|
|
m["join"] = strings.Join
|
|
|
|
|
return m
|
|
|
|
|
}()).Parse(shellTopHTML + traceHTML + shellBottomHTML))
|
|
|
|
|
|
|
|
|
|
func handleHistory(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
|
|
|
|
|
if core == nil {
|
|
|
|
@@ -471,6 +653,24 @@ func handleNotifications(w http.ResponseWriter, r *http.Request, core ipc.CoreAP
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func handleReminders(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
|
|
|
|
|
if core == nil {
|
|
|
|
|
http.Error(w, "reminders disabled (no -core)", http.StatusServiceUnavailable)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
ctx := r.Context()
|
|
|
|
|
reminders, err := core.ListReminders(ctx, 50)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Printf("reminders: %v", err)
|
|
|
|
|
http.Error(w, "reminders error: "+err.Error(), http.StatusBadGateway)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
|
|
|
if err := remindersTmpl.Execute(w, map[string]any{"Reminders": reminders}); err != nil {
|
|
|
|
|
log.Printf("reminders template: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func handleTrace(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
|
|
|
|
|
if core == nil {
|
|
|
|
|
http.Error(w, "trace disabled (no -core)", http.StatusServiceUnavailable)
|
|
|
|
|