From 5c34fb14f91b97bd6b24bfc6def753ea775f3501 Mon Sep 17 00:00:00 2001 From: kami Date: Sun, 5 Jul 2026 13:20:24 +0400 Subject: [PATCH] mavweb: add notifications history page at /notifications RecentNudges IPC method, store adapter, dispatch, and client proxy. Web UI at /notifications showing recent nudge history with color-coded outcomes, nav links from /dash and /history. --- cmd/mavweb/dash.html | 2 +- cmd/mavweb/handlers_test.go | 58 +++++++++++++++++++++++++++++++++++ cmd/mavweb/history.html | 2 +- cmd/mavweb/main.go | 26 ++++++++++++++++ cmd/mavweb/notifications.html | 49 +++++++++++++++++++++++++++++ 5 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 cmd/mavweb/notifications.html diff --git a/cmd/mavweb/dash.html b/cmd/mavweb/dash.html index b21f013..1aa3f96 100644 --- a/cmd/mavweb/dash.html +++ b/cmd/mavweb/dash.html @@ -10,7 +10,7 @@ .updated{color:#666;font-size:.85rem;margin-top:-.5rem;margin-bottom:1rem} nav a{color:#8cf;margin-right:1rem;font-size:.85rem} - +

presence

{{.Presence.Bucket}} — score {{printf "%.2f" .Presence.Score}} ({{ago .Presence.Updated}})

обновляется каждые 10с
diff --git a/cmd/mavweb/handlers_test.go b/cmd/mavweb/handlers_test.go index c17d387..1dadef1 100644 --- a/cmd/mavweb/handlers_test.go +++ b/cmd/mavweb/handlers_test.go @@ -52,6 +52,9 @@ type fakeCore struct { revertNewID int64 revertErr error + // for handleNotifications tests + nudgesErr error + // for handleHistory tests historyFacts []ipc.Fact historyErr error @@ -115,6 +118,9 @@ func (f *fakeCore) RecentFacts(_ context.Context, _ int) ([]ipc.Fact, error) { } func (f *fakeCore) RecentNudges(_ context.Context, _ int) ([]ipc.Nudge, error) { + if f.nudgesErr != nil { + return nil, f.nudgesErr + } if f.dashErr != nil { return nil, f.dashErr } @@ -549,6 +555,58 @@ func TestHandleDash(t *testing.T) { }) } +// --- handleNotifications --- + +func TestHandleNotifications_NilCore_503(t *testing.T) { + t.Parallel() + rr := httptest.NewRecorder() + handleNotifications(rr, httptest.NewRequest(http.MethodGet, "/notifications", nil), nil) + if rr.Code != http.StatusServiceUnavailable { + t.Errorf("status = %d, want 503", rr.Code) + } +} + +func TestHandleNotifications_CoreError_502(t *testing.T) { + t.Parallel() + core := &fakeCore{nudgesErr: ipc.ErrNudgeNotFound} + rr := httptest.NewRecorder() + handleNotifications(rr, httptest.NewRequest(http.MethodGet, "/notifications", nil), core) + if rr.Code != http.StatusBadGateway { + t.Errorf("status = %d, want 502", rr.Code) + } +} + +func TestHandleNotifications_Renders(t *testing.T) { + t.Parallel() + core := &fakeCore{ + nudges: []ipc.Nudge{ + {Rule: "test-rule", Channel: "telegram", Message: "hello world", Outcome: "pending"}, + {Rule: "other-rule", Channel: "voice", Message: "something happened", Outcome: "acted"}, + }, + } + rr := httptest.NewRecorder() + handleNotifications(rr, httptest.NewRequest(http.MethodGet, "/notifications", nil), core) + if rr.Code != http.StatusOK { + t.Fatalf("status = %d, want 200; body=%s", rr.Code, rr.Body.String()) + } + body := rr.Body.String() + if !strings.Contains(body, "test-rule") { + t.Error("rendered output missing rule name") + } + if !strings.Contains(body, "telegram") { + t.Error("rendered output missing channel") + } + if !strings.Contains(body, "pending") { + t.Error("rendered output missing outcome") + } + if !strings.Contains(body, "hello world") { + t.Error("rendered output missing message") + } + if !strings.Contains(body, "other-rule") { + t.Error("rendered output missing second rule") + } +} + // --- handleHistory --- func TestHandleHistory(t *testing.T) { diff --git a/cmd/mavweb/history.html b/cmd/mavweb/history.html index b3283ad..60a5332 100644 --- a/cmd/mavweb/history.html +++ b/cmd/mavweb/history.html @@ -23,7 +23,7 @@ .msg-err{background:#ffe6e6;display:block!important}

maven · command history

- +

{{len .Facts}} facts shown (newest first)

diff --git a/cmd/mavweb/main.go b/cmd/mavweb/main.go index a0ad818..d07112e 100644 --- a/cmd/mavweb/main.go +++ b/cmd/mavweb/main.go @@ -51,6 +51,9 @@ var historyHTML string //go:embed trace.html 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, @@ -139,6 +142,9 @@ func main() { mux.HandleFunc("/trace", func(w http.ResponseWriter, r *http.Request) { handleTrace(w, r, core) }) + mux.HandleFunc("/notifications", func(w http.ResponseWriter, r *http.Request) { + handleNotifications(w, r, core) + }) mux.HandleFunc("/api/revert", func(w http.ResponseWriter, r *http.Request) { handleRevert(w, r, core) }) @@ -378,6 +384,8 @@ input[type=text]{width:22rem}code{background:#f4f4f4;padding:.1rem .3rem} var historyTmpl = template.Must(template.New("history").Parse(historyHTML)) +var notificationsTmpl = template.Must(template.New("notifications").Parse(notificationsHTML)) + var traceTmpl = template.Must(template.New("trace").Funcs(template.FuncMap{ "ago": func(t time.Time) string { if t.IsZero() { @@ -414,6 +422,24 @@ func handleHistory(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) { } } +func handleNotifications(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) { + if core == nil { + http.Error(w, "notifications disabled (no -core)", http.StatusServiceUnavailable) + return + } + ctx := r.Context() + nudges, err := core.RecentNudges(ctx, 50) + if err != nil { + log.Printf("notifications: %v", err) + http.Error(w, "notifications error: "+err.Error(), http.StatusBadGateway) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if err := notificationsTmpl.Execute(w, map[string]any{"Nudges": nudges}); err != nil { + log.Printf("notifications 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) diff --git a/cmd/mavweb/notifications.html b/cmd/mavweb/notifications.html new file mode 100644 index 0000000..3d1d431 --- /dev/null +++ b/cmd/mavweb/notifications.html @@ -0,0 +1,49 @@ + + + + + +Notifications — Maven + + + + +

Notifications

+{{if .Nudges}}
+ +{{range .Nudges}} + + + + + +{{end}}
whenrulechanneloutcomemessage
{{.Ts.Format "15:04"}}{{.Rule}}{{.Channel}}{{.Outcome}}{{.Message}}
+{{else}}
no notifications yet
{{end}} + +