1317ff6369
Operations are now clickable from the host detail page, linking to
/ops/{id} which shows the operation info, host link, duration, and
activity log filtered to that operation. Active operations can be
cancelled, which transitions the host to failed and releases the lock.
SSE activity events now include operation_id for real-time filtering.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
110 lines
3.5 KiB
Go
110 lines
3.5 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"time"
|
|
|
|
"provisioning/internal/model"
|
|
)
|
|
|
|
type Operations struct {
|
|
DB *sql.DB
|
|
}
|
|
|
|
func (s *Operations) Create(ctx context.Context, op model.Operation) (int64, error) {
|
|
res, err := s.DB.ExecContext(ctx, `
|
|
INSERT INTO operations(host_id, kind, state, image_id)
|
|
VALUES(?,?,?,?)
|
|
`, op.HostID, op.Kind, model.OpActive, nullInt64(op.ImageID))
|
|
if err != nil {
|
|
return 0, fmt.Errorf("insert operation: %w", err)
|
|
}
|
|
return res.LastInsertId()
|
|
}
|
|
|
|
func (s *Operations) Complete(ctx context.Context, id int64) error {
|
|
_, err := s.DB.ExecContext(ctx, `UPDATE operations SET state = ?, completed_at = strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE id = ?`, model.OpCompleted, id)
|
|
return err
|
|
}
|
|
|
|
func (s *Operations) Fail(ctx context.Context, id int64, errMsg string) error {
|
|
_, err := s.DB.ExecContext(ctx, `UPDATE operations SET state = ?, completed_at = strftime('%Y-%m-%dT%H:%M:%SZ','now'), error_message = ? WHERE id = ?`, model.OpFailed, errMsg, id)
|
|
return err
|
|
}
|
|
|
|
func (s *Operations) ListByHost(ctx context.Context, hostID int64) ([]model.Operation, error) {
|
|
rows, err := s.DB.QueryContext(ctx, `
|
|
SELECT id, host_id, kind, state, COALESCE(image_id, 0), started_at, completed_at, COALESCE(error_message, '')
|
|
FROM operations WHERE host_id = ? ORDER BY started_at DESC
|
|
`, hostID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list operations: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
var out []model.Operation
|
|
for rows.Next() {
|
|
var op model.Operation
|
|
var startedAt string
|
|
var completedAt sql.NullString
|
|
if err := rows.Scan(&op.ID, &op.HostID, &op.Kind, &op.State, &op.ImageID, &startedAt, &completedAt, &op.ErrorMessage); err != nil {
|
|
return nil, fmt.Errorf("scan operation: %w", err)
|
|
}
|
|
op.StartedAt, _ = time.Parse(time.RFC3339, startedAt)
|
|
if completedAt.Valid {
|
|
t, _ := time.Parse(time.RFC3339, completedAt.String)
|
|
op.CompletedAt = &t
|
|
}
|
|
out = append(out, op)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func (s *Operations) Get(ctx context.Context, id int64) (*model.Operation, error) {
|
|
row := s.DB.QueryRowContext(ctx, `
|
|
SELECT id, host_id, kind, state, COALESCE(image_id, 0), started_at, completed_at, COALESCE(error_message, '')
|
|
FROM operations WHERE id = ?
|
|
`, id)
|
|
var op model.Operation
|
|
var startedAt string
|
|
var completedAt sql.NullString
|
|
if err := row.Scan(&op.ID, &op.HostID, &op.Kind, &op.State, &op.ImageID, &startedAt, &completedAt, &op.ErrorMessage); err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, ErrNotFound
|
|
}
|
|
return nil, fmt.Errorf("get operation: %w", err)
|
|
}
|
|
op.StartedAt, _ = time.Parse(time.RFC3339, startedAt)
|
|
if completedAt.Valid {
|
|
t, _ := time.Parse(time.RFC3339, completedAt.String)
|
|
op.CompletedAt = &t
|
|
}
|
|
return &op, nil
|
|
}
|
|
|
|
func (s *Operations) GetActive(ctx context.Context, hostID int64) (*model.Operation, error) {
|
|
row := s.DB.QueryRowContext(ctx, `
|
|
SELECT id, host_id, kind, state, COALESCE(image_id, 0), started_at, completed_at, COALESCE(error_message, '')
|
|
FROM operations WHERE host_id = ? AND state = ? ORDER BY started_at DESC LIMIT 1
|
|
`, hostID, model.OpActive)
|
|
var op model.Operation
|
|
var startedAt string
|
|
var completedAt sql.NullString
|
|
if err := row.Scan(&op.ID, &op.HostID, &op.Kind, &op.State, &op.ImageID, &startedAt, &completedAt, &op.ErrorMessage); err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, ErrNotFound
|
|
}
|
|
return nil, fmt.Errorf("get active operation: %w", err)
|
|
}
|
|
op.StartedAt, _ = time.Parse(time.RFC3339, startedAt)
|
|
return &op, nil
|
|
}
|
|
|
|
func nullInt64(v int64) any {
|
|
if v == 0 {
|
|
return nil
|
|
}
|
|
return v
|
|
}
|