Critical fix: WorkersHandler was missing workService dependency, causing 500 errors when workers tried to fail tasks. This caused tasks to get stuck in "running" state permanently. Also adds: - /work/tasks endpoint for debugging all tasks across projects - List method to WorkQueue interface for admin views - HTTP client tests for api_client.go and claudebox/client.go (48 tests) - Split work.go DTOs into work_dto.go to stay under 500 lines Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
86 lines
2.4 KiB
Go
86 lines
2.4 KiB
Go
// Package handlers provides HTTP handlers for the rdev API.
|
|
package handlers
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/orchard9/rdev/internal/domain"
|
|
)
|
|
|
|
// WorkTaskDTO is the data transfer object for work tasks.
|
|
type WorkTaskDTO struct {
|
|
ID string `json:"id"`
|
|
ProjectID string `json:"project_id"`
|
|
Type string `json:"type"`
|
|
Spec map[string]any `json:"spec"`
|
|
Status string `json:"status"`
|
|
Priority int `json:"priority"`
|
|
WorkerID string `json:"worker_id,omitempty"`
|
|
CallbackURL string `json:"callback_url,omitempty"`
|
|
CreatedAt string `json:"created_at"`
|
|
StartedAt string `json:"started_at,omitempty"`
|
|
CompletedAt string `json:"completed_at,omitempty"`
|
|
Result *WorkResultDTO `json:"result,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
RetryCount int `json:"retry_count"`
|
|
MaxRetries int `json:"max_retries"`
|
|
}
|
|
|
|
// WorkResultDTO is the data transfer object for work results.
|
|
type WorkResultDTO struct {
|
|
Output string `json:"output,omitempty"`
|
|
Artifacts map[string]string `json:"artifacts,omitempty"`
|
|
}
|
|
|
|
// toWorkTaskDTO converts a domain.WorkTask to a WorkTaskDTO.
|
|
func toWorkTaskDTO(t *domain.WorkTask) *WorkTaskDTO {
|
|
if t == nil {
|
|
return nil
|
|
}
|
|
dto := &WorkTaskDTO{
|
|
ID: t.ID,
|
|
ProjectID: t.ProjectID,
|
|
Type: string(t.Type),
|
|
Spec: t.Spec,
|
|
Status: string(t.Status),
|
|
Priority: t.Priority,
|
|
WorkerID: t.WorkerID,
|
|
CallbackURL: t.CallbackURL,
|
|
CreatedAt: t.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
|
Error: t.Error,
|
|
RetryCount: t.RetryCount,
|
|
MaxRetries: t.MaxRetries,
|
|
}
|
|
if t.StartedAt != nil {
|
|
dto.StartedAt = t.StartedAt.Format("2006-01-02T15:04:05Z07:00")
|
|
}
|
|
if t.CompletedAt != nil {
|
|
dto.CompletedAt = t.CompletedAt.Format("2006-01-02T15:04:05Z07:00")
|
|
}
|
|
if t.Result != nil {
|
|
dto.Result = &WorkResultDTO{
|
|
Output: t.Result.Output,
|
|
Artifacts: t.Result.Artifacts,
|
|
}
|
|
}
|
|
return dto
|
|
}
|
|
|
|
// formatDuration formats a duration in a human-readable way.
|
|
func formatDuration(d interface{ Seconds() float64 }) string {
|
|
secs := d.Seconds()
|
|
if secs < 60 {
|
|
return fmt.Sprintf("%.0fs", secs)
|
|
}
|
|
mins := secs / 60
|
|
if mins < 60 {
|
|
return fmt.Sprintf("%.1fm", mins)
|
|
}
|
|
hours := mins / 60
|
|
if hours < 24 {
|
|
return fmt.Sprintf("%.1fh", hours)
|
|
}
|
|
days := hours / 24
|
|
return fmt.Sprintf("%.1fd", days)
|
|
}
|