mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 23:21:30 +00:00
ee75f149ae
Backend: StatsService with 5 aggregation methods, JSON metrics endpoint, slog-based structured logging middleware. Stats API: dashboard summary, certificates-by-status, expiration timeline, job trends, issuance rate. 23 new backend tests. Frontend: Recharts-powered dashboard with 4 charts (status pie, expiration heatmap, job trends line, issuance bar), agent fleet overview page with OS/arch grouping and version breakdown, deployment rollback buttons on version history. 7 new frontend tests. 78 API endpoints, 744+ total tests (658 Go + 86 Vitest). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
148 lines
4.3 KiB
Go
148 lines
4.3 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/shankar0123/certctl/internal/api/middleware"
|
|
)
|
|
|
|
// StatsService defines the service interface for statistics operations.
|
|
type StatsService interface {
|
|
GetDashboardSummary(ctx context.Context) (interface{}, error)
|
|
GetCertificatesByStatus(ctx context.Context) (interface{}, error)
|
|
GetExpirationTimeline(ctx context.Context, days int) (interface{}, error)
|
|
GetJobStats(ctx context.Context, days int) (interface{}, error)
|
|
GetIssuanceRate(ctx context.Context, days int) (interface{}, error)
|
|
}
|
|
|
|
// StatsHandler handles HTTP requests for statistics and observability endpoints.
|
|
type StatsHandler struct {
|
|
svc StatsService
|
|
}
|
|
|
|
// NewStatsHandler creates a new StatsHandler with a service dependency.
|
|
func NewStatsHandler(svc StatsService) StatsHandler {
|
|
return StatsHandler{svc: svc}
|
|
}
|
|
|
|
// GetDashboardSummary returns a high-level summary of system state.
|
|
// GET /api/v1/stats/summary
|
|
func (h StatsHandler) GetDashboardSummary(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
Error(w, http.StatusMethodNotAllowed, "Method not allowed")
|
|
return
|
|
}
|
|
|
|
requestID := middleware.GetRequestID(r.Context())
|
|
|
|
summary, err := h.svc.GetDashboardSummary(r.Context())
|
|
if err != nil {
|
|
ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to get dashboard summary", requestID)
|
|
return
|
|
}
|
|
|
|
JSON(w, http.StatusOK, summary)
|
|
}
|
|
|
|
// GetCertificatesByStatus returns certificate counts grouped by status.
|
|
// GET /api/v1/stats/certificates-by-status
|
|
func (h StatsHandler) GetCertificatesByStatus(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
Error(w, http.StatusMethodNotAllowed, "Method not allowed")
|
|
return
|
|
}
|
|
|
|
requestID := middleware.GetRequestID(r.Context())
|
|
|
|
counts, err := h.svc.GetCertificatesByStatus(r.Context())
|
|
if err != nil {
|
|
ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to get certificate status counts", requestID)
|
|
return
|
|
}
|
|
|
|
JSON(w, http.StatusOK, counts)
|
|
}
|
|
|
|
// GetExpirationTimeline returns certificates expiring over the next N days.
|
|
// GET /api/v1/stats/expiration-timeline?days=30
|
|
func (h StatsHandler) GetExpirationTimeline(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
Error(w, http.StatusMethodNotAllowed, "Method not allowed")
|
|
return
|
|
}
|
|
|
|
requestID := middleware.GetRequestID(r.Context())
|
|
|
|
// Parse query parameter
|
|
days := 30
|
|
if d := r.URL.Query().Get("days"); d != "" {
|
|
if parsed, err := strconv.Atoi(d); err == nil && parsed > 0 && parsed <= 365 {
|
|
days = parsed
|
|
}
|
|
}
|
|
|
|
timeline, err := h.svc.GetExpirationTimeline(r.Context(), days)
|
|
if err != nil {
|
|
ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to get expiration timeline", requestID)
|
|
return
|
|
}
|
|
|
|
JSON(w, http.StatusOK, timeline)
|
|
}
|
|
|
|
// GetJobTrends returns job success/failure trends over the past N days.
|
|
// GET /api/v1/stats/job-trends?days=30
|
|
func (h StatsHandler) GetJobTrends(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
Error(w, http.StatusMethodNotAllowed, "Method not allowed")
|
|
return
|
|
}
|
|
|
|
requestID := middleware.GetRequestID(r.Context())
|
|
|
|
// Parse query parameter
|
|
days := 30
|
|
if d := r.URL.Query().Get("days"); d != "" {
|
|
if parsed, err := strconv.Atoi(d); err == nil && parsed > 0 && parsed <= 365 {
|
|
days = parsed
|
|
}
|
|
}
|
|
|
|
trends, err := h.svc.GetJobStats(r.Context(), days)
|
|
if err != nil {
|
|
ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to get job trends", requestID)
|
|
return
|
|
}
|
|
|
|
JSON(w, http.StatusOK, trends)
|
|
}
|
|
|
|
// GetIssuanceRate returns the rate of new certificate issuance over the past N days.
|
|
// GET /api/v1/stats/issuance-rate?days=30
|
|
func (h StatsHandler) GetIssuanceRate(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
Error(w, http.StatusMethodNotAllowed, "Method not allowed")
|
|
return
|
|
}
|
|
|
|
requestID := middleware.GetRequestID(r.Context())
|
|
|
|
// Parse query parameter
|
|
days := 30
|
|
if d := r.URL.Query().Get("days"); d != "" {
|
|
if parsed, err := strconv.Atoi(d); err == nil && parsed > 0 && parsed <= 365 {
|
|
days = parsed
|
|
}
|
|
}
|
|
|
|
issuanceRate, err := h.svc.GetIssuanceRate(r.Context(), days)
|
|
if err != nil {
|
|
ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to get issuance rate", requestID)
|
|
return
|
|
}
|
|
|
|
JSON(w, http.StatusOK, issuanceRate)
|
|
}
|