feat(dashboard): dynamic agent metrics + model list from config, no more hardcoded data
CI - Build & Test / Backend (.NET) (push) Failing after 23s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 15s
CI - Build & Test / Security Check (push) Successful in 3s

This commit is contained in:
2026-06-11 16:01:33 +02:00
parent 45c6b24928
commit c29740a466
4 changed files with 266 additions and 11 deletions
+2 -6
View File
@@ -345,16 +345,12 @@ public class DashboardController(
/// <summary>
/// Returns the list of available models that can be assigned to agents.
/// Reads from OpenClaw config dynamically, falls back to hardcoded list.
/// </summary>
[HttpGet("models")]
public ActionResult<List<ModelOption>> GetAvailableModels()
{
var models = new List<ModelOption>
{
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")
};
var models = gateway.GetAvailableModels();
return Ok(models);
}
+4 -1
View File
@@ -8,7 +8,10 @@ public sealed record DashboardAgentInfo(
bool IsActive,
string? CurrentTask,
string? Description,
string[] Tags
string[] Tags,
int Progress = 0,
int Workload = 0,
string? Goal = null
);
public sealed record MessageEntry(
+253 -1
View File
@@ -119,6 +119,11 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration
var agentIds = LoadAgentIdsFromConfig();
var agents = new List<DashboardAgentInfo>();
// Read queue once for workload calculation
var queueItems = new List<QueueItem>();
try { queueItems = await GetQueueAsync(); } catch { }
foreach (var id in agentIds)
{
// Skip the "main" agent (it's the default assistant, not a sub-agent)
@@ -177,6 +182,15 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration
};
}
// 6. Read goal from workspace files
var goal = await ReadAgentGoalAsync(id);
// 7. Calculate progress dynamically
var progress = CalculateAgentProgress(id, isActive, status);
// 8. Calculate workload from queue items
var workload = CalculateAgentWorkload(id, queueItems);
agents.Add(new DashboardAgentInfo(
Id: id,
Name: string.IsNullOrWhiteSpace(name) ? DeriveRole(id) : name,
@@ -185,7 +199,10 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration
IsActive: isActive,
CurrentTask: currentTask,
Description: description,
Tags: tags
Tags: tags,
Progress: progress,
Workload: workload,
Goal: goal
));
}
return agents;
@@ -770,6 +787,241 @@ public sealed class OpenClawGatewayClient(HttpClient httpClient, IConfiguration
}
}
/// <summary>
/// Reads the agent's goal from workspace files (goals.md preferred, then AGENTS.md, SOUL.md).
/// Returns the first meaningful line or null.
/// </summary>
private async Task<string?> ReadAgentGoalAsync(string agentId)
{
try
{
var workspacePath = "/home/node/.openclaw/workspace-" + agentId;
// 1. Try goals.md first
var goalsPath = Path.Combine(workspacePath, "goals.md");
if (System.IO.File.Exists(goalsPath))
{
var content = await System.IO.File.ReadAllTextAsync(goalsPath);
foreach (var line in content.Split('\n'))
{
var trimmed = line.Trim();
if (!string.IsNullOrWhiteSpace(trimmed) && !trimmed.StartsWith('#') && !trimmed.StartsWith('-'))
return trimmed.Length > 120 ? trimmed[..117] + "..." : trimmed;
}
}
// 2. Try SOUL.md "## Rolle" section
var soulPath = Path.Combine(workspacePath, "SOUL.md");
if (System.IO.File.Exists(soulPath))
{
var soul = await System.IO.File.ReadAllTextAsync(soulPath);
using var reader = new StringReader(soul);
string? line;
var inRoleSection = false;
while ((line = reader.ReadLine()) is not null)
{
var trimmed = line.Trim();
if (trimmed.StartsWith("## ", StringComparison.OrdinalIgnoreCase))
{
if (inRoleSection) break;
if (trimmed.IndexOf("Rolle", StringComparison.OrdinalIgnoreCase) >= 0
|| trimmed.IndexOf("Role", StringComparison.OrdinalIgnoreCase) >= 0
|| trimmed.IndexOf("Oberstes", StringComparison.OrdinalIgnoreCase) >= 0)
{
inRoleSection = true;
}
continue;
}
if (inRoleSection && !string.IsNullOrWhiteSpace(trimmed) && !trimmed.StartsWith('-'))
{
return trimmed.Length > 120 ? trimmed[..117] + "..." : trimmed;
}
}
// 3. Look for "## Oberstes Prinzip" as second choice
inRoleSection = false;
reader = new StringReader(soul);
while ((line = reader.ReadLine()) is not null)
{
var trimmed = line.Trim();
if (trimmed.StartsWith("## ") && trimmed.IndexOf("Prinzip", StringComparison.OrdinalIgnoreCase) >= 0)
{
inRoleSection = true;
continue;
}
if (inRoleSection && !string.IsNullOrWhiteSpace(trimmed) && !trimmed.StartsWith('-') && !trimmed.StartsWith('#'))
{
return trimmed.Length > 120 ? trimmed[..117] + "..." : trimmed;
}
}
}
// 4. Try AGENTS.md first heading
var agentsPath = Path.Combine(workspacePath, "AGENTS.md");
if (System.IO.File.Exists(agentsPath))
{
var agentsContent = await System.IO.File.ReadAllTextAsync(agentsPath);
foreach (var line in agentsContent.Split('\n'))
{
var trimmed = line.Trim();
if (trimmed.StartsWith("# "))
return trimmed[2..].Trim();
}
}
}
catch
{
// Fallback to hardcoded
}
// Fallback: known goals
return agentId.ToLowerInvariant() switch
{
"iris" => "Mission Control — maximale Autonomie bei kontrolliertem Risiko",
"programmer" => "Nexus Dashboard & Dungeon System",
"reviewer" => "Zero critical findings before merge",
"architekt" => "Stabile Zero-Downtime-Deployments",
"executor" => "Sichere Host-Execution im Allowlist-Rahmen",
"researcher" => "Verifizierte, strukturierte Recherche-Ergebnisse",
_ => null
};
}
/// <summary>
/// Calculates agent progress (0-100) based on session activity.
/// Active agents with recent activity get higher scores.
/// Idle agents or inactive sessions get lower scores.
/// </summary>
private static int CalculateAgentProgress(string agentId, bool isActive, JsonNode? status)
{
if (!isActive)
return 0;
// Base progress from active status
var progress = 50;
// Boost for agents that have a current task
var hasTask = status?["currentTask"]?.GetValue<string>() is not null
|| status?["task"]?.GetValue<string>() is not null;
if (hasTask)
progress += 20;
// Check for last activity timestamp — more recent = higher progress
var lastActivity = status?["lastActivity"]?.GetValue<string>()
?? status?["lastMessage"]?.GetValue<string>();
if (lastActivity is not null && DateTimeOffset.TryParse(lastActivity, out var lastTs))
{
var minutesSinceActivity = (DateTimeOffset.UtcNow - lastTs).TotalMinutes;
if (minutesSinceActivity < 1)
progress += 25; // actively working
else if (minutesSinceActivity < 5)
progress += 15;
else if (minutesSinceActivity < 15)
progress += 10;
else
progress -= 10; // stale
}
else if (hasTask)
{
// Has task but no timestamp — assume actively working
progress += 15;
}
return Math.Clamp(progress, 0, 100);
}
/// <summary>
/// Calculates agent workload (0-100) based on queue items and active status.
/// More queued tasks or active sessions = higher workload.
/// </summary>
private static int CalculateAgentWorkload(string agentId, List<QueueItem> queueItems)
{
if (queueItems.Count == 0)
return 0;
// Calculate workload based on queue density per agent
var agentQueued = queueItems.Count(q =>
q.Name.Contains(agentId, StringComparison.OrdinalIgnoreCase)
|| q.Id.Contains(agentId, StringComparison.OrdinalIgnoreCase));
// Base workload from total queue pressure
var totalQueuePressure = Math.Min(queueItems.Count * 10, 60);
// Agent-specific queue items add extra weight
var agentPressure = agentQueued * 25;
return Math.Clamp(totalQueuePressure + agentPressure, 0, 100);
}
/// <summary>
/// Returns the list of available models by reading from the OpenClaw config,
/// with fallback to hardcoded list.
/// </summary>
public List<ModelOption> GetAvailableModels()
{
try
{
var configPath = configuration.GetValue<string>("AgentConfigPath")
?? "/home/node/.openclaw/openclaw.json";
if (!System.IO.File.Exists(configPath))
return GetDefaultModels();
var json = System.IO.File.ReadAllText(configPath);
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
// Read models from agents.defaults.models
if (!root.TryGetProperty("agents", out var agentsEl))
return GetDefaultModels();
if (!agentsEl.TryGetProperty("defaults", out var defaultsEl))
return GetDefaultModels();
if (!defaultsEl.TryGetProperty("models", out var modelsEl))
return GetDefaultModels();
var models = new List<ModelOption>();
foreach (var modelProp in modelsEl.EnumerateObject())
{
var modelId = modelProp.Name;
var modelObj = modelProp.Value;
var name = modelId; // fallback: use model ID as name
var provider = ExtractProvider(modelId);
// Check for alias in the model object
if (modelObj.TryGetProperty("alias", out var aliasEl))
{
var alias = aliasEl.GetString();
if (!string.IsNullOrWhiteSpace(alias))
name = alias;
}
models.Add(new ModelOption(modelId, name, provider));
}
return models.Count > 0 ? models : GetDefaultModels();
}
catch
{
return GetDefaultModels();
}
}
private static List<ModelOption> GetDefaultModels()
=> new()
{
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"),
new ModelOption("openai/gpt-5.5", "GPT-5.5", "openai")
};
private static string ExtractProvider(string modelId)
{
var slash = modelId.IndexOf('/');
return slash > 0 ? modelId[..slash] : "unknown";
}
private static string DeriveRole(string agentId) => agentId.ToLowerInvariant() switch
{
"iris" => "Chief of Staff",
+7 -3
View File
@@ -77,6 +77,9 @@ interface DashboardAgentInfo {
currentTask: string
description?: string
tags?: string[]
progress?: number
workload?: number
goal?: string | null
}
interface DashboardOperationEntry {
@@ -217,9 +220,10 @@ function enrichAgent(api: DashboardAgentInfo): AgentNodeData {
color: catalog.color ?? '#6b7385',
icon: catalog.icon ?? 'bot',
hero: catalog.hero ?? false,
goal: catalog.goal ?? 'No goal set',
progress: catalog.progress ?? 0,
workload: catalog.workload ?? 0,
// Use API-provided values with catalog fallback for metrics
goal: api.goal ?? catalog.goal ?? 'No goal set',
progress: api.progress ?? catalog.progress ?? 0,
workload: api.workload ?? catalog.workload ?? 0,
runtimeSeconds: 0,
workingFeed: catalog.workingFeed ?? [],
thinkingStream: catalog.thinkingStream ?? [],