35c6ff5a71
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.
85 lines
2.4 KiB
Go
85 lines
2.4 KiB
Go
package main
|
|
|
|
import (
|
|
_ "embed"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/kami/maven/internal/ipc"
|
|
)
|
|
|
|
//go:embed reminders.html
|
|
var remindersHTML string
|
|
|
|
var remindersTmpl = parsePage("reminders", remindersHTML, nil)
|
|
|
|
// reminderRow is one line on /reminders, with the payload unwrapped and both
|
|
// timestamps already in his clock.
|
|
//
|
|
// The page rendered `{{.Payload}}` and the UTC instant, so a reminder read
|
|
// `{"text":"выпить таблетки"}` and fired an hour off what he was told
|
|
// (Vikunja #469). Neither is a formatting nicety: the envelope is an internal
|
|
// shape he never chose, and a time on a page he reads is the time on his wall.
|
|
type reminderRow struct {
|
|
Created string
|
|
Fires string
|
|
Status string
|
|
Detail string
|
|
Text string
|
|
}
|
|
|
|
// reminderText unwraps the {"text":...} payload the router writes.
|
|
//
|
|
// A copy of store.ReminderText rather than a call to it, because mavweb is one
|
|
// of the pure-Go daemons and internal/store carries the CGO sqlite driver. The
|
|
// ipc DTO is decoupled from the store on purpose, so the unwrap belongs to
|
|
// whoever renders it. Payload that is not that shape is shown as he said it.
|
|
func reminderText(payload string) string {
|
|
var m map[string]any
|
|
if err := json.Unmarshal([]byte(payload), &m); err == nil {
|
|
if t, ok := m["text"]; ok {
|
|
if s, isStr := t.(string); isStr && s != "" {
|
|
return s
|
|
}
|
|
}
|
|
}
|
|
return strings.TrimSpace(payload)
|
|
}
|
|
|
|
func reminderRows(rs []ipc.Reminder) []reminderRow {
|
|
out := make([]reminderRow, 0, len(rs))
|
|
for _, r := range rs {
|
|
status := r.Status
|
|
detail := ""
|
|
if !r.DeliveryBlockedTs.IsZero() {
|
|
status = "blocked"
|
|
detail = r.DeliveryBlockedError
|
|
} else if r.DeliveryAttempts > 0 && !r.NextAttemptTs.IsZero() {
|
|
detail = "retry " + r.NextAttemptTs.Local().Format("02 Jan 15:04")
|
|
}
|
|
out = append(out, reminderRow{
|
|
Created: r.CreatedTs.Local().Format("02 Jan 15:04"),
|
|
Fires: r.FireTs.Local().Format("02 Jan 15:04"),
|
|
Status: status,
|
|
Detail: detail,
|
|
Text: reminderText(r.Payload),
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func handleReminders(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
|
|
if !requireCore(w, r, core, "reminders") {
|
|
return
|
|
}
|
|
reminders, err := core.ListReminders(r.Context(), 50)
|
|
if err != nil {
|
|
writeProblem(w, r, http.StatusBadGateway, problemCoreReadFailed,
|
|
"reminders unavailable", fmt.Errorf("list reminders: %w", err))
|
|
return
|
|
}
|
|
renderPage(w, remindersTmpl, map[string]any{"Reminders": reminderRows(reminders)})
|
|
}
|