package handler import ( "context" "database/sql" "encoding/json" "fmt" "net/http" "strings" "time" "vctp/db" "vctp/server/models" ) // DailyCreationDiagnostics returns missing CreationTime diagnostics for a daily summary table. // @Summary Daily summary CreationTime diagnostics // @Description Returns counts of daily summary rows missing CreationTime and sample rows for the given date. // @Tags diagnostics // @Produce json // @Param date query string true "Daily date (YYYY-MM-DD)" // @Success 200 {object} models.DailyCreationDiagnosticsResponse "Diagnostics result" // @Failure 400 {object} models.ErrorResponse "Invalid request" // @Failure 404 {object} models.ErrorResponse "Summary not found" // @Failure 500 {object} models.ErrorResponse "Server error" // @Router /api/diagnostics/daily-creation [get] func (h *Handler) DailyCreationDiagnostics(w http.ResponseWriter, r *http.Request) { dateValue := strings.TrimSpace(r.URL.Query().Get("date")) if dateValue == "" { writeJSONError(w, http.StatusBadRequest, "date is required") return } loc := time.Now().Location() parsed, err := time.ParseInLocation("2006-01-02", dateValue, loc) if err != nil { writeJSONError(w, http.StatusBadRequest, "date must be YYYY-MM-DD") return } tableName := fmt.Sprintf("inventory_daily_summary_%s", parsed.Format("20060102")) if _, err := db.SafeTableName(tableName); err != nil { writeJSONError(w, http.StatusBadRequest, "invalid summary table name") return } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() dbConn := h.Database.DB() if !db.TableExists(ctx, dbConn, tableName) { writeJSONError(w, http.StatusNotFound, "daily summary table not found") return } var totalRows int64 countQuery := fmt.Sprintf(`SELECT COUNT(*) FROM %s`, tableName) if err := dbConn.GetContext(ctx, &totalRows, countQuery); err != nil { h.Logger.Warn("daily creation diagnostics count failed", "table", tableName, "error", err) writeJSONError(w, http.StatusInternalServerError, "failed to read summary rows") return } var missingTotal int64 missingQuery := fmt.Sprintf(`SELECT COUNT(*) FROM %s WHERE "CreationTime" IS NULL OR "CreationTime" = 0`, tableName) if err := dbConn.GetContext(ctx, &missingTotal, missingQuery); err != nil { h.Logger.Warn("daily creation diagnostics missing count failed", "table", tableName, "error", err) writeJSONError(w, http.StatusInternalServerError, "failed to read missing creation rows") return } var avgIsPresentLtOne int64 avgPresenceQuery := fmt.Sprintf(`SELECT COUNT(*) FROM %s WHERE "AvgIsPresent" IS NOT NULL AND "AvgIsPresent" < 0.999999`, tableName) if err := dbConn.GetContext(ctx, &avgIsPresentLtOne, avgPresenceQuery); err != nil { h.Logger.Warn("daily creation diagnostics avg-is-present count failed", "table", tableName, "error", err) writeJSONError(w, http.StatusInternalServerError, "failed to read avg is present rows") return } var missingPartialCount int64 missingPartialQuery := fmt.Sprintf(`SELECT COUNT(*) FROM %s WHERE ("CreationTime" IS NULL OR "CreationTime" = 0) AND "AvgIsPresent" IS NOT NULL AND "AvgIsPresent" < 0.999999`, tableName) if err := dbConn.GetContext(ctx, &missingPartialCount, missingPartialQuery); err != nil { h.Logger.Warn("daily creation diagnostics missing partial count failed", "table", tableName, "error", err) writeJSONError(w, http.StatusInternalServerError, "failed to read missing partial rows") return } missingPct := 0.0 if totalRows > 0 { missingPct = float64(missingTotal) * 100 / float64(totalRows) } byVcenter := make([]models.DailyCreationMissingByVcenter, 0) byVcenterQuery := fmt.Sprintf(` SELECT "Vcenter", COUNT(*) AS missing_count FROM %s WHERE "CreationTime" IS NULL OR "CreationTime" = 0 GROUP BY "Vcenter" ORDER BY missing_count DESC `, tableName) if rows, err := dbConn.QueryxContext(ctx, byVcenterQuery); err != nil { h.Logger.Warn("daily creation diagnostics by-vcenter failed", "table", tableName, "error", err) } else { for rows.Next() { var vcenter string var count int64 if err := rows.Scan(&vcenter, &count); err != nil { continue } byVcenter = append(byVcenter, models.DailyCreationMissingByVcenter{ Vcenter: vcenter, MissingCount: count, }) } rows.Close() } const sampleLimit = 10 samples := make([]models.DailyCreationMissingSample, 0, sampleLimit) sampleQuery := fmt.Sprintf(` SELECT "Vcenter","VmId","VmUuid","Name","SamplesPresent","AvgIsPresent","SnapshotTime" FROM %s WHERE "CreationTime" IS NULL OR "CreationTime" = 0 ORDER BY "SamplesPresent" DESC LIMIT %d `, tableName, sampleLimit) if rows, err := dbConn.QueryxContext(ctx, sampleQuery); err != nil { h.Logger.Warn("daily creation diagnostics sample failed", "table", tableName, "error", err) } else { for rows.Next() { var ( vcenter string vmId, vmUuid, name sql.NullString samplesPresent, snapshotTime sql.NullInt64 avgIsPresent sql.NullFloat64 ) if err := rows.Scan(&vcenter, &vmId, &vmUuid, &name, &samplesPresent, &avgIsPresent, &snapshotTime); err != nil { continue } samples = append(samples, models.DailyCreationMissingSample{ Vcenter: vcenter, VmId: vmId.String, VmUuid: vmUuid.String, Name: name.String, SamplesPresent: samplesPresent.Int64, AvgIsPresent: avgIsPresent.Float64, SnapshotTime: snapshotTime.Int64, }) } rows.Close() } partialSamples := make([]models.DailyCreationMissingSample, 0, sampleLimit) partialSampleQuery := fmt.Sprintf(` SELECT "Vcenter","VmId","VmUuid","Name","SamplesPresent","AvgIsPresent","SnapshotTime" FROM %s WHERE ("CreationTime" IS NULL OR "CreationTime" = 0) AND "AvgIsPresent" IS NOT NULL AND "AvgIsPresent" < 0.999999 ORDER BY "SamplesPresent" DESC LIMIT %d `, tableName, sampleLimit) if rows, err := dbConn.QueryxContext(ctx, partialSampleQuery); err != nil { h.Logger.Warn("daily creation diagnostics partial sample failed", "table", tableName, "error", err) } else { for rows.Next() { var ( vcenter string vmId, vmUuid, name sql.NullString samplesPresent, snapshotTime sql.NullInt64 avgIsPresent sql.NullFloat64 ) if err := rows.Scan(&vcenter, &vmId, &vmUuid, &name, &samplesPresent, &avgIsPresent, &snapshotTime); err != nil { continue } partialSamples = append(partialSamples, models.DailyCreationMissingSample{ Vcenter: vcenter, VmId: vmId.String, VmUuid: vmUuid.String, Name: name.String, SamplesPresent: samplesPresent.Int64, AvgIsPresent: avgIsPresent.Float64, SnapshotTime: snapshotTime.Int64, }) } rows.Close() } response := models.DailyCreationDiagnosticsResponse{ Status: "OK", Date: parsed.Format("2006-01-02"), Table: tableName, TotalRows: totalRows, MissingCreationCount: missingTotal, MissingCreationPct: missingPct, AvgIsPresentLtOneCount: avgIsPresentLtOne, MissingCreationPartialCount: missingPartialCount, MissingByVcenter: byVcenter, Samples: samples, MissingCreationPartialSamples: partialSamples, } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(response) }