P2R-02: UI rewire against the slim-schedule + source-group model #2
@@ -167,7 +167,12 @@ func (s *Server) dispatchScheduledJob(ctx context.Context, hostID string, conn *
|
|||||||
// dispatchBackupForGroup builds and sends a single backup command.run
|
// dispatchBackupForGroup builds and sends a single backup command.run
|
||||||
// envelope on conn for the given group. Persists the job row first so
|
// envelope on conn for the given group. Persists the job row first so
|
||||||
// the live log viewer can subscribe to it.
|
// the live log viewer can subscribe to it.
|
||||||
func (s *Server) dispatchBackupForGroup(ctx context.Context, conn *ws.Conn, hostID, scheduleID string, g *store.SourceGroup, scheduledAt time.Time) {
|
// dispatchBackupForGroup persists a backup job row, sends the
|
||||||
|
// command.run envelope to the agent, and audit-logs the dispatch.
|
||||||
|
// Returns the persisted job ID on success, or "" on any failure
|
||||||
|
// (failures are slog.Warn-ed). Callers may use the returned ID to,
|
||||||
|
// e.g., redirect the UI to the live job log.
|
||||||
|
func (s *Server) dispatchBackupForGroup(ctx context.Context, conn *ws.Conn, hostID, scheduleID string, g *store.SourceGroup, scheduledAt time.Time) string {
|
||||||
jobID := ulid.Make().String()
|
jobID := ulid.Make().String()
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
scheduleRef := scheduleID
|
scheduleRef := scheduleID
|
||||||
@@ -181,7 +186,7 @@ func (s *Server) dispatchBackupForGroup(ctx context.Context, conn *ws.Conn, host
|
|||||||
}); err != nil {
|
}); err != nil {
|
||||||
slog.Warn("schedule.fire: persist job", "host_id", hostID,
|
slog.Warn("schedule.fire: persist job", "host_id", hostID,
|
||||||
"schedule_id", scheduleID, "group", g.Name, "err", err)
|
"schedule_id", scheduleID, "group", g.Name, "err", err)
|
||||||
return
|
return ""
|
||||||
}
|
}
|
||||||
// Backup ignores RetentionPolicy — the forget cadence lives on
|
// Backup ignores RetentionPolicy — the forget cadence lives on
|
||||||
// host_repo_maintenance and is driven by the server-side ticker
|
// host_repo_maintenance and is driven by the server-side ticker
|
||||||
@@ -196,14 +201,14 @@ func (s *Server) dispatchBackupForGroup(ctx context.Context, conn *ws.Conn, host
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("schedule.fire: marshal command.run",
|
slog.Warn("schedule.fire: marshal command.run",
|
||||||
"host_id", hostID, "schedule_id", scheduleID, "err", err)
|
"host_id", hostID, "schedule_id", scheduleID, "err", err)
|
||||||
return
|
return ""
|
||||||
}
|
}
|
||||||
sendCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
sendCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := conn.Send(sendCtx, env); err != nil {
|
if err := conn.Send(sendCtx, env); err != nil {
|
||||||
slog.Warn("schedule.fire: send command.run",
|
slog.Warn("schedule.fire: send command.run",
|
||||||
"host_id", hostID, "schedule_id", scheduleID, "err", err)
|
"host_id", hostID, "schedule_id", scheduleID, "err", err)
|
||||||
return
|
return ""
|
||||||
}
|
}
|
||||||
_ = s.deps.Store.AppendAudit(ctx, store.AuditEntry{
|
_ = s.deps.Store.AppendAudit(ctx, store.AuditEntry{
|
||||||
ID: ulid.Make().String(),
|
ID: ulid.Make().String(),
|
||||||
@@ -216,4 +221,5 @@ func (s *Server) dispatchBackupForGroup(ctx context.Context, conn *ws.Conn, host
|
|||||||
slog.Info("schedule.fire: dispatched backup",
|
slog.Info("schedule.fire: dispatched backup",
|
||||||
"host_id", hostID, "schedule_id", scheduleID,
|
"host_id", hostID, "schedule_id", scheduleID,
|
||||||
"group", g.Name, "job_id", jobID, "scheduled_at", scheduledAt)
|
"group", g.Name, "job_id", jobID, "scheduled_at", scheduledAt)
|
||||||
|
return jobID
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ package http
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
stdhttp "net/http"
|
stdhttp "net/http"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -324,6 +326,8 @@ func (s *Server) handleUIScheduleRun(w stdhttp.ResponseWriter, r *stdhttp.Reques
|
|||||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
|
type fired struct{ groupName, jobID string }
|
||||||
|
dispatched := make([]fired, 0, len(sc.SourceGroupIDs))
|
||||||
for _, gid := range sc.SourceGroupIDs {
|
for _, gid := range sc.SourceGroupIDs {
|
||||||
g, gerr := s.deps.Store.GetSourceGroup(ctx, host.ID, gid)
|
g, gerr := s.deps.Store.GetSourceGroup(ctx, host.ID, gid)
|
||||||
if gerr != nil {
|
if gerr != nil {
|
||||||
@@ -331,14 +335,35 @@ func (s *Server) handleUIScheduleRun(w stdhttp.ResponseWriter, r *stdhttp.Reques
|
|||||||
"host_id", host.ID, "schedule_id", sid, "group_id", gid, "err", gerr)
|
"host_id", host.ID, "schedule_id", sid, "group_id", gid, "err", gerr)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
s.dispatchBackupForGroup(ctx, conn, host.ID, sid, g, now)
|
jobID := s.dispatchBackupForGroup(ctx, conn, host.ID, sid, g, now)
|
||||||
|
if jobID != "" {
|
||||||
|
dispatched = append(dispatched, fired{groupName: g.Name, jobID: jobID})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if wantsHTML(r) {
|
if wantsHTML(r) {
|
||||||
// HX-Redirect would jump to a single job log, but a multi-group
|
switch len(dispatched) {
|
||||||
// fire produces N jobs. Bounce back to the list — the operator
|
case 0:
|
||||||
// can drill into individual jobs from there.
|
stdhttp.Error(w, "no backup jobs dispatched — see server log", stdhttp.StatusInternalServerError)
|
||||||
w.Header().Set("HX-Redirect", "/hosts/"+host.ID+"/schedules")
|
return
|
||||||
|
case 1:
|
||||||
|
// Single-group schedule: jump straight to the live job log,
|
||||||
|
// same UX as per-source-group Run-now from the Sources tab.
|
||||||
|
w.Header().Set("HX-Redirect", "/jobs/"+dispatched[0].jobID)
|
||||||
|
default:
|
||||||
|
// Multi-group: stay on the schedules tab and toast the
|
||||||
|
// summary. Direct the operator to one of the job logs via
|
||||||
|
// the toast (the most recent job ID is fine).
|
||||||
|
names := make([]string, 0, len(dispatched))
|
||||||
|
for _, f := range dispatched {
|
||||||
|
names = append(names, f.groupName)
|
||||||
|
}
|
||||||
|
msg := strconv.Itoa(len(dispatched)) + " backups dispatched: " + strings.Join(names, ", ")
|
||||||
|
payload, _ := json.Marshal(map[string]any{
|
||||||
|
"rm:toast": map[string]string{"level": "success", "message": msg},
|
||||||
|
})
|
||||||
|
w.Header().Set("HX-Trigger", string(payload))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
w.WriteHeader(stdhttp.StatusNoContent)
|
w.WriteHeader(stdhttp.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user