Make delivery and integration failures explicit

Persist reminder presentations and retry state, atomically complete collapsed deliveries, fall back across away reaches, and block permanent failures visibly (V-715, V-678). Fail closed when enabled integrations lack credentials and keep remote arms explicitly dark (V-691). Give mavweb one sanitized, request-correlated error contract (V-689). Owner explicitly requested direct commits to master.
This commit is contained in:
2026-08-13 02:50:59 +04:00
parent da9114b623
commit 35c6ff5a71
67 changed files with 3174 additions and 477 deletions
+20 -19
View File
@@ -3,8 +3,8 @@ package main
import (
"cmp"
_ "embed"
"fmt"
"html/template"
"log"
"net/http"
"strings"
"time"
@@ -73,7 +73,7 @@ var voiceTmpl = parsePage("voice", voiceHTML, nil)
var ecosystemTmpl = parsePage("ecosystem", ecosystemHTML, nil)
func handleDash(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
if !requireCore(w, core, "dash") {
if !requireCore(w, r, core, "dash") {
return
}
ctx := r.Context()
@@ -82,8 +82,8 @@ func handleDash(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
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)
writeProblem(w, r, http.StatusBadGateway, problemCoreReadFailed,
"core read failed", fmt.Errorf("read dashboard: %w", err))
return
}
renderPage(w, dashTmpl, struct {
@@ -95,13 +95,13 @@ func handleDash(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
}
func handleHistory(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
if !requireCore(w, core, "history") {
if !requireCore(w, r, 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)
writeProblem(w, r, http.StatusBadGateway, problemCoreReadFailed,
"core read failed", fmt.Errorf("read fact history: %w", err))
return
}
renderPage(w, historyTmpl, struct {
@@ -110,13 +110,13 @@ func handleHistory(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
}
func handleTrace(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
if !requireCore(w, core, "trace") {
if !requireCore(w, r, 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)
writeProblem(w, r, http.StatusBadGateway, problemCoreReadFailed,
"core read failed", fmt.Errorf("read tick trace: %w", err))
return
}
// The turn records share this page rather than getting one of their own
@@ -127,7 +127,8 @@ func handleTrace(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
// deploy.
turns, err := core.TurnDecisions(r.Context(), 25)
if err != nil {
log.Printf("trace: turn decisions: %v", err)
logProblem(r, http.StatusOK, problemCoreReadFailed,
"turn decisions unavailable", fmt.Errorf("read turn decisions: %w", err))
}
renderPage(w, traceTmpl, traceData{Tick: trace, Turns: turns})
}
@@ -149,14 +150,14 @@ type morningView struct {
}
func handleMorning(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
if !requireCore(w, core, "morning") {
if !requireCore(w, r, 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)
writeProblem(w, r, http.StatusBadGateway, problemCoreReadFailed,
"core read failed", fmt.Errorf("read morning status: %w", err))
return
}
view := morningView{Routines: status}
@@ -165,8 +166,8 @@ func handleMorning(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
// 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()
view.PlanErr = inlineProblem(r, problemCoreReadFailed,
"day plan unavailable", fmt.Errorf("read day plan: %w", err))
} else {
view.Plan = &plan
}
@@ -186,14 +187,14 @@ type eventsView struct {
const eventsPageLimit = 200
func handleEvents(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
if !requireCore(w, core, "intake journal") {
if !requireCore(w, r, 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()
view.Err = inlineProblem(r, problemCoreReadFailed,
"intake journal unavailable", fmt.Errorf("read intake journal: %w", err))
} else {
view.Events = evs
}