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}}
+| when | rule | channel | outcome | message |
+{{range .Nudges}}
+| {{.Ts.Format "15:04"}} |
+{{.Rule}} |
+{{.Channel}} |
+{{.Outcome}} |
+{{.Message}} |
+
{{end}}
+{{else}}no notifications yet
{{end}}
+
+