Skip to content

Commit d1a5c46

Browse files
committed
feat: add analytics dashboard with execution trends and job duration analysis
- Add analytics page with overview stats, execution trends chart, and job statistics table - Add backend analytics API endpoints for trends and job stats - Add Chart.js integration for data visualization (line charts) - Add job duration trends analysis for individual jobs - Remove unreliable CPU/memory metrics collection feature - Update navigation to include Analytics link
1 parent b16c314 commit d1a5c46

17 files changed

Lines changed: 1438 additions & 8 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Build artifacts
22
/bin/
33
/dist/
4+
internal/api/static/
45
*.out
56

67
# Database

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010
### Added
11+
- Analytics dashboard with execution trends and job duration analysis
12+
- New `/analytics` page accessible from main navigation
13+
- Overview stats: total runs, success rate, runs in last 24h, average duration
14+
- Execution trends chart with toggle between success rate and run counts view
15+
- Configurable time range (7, 14, 30, 90 days)
16+
- Job statistics table showing per-job performance metrics (success rate, avg/min/max duration)
17+
- Job duration trends chart for analyzing individual job performance over time
18+
- Color-coded success rates (green >90%, yellow >70%, red <70%)
19+
- Backend analytics API endpoints:
20+
- `GET /api/analytics/overview` - overall system statistics
21+
- `GET /api/analytics/execution-trends` - daily success/failure/timeout counts
22+
- `GET /api/analytics/job-stats` - per-job performance statistics
23+
- `GET /api/analytics/jobs/{id}/duration-trends` - duration trends for specific job
24+
- Chart.js integration for data visualization (Line charts with vue-chartjs)
25+
26+
### Removed
27+
- CPU/Memory metrics collection feature (removed due to unreliable child process tracking)
28+
- Removed MetricsGauge and MetricsPanel components
29+
- Removed metrics.go from executor package
30+
- Removed gopsutil dependency
31+
- Note: metrics database schema retained for potential future use
32+
1133
- GitHub Actions workflow for automated releases (`.github/workflows/release.yml`)
1234
- Triggers on semantic version tags (v*.*.*)
1335
- Cross-compiles binaries for Linux, macOS, and Windows (AMD64 + ARM64)

internal/api/handlers.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,3 +596,93 @@ func Health(w http.ResponseWriter, r *http.Request) {
596596
w.WriteHeader(http.StatusOK)
597597
fmt.Fprintf(w, `{"status":"ok"}`)
598598
}
599+
600+
// AnalyticsHandlers handles analytics endpoints
601+
type AnalyticsHandlers struct {
602+
store *store.Store
603+
}
604+
605+
// NewAnalyticsHandlers creates analytics handlers
606+
func NewAnalyticsHandlers(st *store.Store) *AnalyticsHandlers {
607+
return &AnalyticsHandlers{store: st}
608+
}
609+
610+
// GetExecutionTrends handles GET /api/analytics/execution-trends
611+
func (h *AnalyticsHandlers) GetExecutionTrends(w http.ResponseWriter, r *http.Request) {
612+
daysStr := r.URL.Query().Get("days")
613+
days := 30 // default
614+
if d, err := strconv.Atoi(daysStr); err == nil && d > 0 && d <= 365 {
615+
days = d
616+
}
617+
618+
trends, err := h.store.GetExecutionTrends(days)
619+
if err != nil {
620+
WriteError(w, http.StatusInternalServerError, "Failed to get execution trends", "INTERNAL_ERROR")
621+
return
622+
}
623+
624+
WriteJSON(w, http.StatusOK, map[string]interface{}{
625+
"trends": trends,
626+
"days": days,
627+
})
628+
}
629+
630+
// GetJobStats handles GET /api/analytics/job-stats
631+
func (h *AnalyticsHandlers) GetJobStats(w http.ResponseWriter, r *http.Request) {
632+
stats, err := h.store.GetJobStats()
633+
if err != nil {
634+
WriteError(w, http.StatusInternalServerError, "Failed to get job stats", "INTERNAL_ERROR")
635+
return
636+
}
637+
638+
WriteJSON(w, http.StatusOK, map[string]interface{}{
639+
"jobs": stats,
640+
"total": len(stats),
641+
})
642+
}
643+
644+
// GetJobDurationTrends handles GET /api/analytics/jobs/{id}/duration-trends
645+
func (h *AnalyticsHandlers) GetJobDurationTrends(w http.ResponseWriter, r *http.Request) {
646+
jobID := r.PathValue("id")
647+
if jobID == "" {
648+
WriteError(w, http.StatusBadRequest, "Job ID is required", "INVALID_ID")
649+
return
650+
}
651+
652+
daysStr := r.URL.Query().Get("days")
653+
days := 30 // default
654+
if d, err := strconv.Atoi(daysStr); err == nil && d > 0 && d <= 365 {
655+
days = d
656+
}
657+
658+
trends, err := h.store.GetJobDurationTrends(jobID, days)
659+
if err != nil {
660+
WriteError(w, http.StatusInternalServerError, "Failed to get duration trends", "INTERNAL_ERROR")
661+
return
662+
}
663+
664+
// Get job name
665+
job, _ := h.store.GetJob(jobID)
666+
jobName := ""
667+
if job != nil {
668+
jobName = job.Name
669+
}
670+
671+
WriteJSON(w, http.StatusOK, map[string]interface{}{
672+
"job_id": jobID,
673+
"job_name": jobName,
674+
"trends": trends,
675+
"days": days,
676+
})
677+
}
678+
679+
// GetOverallStats handles GET /api/analytics/overview
680+
func (h *AnalyticsHandlers) GetOverallStats(w http.ResponseWriter, r *http.Request) {
681+
stats, err := h.store.GetOverallStats()
682+
if err != nil {
683+
WriteError(w, http.StatusInternalServerError, "Failed to get overall stats", "INTERNAL_ERROR")
684+
return
685+
}
686+
687+
WriteJSON(w, http.StatusOK, stats)
688+
}

internal/api/router.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ func NewRouter(st *store.Store, jwtManager *auth.JWTManager, wsHub *WSHub, corsO
1919
runHandlers := NewRunHandlers(st)
2020
scheduleHandlers := NewScheduleHandlers(st)
2121
dashboardHandlers := NewDashboardHandlers(st)
22+
analyticsHandlers := NewAnalyticsHandlers(st)
2223

2324
// Middleware
2425
authMw := AuthMiddleware(jwtManager, st)
@@ -64,6 +65,12 @@ func NewRouter(st *store.Store, jwtManager *auth.JWTManager, wsHub *WSHub, corsO
6465
// Dashboard endpoints
6566
mux.Handle("GET "+apiBasePath+"/dashboard/stats", authMw(http.HandlerFunc(dashboardHandlers.GetStats)))
6667

68+
// Analytics endpoints
69+
mux.Handle("GET "+apiBasePath+"/analytics/overview", authMw(http.HandlerFunc(analyticsHandlers.GetOverallStats)))
70+
mux.Handle("GET "+apiBasePath+"/analytics/execution-trends", authMw(http.HandlerFunc(analyticsHandlers.GetExecutionTrends)))
71+
mux.Handle("GET "+apiBasePath+"/analytics/job-stats", authMw(http.HandlerFunc(analyticsHandlers.GetJobStats)))
72+
mux.Handle("GET "+apiBasePath+"/analytics/jobs/{id}/duration-trends", authMw(http.HandlerFunc(analyticsHandlers.GetJobDurationTrends)))
73+
6774
// WebSocket endpoints (no auth middleware applied here - handler manages auth internally)
6875
mux.HandleFunc("GET "+apiBasePath+"/ws/logs", wsHub.HandleLogsWebSocket)
6976

internal/executor/executor.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ func (e *Executor) Execute(ctx context.Context, run *store.Run, job *store.Job)
124124

125125
// Wait for command to complete or timeout
126126
err = cmd.Wait()
127+
127128
// Ensure all logs are fully written before proceeding
128129
wg.Wait()
129130

0 commit comments

Comments
 (0)