CUB-127: implement Control Center CRUD API in Go
This commit is contained in:
177
go-backend/internal/store/agent.go
Normal file
177
go-backend/internal/store/agent.go
Normal file
@@ -0,0 +1,177 @@
|
||||
// Package store provides thread-safe in-memory data stores for the
|
||||
// Control Center API. These will be replaced with PostgreSQL-backed
|
||||
// implementations once CUB-120 (schema design) is complete.
|
||||
package store
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"code.cubecraftcreations.com/CubeCraft-Creations/Control-Center/go-backend/internal/models"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// AgentStore provides thread-safe CRUD operations for agents.
|
||||
type AgentStore struct {
|
||||
mu sync.RWMutex
|
||||
agents map[string]models.AgentCardData
|
||||
history map[string][]models.AgentStatusHistoryEntry // agentID -> history
|
||||
}
|
||||
|
||||
// NewAgentStore returns an initialized AgentStore.
|
||||
func NewAgentStore() *AgentStore {
|
||||
return &AgentStore{
|
||||
agents: make(map[string]models.AgentCardData),
|
||||
history: make(map[string][]models.AgentStatusHistoryEntry),
|
||||
}
|
||||
}
|
||||
|
||||
// List returns all agents, optionally filtered by status.
|
||||
func (s *AgentStore) List(statusFilter models.AgentStatus) []models.AgentCardData {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
result := make([]models.AgentCardData, 0, len(s.agents))
|
||||
for _, a := range s.agents {
|
||||
if statusFilter != "" && a.Status != statusFilter {
|
||||
continue
|
||||
}
|
||||
result = append(result, a)
|
||||
}
|
||||
|
||||
// Sort by display name for consistent output.
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return result[i].DisplayName < result[j].DisplayName
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// Get returns a single agent by ID, or false if not found.
|
||||
func (s *AgentStore) Get(id string) (models.AgentCardData, bool) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
a, ok := s.agents[id]
|
||||
return a, ok
|
||||
}
|
||||
|
||||
// Create inserts a new agent. Returns false if the ID already exists.
|
||||
func (s *AgentStore) Create(a models.AgentCardData) bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if _, exists := s.agents[a.ID]; exists {
|
||||
return false
|
||||
}
|
||||
if a.LastActivity == "" {
|
||||
a.LastActivity = time.Now().UTC().Format(time.RFC3339)
|
||||
}
|
||||
s.agents[a.ID] = a
|
||||
|
||||
// Record initial history entry.
|
||||
s.appendHistoryLocked(a.ID, models.AgentStatusHistoryEntry{
|
||||
ID: uuid.New().String(),
|
||||
AgentID: a.ID,
|
||||
Status: a.Status,
|
||||
Task: a.CurrentTask,
|
||||
Timestamp: a.LastActivity,
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
// Update applies partial updates to an agent. Returns the updated agent or false if not found.
|
||||
func (s *AgentStore) Update(id string, req models.UpdateAgentRequest) (models.AgentCardData, bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
a, ok := s.agents[id]
|
||||
if !ok {
|
||||
return models.AgentCardData{}, false
|
||||
}
|
||||
|
||||
prevStatus := a.Status
|
||||
prevTask := a.CurrentTask
|
||||
|
||||
if req.Status != nil {
|
||||
a.Status = *req.Status
|
||||
}
|
||||
if req.CurrentTask != nil {
|
||||
a.CurrentTask = req.CurrentTask
|
||||
}
|
||||
if req.TaskProgress != nil {
|
||||
a.TaskProgress = req.TaskProgress
|
||||
}
|
||||
if req.TaskElapsed != nil {
|
||||
a.TaskElapsed = req.TaskElapsed
|
||||
}
|
||||
if req.Channel != nil {
|
||||
a.Channel = *req.Channel
|
||||
}
|
||||
if req.ErrorMessage != nil {
|
||||
a.ErrorMessage = req.ErrorMessage
|
||||
}
|
||||
a.LastActivity = time.Now().UTC().Format(time.RFC3339)
|
||||
s.agents[id] = a
|
||||
|
||||
// Record history entry if status or task changed.
|
||||
if (req.Status != nil && *req.Status != prevStatus) || (req.CurrentTask != nil && prevTask == nil) ||
|
||||
(req.CurrentTask != nil && prevTask != nil && *req.CurrentTask != *prevTask) {
|
||||
status := a.Status
|
||||
if req.Status != nil {
|
||||
status = *req.Status
|
||||
}
|
||||
s.appendHistoryLocked(id, models.AgentStatusHistoryEntry{
|
||||
ID: uuid.New().String(),
|
||||
AgentID: id,
|
||||
Status: status,
|
||||
Task: a.CurrentTask,
|
||||
Timestamp: a.LastActivity,
|
||||
})
|
||||
}
|
||||
|
||||
return a, true
|
||||
}
|
||||
|
||||
// Delete removes an agent. Returns true if the agent existed.
|
||||
func (s *AgentStore) Delete(id string) bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if _, ok := s.agents[id]; !ok {
|
||||
return false
|
||||
}
|
||||
delete(s.agents, id)
|
||||
delete(s.history, id)
|
||||
return true
|
||||
}
|
||||
|
||||
// History returns the status history for an agent, newest first.
|
||||
func (s *AgentStore) History(agentID string) []models.AgentStatusHistoryEntry {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
entries, ok := s.history[agentID]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
// Return a defensive copy, sorted newest first (by index when timestamps tie).
|
||||
result := make([]models.AgentStatusHistoryEntry, len(entries))
|
||||
copy(result, entries)
|
||||
sort.SliceStable(result, func(i, j int) bool {
|
||||
if result[i].Timestamp == result[j].Timestamp {
|
||||
return i > j // later index = newer when timestamps match
|
||||
}
|
||||
return result[i].Timestamp > result[j].Timestamp
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// Count returns the total number of agents.
|
||||
func (s *AgentStore) Count() int {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return len(s.agents)
|
||||
}
|
||||
|
||||
// appendHistoryLocked adds a history entry. Caller must hold s.mu (write lock).
|
||||
func (s *AgentStore) appendHistoryLocked(agentID string, entry models.AgentStatusHistoryEntry) {
|
||||
s.history[agentID] = append(s.history[agentID], entry)
|
||||
}
|
||||
67
go-backend/internal/store/project.go
Normal file
67
go-backend/internal/store/project.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"code.cubecraftcreations.com/CubeCraft-Creations/Control-Center/go-backend/internal/models"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ProjectStore provides thread-safe CRUD operations for projects.
|
||||
type ProjectStore struct {
|
||||
mu sync.RWMutex
|
||||
projects map[string]models.Project
|
||||
}
|
||||
|
||||
// NewProjectStore returns an initialized ProjectStore.
|
||||
func NewProjectStore() *ProjectStore {
|
||||
return &ProjectStore{
|
||||
projects: make(map[string]models.Project),
|
||||
}
|
||||
}
|
||||
|
||||
// List returns all projects ordered by name.
|
||||
func (s *ProjectStore) List() []models.Project {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
result := make([]models.Project, 0, len(s.projects))
|
||||
for _, p := range s.projects {
|
||||
result = append(result, p)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return result[i].Name < result[j].Name
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// Create inserts a new project.
|
||||
func (s *ProjectStore) Create(p models.Project) models.Project {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if p.ID == "" {
|
||||
p.ID = uuid.New().String()
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
if p.CreatedAt.IsZero() {
|
||||
p.CreatedAt = now
|
||||
}
|
||||
if p.UpdatedAt.IsZero() {
|
||||
p.UpdatedAt = now
|
||||
}
|
||||
if p.AgentIDs == nil {
|
||||
p.AgentIDs = []string{}
|
||||
}
|
||||
s.projects[p.ID] = p
|
||||
return p
|
||||
}
|
||||
|
||||
// Count returns the total number of projects.
|
||||
func (s *ProjectStore) Count() int {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return len(s.projects)
|
||||
}
|
||||
80
go-backend/internal/store/session.go
Normal file
80
go-backend/internal/store/session.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"code.cubecraftcreations.com/CubeCraft-Creations/Control-Center/go-backend/internal/models"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// SessionStore provides thread-safe CRUD operations for sessions.
|
||||
type SessionStore struct {
|
||||
mu sync.RWMutex
|
||||
sessions map[string]models.Session // id -> session
|
||||
}
|
||||
|
||||
// NewSessionStore returns an initialized SessionStore.
|
||||
func NewSessionStore() *SessionStore {
|
||||
return &SessionStore{
|
||||
sessions: make(map[string]models.Session),
|
||||
}
|
||||
}
|
||||
|
||||
// ListActive returns all sessions with status "running" or "streaming", newest first.
|
||||
func (s *SessionStore) ListActive() []models.Session {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
result := make([]models.Session, 0)
|
||||
for _, sess := range s.sessions {
|
||||
if sess.Status == "running" || sess.Status == "streaming" {
|
||||
result = append(result, sess)
|
||||
}
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return result[i].LastActivityAt.After(result[j].LastActivityAt)
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// Create inserts a new session.
|
||||
func (s *SessionStore) Create(sess models.Session) models.Session {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if sess.ID == "" {
|
||||
sess.ID = uuid.New().String()
|
||||
}
|
||||
if sess.StartedAt.IsZero() {
|
||||
sess.StartedAt = time.Now().UTC()
|
||||
}
|
||||
if sess.LastActivityAt.IsZero() {
|
||||
sess.LastActivityAt = sess.StartedAt
|
||||
}
|
||||
s.sessions[sess.ID] = sess
|
||||
return sess
|
||||
}
|
||||
|
||||
// UpdateStatus updates the status and last-activity timestamp of a session.
|
||||
func (s *SessionStore) UpdateStatus(id, status string) bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
sess, ok := s.sessions[id]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
sess.Status = status
|
||||
sess.LastActivityAt = time.Now().UTC()
|
||||
s.sessions[id] = sess
|
||||
return true
|
||||
}
|
||||
|
||||
// Count returns the total number of sessions.
|
||||
func (s *SessionStore) Count() int {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return len(s.sessions)
|
||||
}
|
||||
64
go-backend/internal/store/task.go
Normal file
64
go-backend/internal/store/task.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"code.cubecraftcreations.com/CubeCraft-Creations/Control-Center/go-backend/internal/models"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// TaskStore provides thread-safe CRUD operations for tasks.
|
||||
type TaskStore struct {
|
||||
mu sync.RWMutex
|
||||
tasks map[string]models.Task
|
||||
}
|
||||
|
||||
// NewTaskStore returns an initialized TaskStore.
|
||||
func NewTaskStore() *TaskStore {
|
||||
return &TaskStore{
|
||||
tasks: make(map[string]models.Task),
|
||||
}
|
||||
}
|
||||
|
||||
// ListRecent returns tasks ordered by updated_at descending (newest first).
|
||||
func (s *TaskStore) ListRecent() []models.Task {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
result := make([]models.Task, 0, len(s.tasks))
|
||||
for _, t := range s.tasks {
|
||||
result = append(result, t)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return result[i].UpdatedAt.After(result[j].UpdatedAt)
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// Create inserts a new task.
|
||||
func (s *TaskStore) Create(t models.Task) models.Task {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if t.ID == "" {
|
||||
t.ID = uuid.New().String()
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
if t.CreatedAt.IsZero() {
|
||||
t.CreatedAt = now
|
||||
}
|
||||
if t.UpdatedAt.IsZero() {
|
||||
t.UpdatedAt = now
|
||||
}
|
||||
s.tasks[t.ID] = t
|
||||
return t
|
||||
}
|
||||
|
||||
// Count returns the total number of tasks.
|
||||
func (s *TaskStore) Count() int {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return len(s.tasks)
|
||||
}
|
||||
Reference in New Issue
Block a user