diff --git a/internal/server/http/server.go b/internal/server/http/server.go index 17ecc7a..9679a5c 100644 --- a/internal/server/http/server.go +++ b/internal/server/http/server.go @@ -196,6 +196,7 @@ func (s *Server) routes(r chi.Router) { r.Get("/hosts/{id}/sources/new", s.handleUISourceGroupNewGet) r.Get("/hosts/{id}/sources/{gid}/edit", s.handleUISourceGroupEditGet) r.Get("/hosts/{id}/repo", s.handleUIHostRepo) + r.Get("/hosts/{id}/repo/trend", s.handleUIRepoTrend) r.Get("/hosts/{id}/schedules", s.handleUISchedulesList) r.Get("/hosts/{id}/schedules/new", s.handleUIScheduleNewGet) r.Get("/hosts/{id}/schedules/{sid}/edit", s.handleUIScheduleEditGet) diff --git a/internal/server/http/ui_repo.go b/internal/server/http/ui_repo.go index 461a1ab..4807a63 100644 --- a/internal/server/http/ui_repo.go +++ b/internal/server/http/ui_repo.go @@ -1,9 +1,12 @@ package http import ( + "context" "encoding/json" "errors" + "html/template" "log/slog" + "math" stdhttp "net/http" "strconv" "strings" @@ -13,6 +16,7 @@ import ( "gitea.dcglab.co.uk/steve/restic-manager/internal/server/ui" "gitea.dcglab.co.uk/steve/restic-manager/internal/store" + "gitea.dcglab.co.uk/steve/restic-manager/internal/web/sparkline" ) // ui_repo.go — HTML form-driven repo-tab handlers (connection, @@ -27,6 +31,15 @@ import ( // POST /hosts/{id}/admin-credentials — admin (prune) creds // POST /hosts/{id}/admin-credentials/delete — clear admin creds +// repoTrendView is the data the repo_size_chart partial needs. +// HostID + Range round-trip through the htmx range pills; ChartSVG +// is pre-rendered server-side so the partial is just a wrapper. +type repoTrendView struct { + HostID string + Range string + ChartSVG template.HTML +} + // repoStatsView is a flat, pre-dereferenced projection of // store.HostRepoStats for use in templates. Nil pointer fields are // collapsed to zero/false and accompanied by a Has* sentinel so the @@ -74,6 +87,10 @@ type hostRepoPage struct { // Nil when no row exists yet (fresh hosts). StatsView *repoStatsView + // Trend holds the pre-rendered chart fragment data for the + // 30/90/365-day repo-size + snapshot-count overlay chart. + Trend repoTrendView + // Snapshots-by-tag — map[group_name]count, plus an "untagged" row. SnapshotsByTag map[string]int UntaggedSnapshots int @@ -225,9 +242,52 @@ func (s *Server) loadHostRepoPage(r *stdhttp.Request, host store.Host) (*hostRep } } } + p.Trend = s.buildRepoTrendView(r.Context(), host.ID, "30d") + return p, nil } +// buildRepoTrendView builds the chart-partial data for a host. Used +// both by the page-load (initial 30d render) and the htmx fragment +// endpoint (range switching). An invalid rangeKey falls back to "30d". +func (s *Server) buildRepoTrendView(ctx context.Context, hostID, rangeKey string) repoTrendView { + days := 30 + switch rangeKey { + case "90d": + days = 90 + case "1y": + days = 365 + default: + rangeKey = "30d" + } + since := time.Now().UTC().AddDate(0, 0, -days) + pts, err := s.deps.Store.ListHostRepoStatsHistory(ctx, hostID, since) + if err != nil { + slog.Warn("ui repo trend: list history", "host_id", hostID, "err", err) + } + sizes := make([]float64, len(pts)) + counts := make([]float64, len(pts)) + dayList := make([]time.Time, len(pts)) + for i, p := range pts { + dayList[i] = p.Day + if p.TotalSizeBytes == nil { + sizes[i] = math.NaN() + } else { + sizes[i] = float64(*p.TotalSizeBytes) + } + if p.SnapshotCount == nil { + counts[i] = math.NaN() + } else { + counts[i] = float64(*p.SnapshotCount) + } + } + chartSVG := sparkline.RenderChart([]sparkline.Series{ + {Name: "size", Stroke: "#3b82f6", Axis: sparkline.AxisLeft, Format: sparkline.FormatBytes, Points: sizes}, + {Name: "snapshots", Stroke: "#f59e0b", Axis: sparkline.AxisRight, Format: sparkline.FormatCount, Points: counts}, + }, dayList, sparkline.ChartOpts{Width: 600, Height: 220}) + return repoTrendView{HostID: hostID, Range: rangeKey, ChartSVG: chartSVG} +} + func (s *Server) handleUIHostRepo(w stdhttp.ResponseWriter, r *stdhttp.Request) { u := s.requireUIUser(w, r) if u == nil { diff --git a/internal/server/http/ui_repo_trend.go b/internal/server/http/ui_repo_trend.go new file mode 100644 index 0000000..4c7786a --- /dev/null +++ b/internal/server/http/ui_repo_trend.go @@ -0,0 +1,25 @@ +// ui_repo_trend.go — htmx fragment endpoint for the repo-page +// trend chart. Returns just the chart partial wrapped in +//