diff --git a/server/handler/snapshotRegenerateHourly.go b/server/handler/snapshotRegenerateHourly.go new file mode 100644 index 0000000..8e62399 --- /dev/null +++ b/server/handler/snapshotRegenerateHourly.go @@ -0,0 +1,68 @@ +package handler + +import ( + "encoding/json" + "net/http" + "os" + "path/filepath" + "strings" + "time" + "vctp/internal/report" +) + +// SnapshotRegenerateHourlyReports regenerates missing hourly snapshot XLSX reports on disk. +// @Summary Regenerate hourly snapshot reports +// @Description Regenerates XLSX reports for hourly snapshots when the report files are missing or empty. +// @Tags snapshots +// @Produce json +// @Success 200 {object} map[string]interface{} "Regeneration summary" +// @Failure 500 {object} map[string]string "Server error" +// @Router /api/snapshots/regenerate-hourly-reports [post] +func (h *Handler) SnapshotRegenerateHourlyReports(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + reportsDir := strings.TrimSpace(h.Settings.Values.Settings.ReportsDir) + if reportsDir == "" { + reportsDir = "/var/lib/vctp/reports" + } + if err := os.MkdirAll(reportsDir, 0o755); err != nil { + h.Logger.Error("failed to create reports directory", "error", err, "path", reportsDir) + writeJSONError(w, http.StatusInternalServerError, "failed to create reports directory") + return + } + + start := time.Unix(0, 0) + end := time.Now().AddDate(10, 0, 0) // sufficiently in the future to include all records + records, err := report.SnapshotRecordsWithFallback(ctx, h.Database, "hourly", "inventory_hourly_", "epoch", start, end) + if err != nil { + h.Logger.Error("failed to list hourly snapshots", "error", err) + writeJSONError(w, http.StatusInternalServerError, "failed to list hourly snapshots") + return + } + + var regenerated, skipped, errors int + for _, rec := range records { + dest := filepath.Join(reportsDir, rec.TableName+".xlsx") + if info, err := os.Stat(dest); err == nil && info.Size() > 0 { + skipped++ + continue + } + if _, err := report.SaveTableReport(h.Logger, h.Database, ctx, rec.TableName, reportsDir); err != nil { + errors++ + h.Logger.Warn("failed to regenerate hourly report", "table", rec.TableName, "error", err) + continue + } + regenerated++ + } + + resp := map[string]interface{}{ + "status": "OK", + "total": len(records), + "regenerated": regenerated, + "skipped": skipped, + "errors": errors, + "reports_dir": reportsDir, + "snapshotType": "hourly", + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) +} diff --git a/server/router/router.go b/server/router/router.go index 94e0bf7..c38ef75 100644 --- a/server/router/router.go +++ b/server/router/router.go @@ -64,6 +64,7 @@ func New(logger *slog.Logger, database db.Database, buildTime string, sha1ver st mux.HandleFunc("/api/report/snapshot", h.SnapshotReportDownload) mux.HandleFunc("/api/snapshots/aggregate", h.SnapshotAggregateForce) mux.HandleFunc("/api/snapshots/migrate", h.SnapshotMigrate) + mux.HandleFunc("/api/snapshots/regenerate-hourly-reports", h.SnapshotRegenerateHourlyReports) mux.HandleFunc("/snapshots/hourly", h.SnapshotHourlyList) mux.HandleFunc("/snapshots/daily", h.SnapshotDailyList)