rdev/internal/domain/operation.go
jordan c280a92012 feat: add operations audit system and template improvements
Operations Audit (new feature):
- Add Operation domain model with status tracking (pending, running, completed, failed, cancelled)
- Add OperationRepository with PostgreSQL implementation
- Add OperationService for CRUD and lifecycle management
- Add operations handlers (list, get, cancel endpoints)
- Add migration 015_operations.sql for operations table
- Add operation cleanup worker for stale operation handling
- Add ErrOperationNotFound to domain errors

Template Improvements:
- Add CLAUDE.md configuration files to astro-landing, default, and go-api templates
- Fix PORT template variable usage in nginx configs for app templates
- Add replace directives for local pkg module in Go templates
- Simplify Go service/worker Dockerfiles for workspace builds
- Fix TypeScript error in logger template

Other:
- Refactor landing-test.sh cookbook script
- Update CLAUDE.md version reference

Note: Some files exceed 500-line limit (pre-existing debt + new feature)
- component.go: 550 lines (unchanged, pre-existing)
- main.go: 522 lines (added operations wiring)
- operation_repo.go: 569 lines (new, needs splitting)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 19:08:57 -07:00

214 lines
5.7 KiB
Go

package domain
import (
"strings"
"time"
)
// OperationType represents the type of operation.
type OperationType string
const (
OperationTypeProjectCreate OperationType = "project.create"
OperationTypeComponentAdd OperationType = "component.add"
OperationTypeBuild OperationType = "build"
OperationTypeResourceProvision OperationType = "resource.provision"
)
// IsValid returns true if the operation type is known.
func (t OperationType) IsValid() bool {
switch t {
case OperationTypeProjectCreate, OperationTypeComponentAdd,
OperationTypeBuild, OperationTypeResourceProvision:
return true
}
return false
}
// OperationStatus represents the status of an operation.
type OperationStatus string
const (
OperationStatusPending OperationStatus = "pending"
OperationStatusRunning OperationStatus = "running"
OperationStatusCompleted OperationStatus = "completed"
OperationStatusFailed OperationStatus = "failed"
)
// IsValid returns true if the status is known.
func (s OperationStatus) IsValid() bool {
switch s {
case OperationStatusPending, OperationStatusRunning,
OperationStatusCompleted, OperationStatusFailed:
return true
}
return false
}
// IsTerminal returns true if the status is a final state.
func (s OperationStatus) IsTerminal() bool {
return s == OperationStatusCompleted || s == OperationStatusFailed
}
// OperationStep represents a single step within an operation.
type OperationStep struct {
// Name is the step identifier (e.g., "git", "build-api", "deploy-web").
Name string `json:"name"`
// Status is the step status.
Status OperationStatus `json:"status"`
// StartedAt is when the step started.
StartedAt time.Time `json:"started_at"`
// DurationMs is the step duration in milliseconds.
DurationMs int64 `json:"duration_ms,omitempty"`
// Output contains step-specific output data.
Output map[string]any `json:"output,omitempty"`
// Error is a one-line error summary.
Error string `json:"error,omitempty"`
// ErrorDetail is the full error detail.
ErrorDetail string `json:"error_detail,omitempty"`
}
// Operation represents a tracked project operation.
type Operation struct {
// ID is the unique operation identifier.
ID string `json:"id"`
// ProjectID is the project this operation belongs to.
ProjectID string `json:"project_id"`
// Type is the operation type.
Type OperationType `json:"type"`
// Status is the current operation status.
Status OperationStatus `json:"status"`
// RequestID is the HTTP request that initiated this operation.
RequestID string `json:"request_id,omitempty"`
// TriggeredBy is the ID of the parent operation that triggered this one.
TriggeredBy string `json:"triggered_by,omitempty"`
// CommitSHA is the git commit this operation created or was triggered by.
CommitSHA string `json:"commit_sha,omitempty"`
// ExternalRef is an external reference (e.g., "build#42").
ExternalRef string `json:"external_ref,omitempty"`
// StartedAt is when the operation started.
StartedAt time.Time `json:"started_at"`
// CompletedAt is when the operation finished.
CompletedAt *time.Time `json:"completed_at,omitempty"`
// DurationMs is the total operation duration in milliseconds.
DurationMs int64 `json:"duration_ms,omitempty"`
// Input contains the operation input parameters.
Input map[string]any `json:"input,omitempty"`
// Output contains the operation output/result.
Output map[string]any `json:"output,omitempty"`
// Error is a one-line error summary.
Error string `json:"error,omitempty"`
// ErrorDetail is the full error detail (truncated to 10KB).
ErrorDetail string `json:"error_detail,omitempty"`
// Steps contains the operation steps.
Steps []OperationStep `json:"steps,omitempty"`
// CreatedAt is when the record was created.
CreatedAt time.Time `json:"created_at"`
}
// StepsSummary returns a human-readable summary of step statuses.
// Example: "git ✓ → build-web ✓ → build-api ✗"
func (o *Operation) StepsSummary() string {
if len(o.Steps) == 0 {
return ""
}
var parts []string
for _, step := range o.Steps {
symbol := "?"
switch step.Status {
case OperationStatusCompleted:
symbol = "✓"
case OperationStatusFailed:
symbol = "✗"
case OperationStatusRunning:
symbol = "…"
case OperationStatusPending:
symbol = "○"
}
parts = append(parts, step.Name+" "+symbol)
}
return strings.Join(parts, " → ")
}
// FailedStep returns the first failed step, or nil if none failed.
func (o *Operation) FailedStep() *OperationStep {
for i := range o.Steps {
if o.Steps[i].Status == OperationStatusFailed {
return &o.Steps[i]
}
}
return nil
}
// OperationFilters specifies criteria for listing operations.
type OperationFilters struct {
// ProjectID filters by project (required for List).
ProjectID string
// Type filters by operation type.
Type OperationType
// Status filters by operation status.
Status OperationStatus
// CommitSHA filters by commit SHA.
CommitSHA string
// Since filters operations started after this time.
Since time.Time
// Limit is the maximum number of operations to return.
Limit int
}
// DefaultOperationFilters returns filters with default values.
func DefaultOperationFilters() OperationFilters {
return OperationFilters{
Limit: 50,
}
}
// Normalize applies defaults and limits to the filters.
func (f *OperationFilters) Normalize() {
if f.Limit <= 0 {
f.Limit = 50
}
if f.Limit > 200 {
f.Limit = 200
}
}
// MaxErrorDetailSize is the maximum size of error_detail (10KB).
const MaxErrorDetailSize = 10 * 1024
// TruncateErrorDetail truncates error detail to the maximum allowed size.
func TruncateErrorDetail(detail string) string {
if len(detail) <= MaxErrorDetailSize {
return detail
}
return detail[:MaxErrorDetailSize-3] + "..."
}