package lanes import ( "context" "inbox/internal/base/slug" "inbox/internal/base/timeutil" "inbox/internal/domain/lane" ) type Repository interface { CreateLane(ctx context.Context, value lane.Record) (lane.Record, error) GetLane(ctx context.Context, laneID string) (lane.Record, error) ListLanesByTopic(ctx context.Context, topicID string) ([]lane.Record, error) ListLanesByWorkspace(ctx context.Context, workspaceID string) ([]lane.Record, error) UpdateLane(ctx context.Context, value lane.Record) (lane.Record, error) } type RuntimeManager interface { EnsureLane(ctx context.Context, laneID string) (lane.Record, error) StopLane(ctx context.Context, laneID string) (lane.Record, error) } type Service struct { repo Repository runtime RuntimeManager clock timeutil.Clock } type Patch struct { Name *string Status *lane.Status ResultSummaryMarkdown *string ErrorMessage *string StartedAt *string CompletedAt *string } func NewService(repo Repository, runtime RuntimeManager, clock timeutil.Clock) *Service { if clock == nil { clock = timeutil.SystemClock{} } return &Service{repo: repo, runtime: runtime, clock: clock} } func (s *Service) Create(ctx context.Context, value lane.Record) (lane.Record, error) { if value.Slug == "" { value.Slug = slug.Normalize(value.Name) } if value.Status == "" { value.Status = lane.StatusDraft } return s.repo.CreateLane(ctx, value) } func (s *Service) Get(ctx context.Context, laneID string) (lane.Record, error) { return s.repo.GetLane(ctx, laneID) } func (s *Service) ListByTopic(ctx context.Context, topicID string) ([]lane.Record, error) { return s.repo.ListLanesByTopic(ctx, topicID) } func (s *Service) ListByWorkspace(ctx context.Context, workspaceID string) ([]lane.Record, error) { return s.repo.ListLanesByWorkspace(ctx, workspaceID) } func (s *Service) Patch(ctx context.Context, laneID string, patch Patch) (lane.Record, error) { current, err := s.repo.GetLane(ctx, laneID) if err != nil { return lane.Record{}, err } if patch.Name != nil { current.Name = *patch.Name current.Slug = slug.Normalize(current.Name) } if patch.Status != nil { current.Status = *patch.Status if current.Status == lane.StatusRunning && current.StartedAt == "" { current.StartedAt = timeutil.FormatRFC3339(s.clock.Now()) } if (current.Status == lane.StatusSucceeded || current.Status == lane.StatusFailed || current.Status == lane.StatusCancelled) && current.CompletedAt == "" { current.CompletedAt = timeutil.FormatRFC3339(s.clock.Now()) } } if patch.ResultSummaryMarkdown != nil { current.ResultSummaryMarkdown = *patch.ResultSummaryMarkdown } if patch.ErrorMessage != nil { current.ErrorMessage = *patch.ErrorMessage } if patch.StartedAt != nil { current.StartedAt = *patch.StartedAt } if patch.CompletedAt != nil { current.CompletedAt = *patch.CompletedAt } return s.repo.UpdateLane(ctx, current) } func (s *Service) Start(ctx context.Context, laneID string) (lane.Record, error) { if s.runtime == nil { return lane.Record{}, nil } return s.runtime.EnsureLane(ctx, laneID) } func (s *Service) Stop(ctx context.Context, laneID string) (lane.Record, error) { if s.runtime == nil { return lane.Record{}, nil } return s.runtime.StopLane(ctx, laneID) }