using System.Net.WebSockets; using CopilotCoordinator.Models; using CopilotCoordinator.Services; var builder = WebApplication.CreateBuilder(args); // Configure port from config (default 8090) var port = builder.Configuration.GetValue("Port", 8090); builder.WebHost.UseUrls($"http://0.0.0.0:{port}"); // Register services builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddHttpClient(); var app = builder.Build(); // Load sequence definitions from config file if present var seqRunner = app.Services.GetRequiredService(); var seqConfig = builder.Configuration.GetSection("Sequences"); if (seqConfig.Exists()) { var sequences = seqConfig.Get>() ?? new(); var categories = builder.Configuration.GetSection("SequenceCategories").Get>() ?? new(); seqRunner.LoadSequences(sequences, categories); } // --- Lock expiration background timer (1 second, like legacy CameraLockExpirationWorker) --- var lockManager = app.Services.GetRequiredService(); var expirationTimer = new PeriodicTimer(TimeSpan.FromSeconds(1)); _ = Task.Run(async () => { while (await expirationTimer.WaitForNextTickAsync()) { try { await lockManager.CheckExpirations(); } catch (Exception ex) { app.Logger.LogError(ex, "Lock expiration check failed"); } } }); // --- WebSocket endpoint --- app.UseWebSockets(); app.Map("/ws", async (HttpContext ctx) => { if (!ctx.WebSockets.IsWebSocketRequest) { ctx.Response.StatusCode = 400; return; } var keyboardId = ctx.Request.Query["keyboard"].FirstOrDefault() ?? "unknown"; var ws = await ctx.WebSockets.AcceptWebSocketAsync(); var clientId = Guid.NewGuid().ToString(); var broadcaster = ctx.RequestServices.GetRequiredService(); await broadcaster.HandleConnection(ws, clientId, keyboardId); }); // --- Health & Status --- app.MapGet("/health", () => Results.Ok(new { status = "ok", timestamp = DateTime.UtcNow })); app.MapGet("/status", async (LockManager locks, SequenceRunner sequences, WsBroadcaster ws) => { var allLocks = await locks.GetAllLocks(); var running = await sequences.GetRunning(); var keyboards = ws.GetConnectedKeyboards(); return Results.Ok(new { status = "ok", keyboards = keyboards.Count, keyboardList = keyboards, activeLocks = allLocks.Count, locks = allLocks, runningSequences = running.Count, sequences = running }); }); // --- Lock endpoints --- app.MapPost("/locks/try", async (LockRequest req, LockManager mgr) => { var priority = Enum.TryParse(req.Priority, true, out var p) ? p : CameraLockPriority.Low; var result = await mgr.TryLock(req.CameraId, req.KeyboardId, priority); return Results.Ok(result); }); app.MapPost("/locks/release", async (UnlockRequest req, LockManager mgr) => { await mgr.Unlock(req.CameraId, req.KeyboardId); return Results.Ok(new { success = true }); }); app.MapPost("/locks/takeover", async (TakeoverRequest req, LockManager mgr) => { var priority = Enum.TryParse(req.Priority, true, out var p) ? p : CameraLockPriority.Low; await mgr.RequestTakeover(req.CameraId, req.KeyboardId, priority); return Results.Ok(new { success = true }); }); app.MapPost("/locks/confirm", async (TakeoverConfirmRequest req, LockManager mgr) => { await mgr.ConfirmTakeover(req.CameraId, req.KeyboardId, req.Confirm); return Results.Ok(new { success = true }); }); app.MapPost("/locks/reset", async (ResetExpirationRequest req, LockManager mgr) => { await mgr.ResetExpiration(req.CameraId, req.KeyboardId); return Results.Ok(new { success = true }); }); app.MapGet("/locks", async (LockManager mgr) => { var locks = await mgr.GetAllLocks(); return Results.Ok(locks); }); app.MapGet("/locks/{keyboardId}", async (string keyboardId, LockManager mgr) => { var ids = await mgr.GetLockedCameraIds(keyboardId); return Results.Ok(ids); }); // --- Sequence endpoints --- app.MapPost("/sequences/start", async (SequenceStartRequest req, SequenceRunner runner) => { var result = await runner.Start(req.ViewerId, req.SequenceId); return result != null ? Results.Ok(result) : Results.NotFound(new { error = "Sequence not found" }); }); app.MapPost("/sequences/stop", async (SequenceStopRequest req, SequenceRunner runner) => { await runner.Stop(req.ViewerId); return Results.Ok(new { success = true }); }); app.MapGet("/sequences/running", async (SequenceRunner runner) => { var running = await runner.GetRunning(); return Results.Ok(running); }); app.MapGet("/sequences", (int? categoryId, SequenceRunner runner) => { var sequences = runner.GetSequences(categoryId); return Results.Ok(sequences); }); app.MapGet("/sequences/categories", (SequenceRunner runner) => { return Results.Ok(runner.GetCategories()); }); app.Logger.LogInformation("COPILOT Coordinator starting on port {Port}", port); app.Run();