using Microsoft.AspNetCore.Mvc; using Nexus.Api.Models; using Nexus.Api.Services; namespace Nexus.Api.Controllers; [ApiController] [Route("api/dashboard")] public class DashboardController(OpenClawGatewayClient gateway, ILogger logger) : ControllerBase { /// /// Gateway health + session_status + subagents count. /// Returns HTTP 200 even when gateway is down (gatewayOk: false). /// [HttpGet("status")] public async Task GetStatus() { try { return await gateway.GetStatusAsync(); } catch (Exception ex) { logger.LogWarning(ex, "Dashboard status check failed"); return new DashboardStatus(false, "Offline", 0, 0); } } /// /// Returns all agents with their current status. /// Combines sessions_list + sub_agents_list. /// [HttpGet("agents")] public async Task> GetAgents() { try { return await gateway.GetAgentsAsync(); } catch (Exception ex) { logger.LogWarning(ex, "Dashboard agents fetch failed"); return new List(); } } /// /// Returns the latest assistant messages (operations/feed) from the Iris session. /// Filtered to role == "assistant" — those are the work feed entries. /// [HttpGet("operations")] public async Task> GetOperations([FromQuery] int limit = 20) { try { var messages = await gateway.GetSessionHistoryAsync("iris", Math.Clamp(limit, 1, 100)); var feed = new List(); foreach (var msg in messages) { if (!string.Equals(msg.Role, "assistant", StringComparison.OrdinalIgnoreCase)) continue; if (string.IsNullOrWhiteSpace(msg.Content)) continue; // Parse timestamp for display-friendly "time ago" var ts = ParseTimestamp(msg.Timestamp); var timeAgo = FormatTimeAgo(ts); // Extract a short agent indicator and action from content var (agent, action) = ExtractAgentAction(msg.Content); feed.Add(new FeedEntry(agent, action, msg.Timestamp, timeAgo)); } return feed; } catch (Exception ex) { logger.LogWarning(ex, "Dashboard operations fetch failed"); return new List(); } } /// /// Send a chat message to the Iris session. /// [HttpPost("chat/send")] public async Task SendChat([FromBody] ChatRequest request) { if (string.IsNullOrWhiteSpace(request.Message)) return new ChatResponse(false, null, "Message is required"); try { var agentId = string.IsNullOrWhiteSpace(request.AgentId) ? "iris" : request.AgentId.Trim(); return await gateway.SendChatMessageAsync(agentId, request.Message.Trim()); } catch (Exception ex) { logger.LogWarning(ex, "Dashboard chat send failed"); return new ChatResponse(false, null, "Gateway nicht erreichbar"); } } /// /// Returns chat messages (user + assistant only, not tool messages). /// [HttpGet("chat/messages")] public async Task> GetMessages( [FromQuery] string? sessionKey, [FromQuery] int limit = 50, [FromQuery] int offset = 0) { try { var key = string.IsNullOrWhiteSpace(sessionKey) ? "agent:iris:main" : sessionKey.Trim(); var messages = await gateway.GetSessionHistoryAsync(key, Math.Clamp(limit, 1, 200), Math.Max(0, offset)); return messages .Where(m => string.Equals(m.Role, "user", StringComparison.OrdinalIgnoreCase) || string.Equals(m.Role, "assistant", StringComparison.OrdinalIgnoreCase)) .ToList(); } catch (Exception ex) { logger.LogWarning(ex, "Dashboard messages fetch failed"); return new List(); } } /// /// Returns the cron queue / pending tasks. /// [HttpGet("queue")] public async Task> GetQueue() { try { return await gateway.GetQueueAsync(); } catch (Exception ex) { logger.LogWarning(ex, "Dashboard queue fetch failed"); return new List(); } } /// /// Returns the current model and provider for a specific agent session. /// Calls session_status with the agent's session key. /// [HttpGet("agents/{id}/model")] public async Task> GetAgentModel(string id) { try { var info = await gateway.GetAgentModelAsync(id); if (info is null) return NotFound(new { error = $"Agent '{id}' not found or gateway unreachable" }); return Ok(info); } catch (Exception ex) { logger.LogWarning(ex, "GetAgentModel failed for {AgentId}", id); return StatusCode(500, new { error = "Internal error" }); } } /// /// Sets the model for a specific agent session. /// Calls session_status with model parameter. /// [HttpPut("agents/{id}/model")] public async Task SetAgentModel(string id, [FromBody] SetModelRequest request) { if (string.IsNullOrWhiteSpace(request.Model)) return BadRequest(new { error = "Model is required" }); try { var ok = await gateway.SetAgentModelAsync(id, request.Model); if (!ok) return StatusCode(502, new { error = "Gateway did not accept the change" }); return Ok(new { status = "ok", model = request.Model }); } catch (Exception ex) { logger.LogWarning(ex, "SetAgentModel failed for {AgentId}", id); return StatusCode(500, new { error = "Internal error" }); } } /// /// Returns the list of available models that can be assigned to agents. /// [HttpGet("models")] public ActionResult> GetAvailableModels() { var models = new List { new ModelOption("openai/gpt-5.4", "GPT-5.4", "openai"), new ModelOption("deepseek/deepseek-v4-flash", "DeepSeek V4 Flash", "deepseek"), new ModelOption("deepseek/deepseek-v4-pro", "DeepSeek V4 Pro", "deepseek") }; return Ok(models); } // ========== Helpers ========== private static DateTimeOffset ParseTimestamp(string timestamp) { if (DateTimeOffset.TryParse(timestamp, null, System.Globalization.DateTimeStyles.None, out var dt)) return dt; return DateTimeOffset.UtcNow; } private static string FormatTimeAgo(DateTimeOffset ts) { var diff = DateTimeOffset.UtcNow - ts; if (diff.TotalMinutes < 1) return "just now"; if (diff.TotalMinutes < 60) return $"{(int)diff.TotalMinutes}m ago"; if (diff.TotalHours < 24) return $"{(int)diff.TotalHours}h ago"; if (diff.TotalDays < 7) return $"{(int)diff.TotalDays}d ago"; return ts.ToString("MMM dd"); } private static (string Agent, string Action) ExtractAgentAction(string content) { // Take first line or first ~80 chars as the action summary var firstLine = content.Split('\n', 2)[0].Trim(); var summary = firstLine.Length > 80 ? firstLine[..80] + "…" : firstLine; // Try to identify which agent this came from var agent = "Iris"; foreach (var marker in new[] { "**Agent:**", "**Agent:** ", "*Agent:* ", "Agent:" }) { var idx = content.IndexOf(marker, StringComparison.OrdinalIgnoreCase); if (idx >= 0) { var after = content[(idx + marker.Length)..].TrimStart(); var end = after.IndexOfAny(['\n', '\r', ',', '.']); var found = end > 0 ? after[..end].Trim() : after.Split('\n', 2)[0].Trim(); if (!string.IsNullOrWhiteSpace(found) && found.Length < 30) { agent = found; break; } } } // Try to find agent name at the start in brackets like [Agent: Iris] if (agent == "Iris") { var bracketMatch = System.Text.RegularExpressions.Regex.Match(content, @"\[Agent:\s*([^\]]+)\]"); if (bracketMatch.Success) agent = bracketMatch.Groups[1].Value.Trim(); } return (agent, summary); } }