diff --git a/backend/ControlCenter/ControlCenter.csproj b/backend/ControlCenter/ControlCenter.csproj index 66ebc83..b688abd 100644 --- a/backend/ControlCenter/ControlCenter.csproj +++ b/backend/ControlCenter/ControlCenter.csproj @@ -11,6 +11,8 @@ + + diff --git a/backend/ControlCenter/Data/Configurations/AgentStateConfiguration.cs b/backend/ControlCenter/Data/Configurations/AgentStateConfiguration.cs new file mode 100644 index 0000000..96df065 --- /dev/null +++ b/backend/ControlCenter/Data/Configurations/AgentStateConfiguration.cs @@ -0,0 +1,69 @@ +using ControlCenter.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace ControlCenter.Data.Configurations; + +/// +/// EF Core entity configuration for . +/// +/// Maps to the agents table with snake_case column naming, +/// consistent with the Extrudex project conventions. +/// +/// CUB-56 will create the migration; this configuration only +/// defines the entity-to-table mapping and constraints. +/// +public class AgentStateConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + // Table name + builder.ToTable("agents"); + + // Primary key + builder.HasKey(a => a.Id); + + // Properties with snake_case column names + builder.Property(a => a.Id) + .HasColumnName("id") + .ValueGeneratedOnAdd(); + + builder.Property(a => a.Status) + .HasColumnName("status") + .IsRequired() + .HasMaxLength(20) + .HasDefaultValue("idle"); + + builder.Property(a => a.Task) + .HasColumnName("task") + .IsRequired(false) + .HasMaxLength(500); + + builder.Property(a => a.Progress) + .HasColumnName("progress") + .IsRequired(false); + + builder.Property(a => a.SessionKey) + .HasColumnName("session_key") + .IsRequired() + .HasMaxLength(255); + + builder.Property(a => a.Channel) + .HasColumnName("channel") + .IsRequired() + .HasMaxLength(50); + + builder.Property(a => a.LastActivity) + .HasColumnName("last_activity") + .IsRequired() + .HasDefaultValueSql("NOW()"); + + // Indexes + builder.HasIndex(a => a.SessionKey) + .IsUnique() + .HasDatabaseName("ix_agents_session_key"); + + builder.HasIndex(a => a.Status) + .HasDatabaseName("ix_agents_status"); + } +} \ No newline at end of file diff --git a/backend/ControlCenter/Data/ControlCenterDbContext.cs b/backend/ControlCenter/Data/ControlCenterDbContext.cs new file mode 100644 index 0000000..1a7fd63 --- /dev/null +++ b/backend/ControlCenter/Data/ControlCenterDbContext.cs @@ -0,0 +1,39 @@ +using ControlCenter.Models; +using Microsoft.EntityFrameworkCore; + +namespace ControlCenter.Data; + +/// +/// EF Core DbContext for the Control Center database. +/// +/// Provides access to all persistent entities. +/// Uses snake_case naming conventions for PostgreSQL columns, +/// applied via . +/// +/// Connection string is configured in appsettings.json +/// under ConnectionStrings:ControlCenterDb. +/// +public class ControlCenterDbContext : DbContext +{ + /// + /// Agent state records — one row per active or recently active agent. + /// Maps to the agents table. + /// + public DbSet AgentStates => Set(); + + public ControlCenterDbContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // Apply all entity configurations from this assembly + modelBuilder.ApplyConfigurationsFromAssembly( + typeof(Configurations.AgentStateConfiguration).Assembly); + + // Global snake_case naming convention for any unmapped columns + // (explicit configurations take precedence) + base.OnModelCreating(modelBuilder); + } +} \ No newline at end of file diff --git a/backend/ControlCenter/Models/AgentState.cs b/backend/ControlCenter/Models/AgentState.cs new file mode 100644 index 0000000..500cb20 --- /dev/null +++ b/backend/ControlCenter/Models/AgentState.cs @@ -0,0 +1,59 @@ +namespace ControlCenter.Models; + +/// +/// Persistent state for an agent in the Control Center. +/// Maps to the agents table in PostgreSQL. +/// +/// Tracks the operational status, current task, progress, +/// session identity, and last activity timestamp for each agent +/// in the minion fleet. +/// +/// The property uses string values matching +/// the AgentStatus enum: "active", "idle", "thinking", "error". +/// Stored as varchar rather than a DB enum for schema flexibility. +/// +public class AgentState +{ + /// + /// Unique identifier for the agent state record. + /// Defaults to a new on creation. + /// + public Guid Id { get; set; } = Guid.NewGuid(); + + /// + /// Operational status of the agent. + /// Valid values: "active", "idle", "thinking", "error". + /// Maps to the AgentStatus enum used by SignalR. + /// + public string Status { get; set; } = "idle"; + + /// + /// Description of the agent's current task, if any. + /// Null when the agent is idle with no active assignment. + /// + public string? Task { get; set; } + + /// + /// Task progress percentage (0–100). + /// Null when progress is not trackable for the current task. + /// + public int? Progress { get; set; } + + /// + /// Full session key identifying the agent's active session. + /// Format: agent:{agentId}:{channel}:... + /// Used to correlate SignalR events with persistent state. + /// + public string SessionKey { get; set; } = string.Empty; + + /// + /// The channel the agent is operating on (e.g., "telegram", "discord", "slack"). + /// + public string Channel { get; set; } = string.Empty; + + /// + /// Timestamp of the agent's last recorded activity. + /// Updated on every status change or task progress event. + /// + public DateTime LastActivity { get; set; } = DateTime.UtcNow; +} \ No newline at end of file diff --git a/backend/ControlCenter/Program.cs b/backend/ControlCenter/Program.cs index 757b20a..aee367d 100644 --- a/backend/ControlCenter/Program.cs +++ b/backend/ControlCenter/Program.cs @@ -1,6 +1,9 @@ using System.Reflection; +using ControlCenter.Data; using ControlCenter.Hubs; +using ControlCenter.Repositories; using ControlCenter.Services; +using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); @@ -25,6 +28,19 @@ builder.Services.AddSwaggerGen(c => } }); +// ── Database ──────────────────────────────────────────────── +// PostgreSQL via EF Core with Npgsql. +// Connection string from appsettings.json or environment variable. +builder.Services.AddDbContext(options => + options.UseNpgsql( + builder.Configuration.GetConnectionString("ControlCenterDb") + ?? throw new InvalidOperationException( + "Connection string 'ControlCenterDb' not found. " + + "Add it to appsettings.json or set the environment variable."))); + +// ── Repositories ──────────────────────────────────────────── +builder.Services.AddScoped(); + // ── CORS (kiosk + remote browser) ───────────────────────── // The Control Center frontend runs on a different origin than the backend. // SignalR requires credentials for WebSocket transport, so we use diff --git a/backend/ControlCenter/Repositories/AgentStateRepository.cs b/backend/ControlCenter/Repositories/AgentStateRepository.cs new file mode 100644 index 0000000..752f8f7 --- /dev/null +++ b/backend/ControlCenter/Repositories/AgentStateRepository.cs @@ -0,0 +1,62 @@ +using ControlCenter.Data; +using ControlCenter.Models; +using Microsoft.EntityFrameworkCore; + +namespace ControlCenter.Repositories; + +/// +/// EF Core implementation of . +/// +/// Queries the agents table via +/// using snake_case column mappings defined in the . +/// All write operations call SaveChangesAsync immediately. +/// +public class AgentStateRepository : IAgentStateRepository +{ + private readonly ControlCenterDbContext _context; + private readonly ILogger _logger; + + public AgentStateRepository( + ControlCenterDbContext context, + ILogger logger) + { + _context = context; + _logger = logger; + } + + /// + public async Task> GetAllAsync() + { + _logger.LogDebug("Fetching all agent states"); + return await _context.AgentStates.ToListAsync(); + } + + /// + public async Task GetBySessionKeyAsync(string sessionKey) + { + _logger.LogDebug("Looking up agent state by session key: {SessionKey}", sessionKey); + return await _context.AgentStates + .FirstOrDefaultAsync(a => a.SessionKey == sessionKey); + } + + /// + public async Task UpdateStatusAsync(Guid id, string status) + { + _logger.LogDebug("Updating agent status: {Id} → {Status}", id, status); + + var entity = await _context.AgentStates.FindAsync(id); + if (entity is null) + { + _logger.LogWarning("Agent state not found: {Id}", id); + return null; + } + + entity.Status = status; + entity.LastActivity = DateTime.UtcNow; + + _context.AgentStates.Update(entity); + await _context.SaveChangesAsync(); + + return entity; + } +} \ No newline at end of file diff --git a/backend/ControlCenter/Repositories/IAgentStateRepository.cs b/backend/ControlCenter/Repositories/IAgentStateRepository.cs new file mode 100644 index 0000000..6097439 --- /dev/null +++ b/backend/ControlCenter/Repositories/IAgentStateRepository.cs @@ -0,0 +1,38 @@ +using ControlCenter.Models; + +namespace ControlCenter.Repositories; + +/// +/// Repository interface for querying and updating agent state. +/// +/// Provides the data-access contract used by controllers, +/// background services, and SignalR hubs to read and mutate +/// persistent agent state. +/// +/// Implementation should use +/// via EF Core with PostgreSQL (snake_case columns). +/// +public interface IAgentStateRepository +{ + /// + /// Returns all agent states from the database. + /// + /// A collection of all records. + Task> GetAllAsync(); + + /// + /// Finds an agent state by its session key. + /// + /// The full session key, e.g. "agent:dex:telegram:direct:...". + /// The matching , or null if not found. + Task GetBySessionKeyAsync(string sessionKey); + + /// + /// Updates the status of an agent state record. + /// Also updates LastActivity to . + /// + /// The unique identifier of the agent state record. + /// The new status value ("active", "idle", "thinking", "error"). + /// The updated , or null if the record was not found. + Task UpdateStatusAsync(Guid id, string status); +} \ No newline at end of file diff --git a/backend/ControlCenter/appsettings.json b/backend/ControlCenter/appsettings.json index 60f801d..25d46d9 100644 --- a/backend/ControlCenter/appsettings.json +++ b/backend/ControlCenter/appsettings.json @@ -8,6 +8,10 @@ }, "AllowedHosts": "*", + "ConnectionStrings": { + "ControlCenterDb": "Host=localhost;Port=5432;Database=control_center;Username=control_center;Password=changeme" + }, + "Gateway": { "WebSocketUrl": "ws://localhost:3271/ws", "AuthToken": ""