From 11acfde0812957549a7b79157e71e12df4b2c0f8 Mon Sep 17 00:00:00 2001 From: Meleki Date: Sat, 7 Mar 2026 19:40:26 +0100 Subject: [PATCH] Dashboard-Performance: API-Endpoints konsolidieren MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - GET /api/sessions: Git-Status inline integriert (parallel pro Session) - Neue Hilfsmethoden GetGitStatusSummaryAsync (Counts) und GetGitStatusDetailedAsync (Dateinamen, für Einzel-Endpoint) - GetSessionsWithStats gibt typisierte SessionWithStats zurück - Neue Models: SessionWithStats, GitStatusSummary - GET /api/agents: Skills inline pro Agent integriert - SkillService als Parameter in HandleList - Skills als Array mit slug/name/description Frontend: - Dashboard: 1 paralleler Promise.all statt sequentieller Requests - Vorher: agents → sessions → N×git-status → M×agent-skills → cronjobs → shares - Nachher: Promise.all([agents, sessions, cronjobs, shares]) (4 Requests) - Git-Status direkt aus Session-Response, Skills aus Agent-Response - Session-Page: Agent-Skills aus inline-Daten statt N×apiGetAgentSkills Reduziert Dashboard-Requests von ~4+N+M auf 4 (fix). --- ARCHITECTURE_QUICK.md | 4 +- Endpoints/AgentEndpoint.cs | 46 ++++++- Endpoints/SessionEndpoint.cs | 201 ++++++++++++++++++++++++------- Models/SessionRecord.cs | 18 +++ Services/DatabaseService.cs | 25 +--- wwwroot/js/pages/dashboard.js | 84 ++++++------- wwwroot/js/pages/session-page.js | 6 +- 7 files changed, 265 insertions(+), 119 deletions(-) diff --git a/ARCHITECTURE_QUICK.md b/ARCHITECTURE_QUICK.md index c1f9a5e..2647327 100644 --- a/ARCHITECTURE_QUICK.md +++ b/ARCHITECTURE_QUICK.md @@ -74,8 +74,8 @@ User 1──* Agent 1──* Session 1──* Chain 1──* Message 1──* At | Auth | `/api/auth/` | `POST login`, `POST logout`, `GET me`, `GET autologin` | | Chat | `/api/chat` | `POST` → SSE Stream, `POST stop` | | History | `/api/history` | GET history, poll, archive, marks, delete | -| Sessions | `/api/sessions/` | CRUD + system-prompt, exec, git-status | -| Agents | `/api/agents/` | CRUD | +| Sessions | `/api/sessions/` | CRUD + system-prompt, exec, git-status (Liste inkl. inline Git-Status) | +| Agents | `/api/agents/` | CRUD (Liste inkl. inline Skills) | | Admin | `/api/admin/` | User CRUD, impersonate, login-link (Admin-only) | | Workspace | `/api/workspace/` | tree, file read/write, mkdir, rmdir, browse, agent-files | | Telegram | `/api/telegram/` | webhook (unauth), config, registration | diff --git a/Endpoints/AgentEndpoint.cs b/Endpoints/AgentEndpoint.cs index aa8d17a..95e779f 100644 --- a/Endpoints/AgentEndpoint.cs +++ b/Endpoints/AgentEndpoint.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; +using System.Linq; using System.Text.Json; using System.Threading.Tasks; using Gott.Extensions; @@ -20,11 +22,51 @@ public static class AgentEndpoint app.MapDelete("/api/agents/{agentId}", HandleDelete); } - private static async Task HandleList(HttpContext ctx, DatabaseService db) + private static async Task HandleList(HttpContext ctx, DatabaseService db, SkillService skillService) { var userId = ctx.GetUserId(); var agents = await db.GetAgents(userId); - await ctx.Response.WriteAsJsonAsync(agents); + var allSkills = skillService.GetAllSkills(); + var skillMap = allSkills.ToDictionary(s => s.Slug); + + var results = new List(); + foreach (var a in agents) + { + var agentSkillSlugs = await db.GetAgentSkills(a.AgentId); + var skills = agentSkillSlugs + .Where(slug => skillMap.ContainsKey(slug)) + .Select(slug => new { slug, name = skillMap[slug].Name, description = skillMap[slug].Description }) + .ToList(); + + results.Add(new + { + a.AgentId, + a.Name, + a.Avatar, + a.Description, + a.Model, + a.ToolChoice, + a.CustomCommands, + a.AutoCommit, + a.UseSoul, + a.UseIdentity, + a.UseUserInfo, + a.UseMemory, + a.UseWebSearch, + a.WebSearchVersion, + a.AllowUserChoice, + a.TaskInstruction, + a.AvatarScale, + a.AvatarOffsetX, + a.AvatarOffsetY, + a.IsDeleted, + a.CreatedAt, + a.UpdatedAt, + Skills = skills + }); + } + + await ctx.Response.WriteAsJsonAsync(results); } private static async Task HandleGet(HttpContext ctx, DatabaseService db) diff --git a/Endpoints/SessionEndpoint.cs b/Endpoints/SessionEndpoint.cs index 8b2f2d3..4366087 100644 --- a/Endpoints/SessionEndpoint.cs +++ b/Endpoints/SessionEndpoint.cs @@ -3,8 +3,10 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using Gott.Extensions; +using Gott.Models; using Gott.Services; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; @@ -31,8 +33,64 @@ public static class SessionEndpoint private static async Task HandleList(HttpContext ctx, DatabaseService db) { var userId = ctx.GetUserId(); - var sessions = await db.GetSessionsWithStats(userId); - await ctx.Response.WriteAsJsonAsync(sessions); + var user = await db.GetUser(userId); + var sessionList = await db.GetSessionsWithStats(userId); + + // Git-Status parallel für alle Sessions mit Workspace laden + var gitTasks = new Dictionary>(); + for (int i = 0; i < sessionList.Count; i++) + { + var resolvedWs = WorkspaceHelper.ResolveWorkspace(sessionList[i].Session, user); + if (!string.IsNullOrEmpty(resolvedWs)) + gitTasks[i] = GetGitStatusSummaryAsync(resolvedWs); + } + + if (gitTasks.Count > 0) + await Task.WhenAll(gitTasks.Values); + + // Ergebnisse zusammenführen + var results = new List(); + for (int i = 0; i < sessionList.Count; i++) + { + var sw = sessionList[i]; + var s = sw.Session; + object gitStatus = null; + if (gitTasks.TryGetValue(i, out var task) && task.IsCompletedSuccessfully && task.Result != null) + { + var gs = task.Result; + if (gs.Add > 0 || gs.Edit > 0 || gs.Delete > 0 || gs.Outgoing > 0) + gitStatus = new { add = gs.Add, edit = gs.Edit, delete = gs.Delete, outgoing = gs.Outgoing }; + } + results.Add(new + { + s.SessionId, + s.Name, + s.Description, + s.Image, + s.AgentId, + s.Workspace, + s.Context, + s.CustomCommands, + s.AvatarScale, + s.AvatarOffsetX, + s.AvatarOffsetY, + s.ReviewAgentId, + s.MaxReviewIterations, + s.FinalizerAgentId, + s.PreparerAgentId, + s.IsDeleted, + s.CreatedAt, + s.UpdatedAt, + sw.MessageCount, + sw.ArchiveMessageCount, + sw.TotalMessageCount, + sw.TelegramConfigured, + sw.TelegramLinked, + GitStatus = gitStatus + }); + } + + await ctx.Response.WriteAsJsonAsync(results); } private static async Task HandleGet(HttpContext ctx, DatabaseService db) @@ -396,6 +454,23 @@ public static class SessionEndpoint return; } + var result = await GetGitStatusDetailedAsync(workingDir); + if (result == null) + { + ctx.Response.StatusCode = 500; + await ctx.Response.WriteAsJsonAsync(new { error = "Git-Status konnte nicht ermittelt werden" }); + return; + } + + await ctx.Response.WriteAsJsonAsync(result); + } + + /// + /// Ermittelt eine Git-Status-Zusammenfassung (nur Counts) für die Dashboard-Ansicht. + /// Gibt null zurück bei Fehlern (kein Git-Repo, kein git installiert etc.). + /// + private static async Task GetGitStatusSummaryAsync(string workingDir) + { try { var psi = new ProcessStartInfo @@ -410,24 +485,85 @@ public static class SessionEndpoint }; using var proc = Process.Start(psi); - if (proc == null) - { - ctx.Response.StatusCode = 500; - await ctx.Response.WriteAsJsonAsync(new { error = "Git-Prozess konnte nicht gestartet werden" }); - return; - } + if (proc == null) return null; var stdout = await proc.StandardOutput.ReadToEndAsync(); - var stderr = await proc.StandardError.ReadToEndAsync(); await proc.WaitForExitAsync(); + if (proc.ExitCode != 0) return null; - if (proc.ExitCode != 0) + int addCount = 0, editCount = 0, deleteCount = 0; + var lines = stdout.Split('\n', StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) { - ctx.Response.StatusCode = 500; - await ctx.Response.WriteAsJsonAsync(new { error = stderr.Trim() }); - return; + if (line.Length < 4) continue; + var x = line[0]; + var y = line[1]; + + if (x == '?' && y == '?') addCount++; + else if (x == 'D' || y == 'D') deleteCount++; + else if (x == 'A') addCount++; + else editCount++; } + // Outgoing commits + int outgoing = 0; + try + { + var outPsi = new ProcessStartInfo + { + FileName = "git", + Arguments = "rev-list --count @{u}..HEAD", + WorkingDirectory = workingDir, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var outProc = Process.Start(outPsi); + if (outProc != null) + { + var outResult = (await outProc.StandardOutput.ReadToEndAsync()).Trim(); + await outProc.WaitForExitAsync(); + if (outProc.ExitCode == 0 && int.TryParse(outResult, out var c)) + outgoing = c; + } + } + catch { } + + return new GitStatusSummary { Add = addCount, Edit = editCount, Delete = deleteCount, Outgoing = outgoing }; + } + catch + { + return null; + } + } + + /// + /// Ermittelt den detaillierten Git-Status (mit Dateinamen) für den Einzel-Endpoint. + /// + private static async Task GetGitStatusDetailedAsync(string workingDir) + { + try + { + var psi = new ProcessStartInfo + { + FileName = "git", + Arguments = "status --porcelain", + WorkingDirectory = workingDir, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var proc = Process.Start(psi); + if (proc == null) return null; + + var stdout = await proc.StandardOutput.ReadToEndAsync(); + await proc.WaitForExitAsync(); + if (proc.ExitCode != 0) return null; + var add = new List(); var edit = new List(); var delete = new List(); @@ -436,35 +572,20 @@ public static class SessionEndpoint foreach (var line in lines) { if (line.Length < 4) continue; - - var x = line[0]; // index status - var y = line[1]; // working tree status + var x = line[0]; + var y = line[1]; var file = line.Substring(3); - // Bei Renames: "R old -> new" => nur den neuen Namen nehmen var arrowIdx = file.IndexOf(" -> ", StringComparison.Ordinal); if (arrowIdx >= 0) file = file.Substring(arrowIdx + 4); - if (x == '?' && y == '?') - { - add.Add(file); - } - else if (x == 'D' || y == 'D') - { - delete.Add(file); - } - else if (x == 'A') - { - add.Add(file); - } - else - { - edit.Add(file); - } + if (x == '?' && y == '?') add.Add(file); + else if (x == 'D' || y == 'D') delete.Add(file); + else if (x == 'A') add.Add(file); + else edit.Add(file); } - // Outgoing commits zählen (nur wenn Remote existiert) int outgoing = 0; try { @@ -488,17 +609,13 @@ public static class SessionEndpoint outgoing = count; } } - catch - { - // Kein Remote oder kein Upstream – outgoing bleibt 0 - } + catch { } - await ctx.Response.WriteAsJsonAsync(new { add, edit, delete, outgoing }); + return new { add, edit, delete, outgoing }; } - catch (Exception ex) + catch { - ctx.Response.StatusCode = 500; - await ctx.Response.WriteAsJsonAsync(new { error = ex.Message }); + return null; } } } diff --git a/Models/SessionRecord.cs b/Models/SessionRecord.cs index 15d3f05..d9884e8 100644 --- a/Models/SessionRecord.cs +++ b/Models/SessionRecord.cs @@ -27,3 +27,21 @@ public class SessionRecord public string CreatedAt { get; set; } public string UpdatedAt { get; set; } } + +public class SessionWithStats +{ + public SessionRecord Session { get; set; } + public int MessageCount { get; set; } + public int ArchiveMessageCount { get; set; } + public int TotalMessageCount { get; set; } + public bool TelegramConfigured { get; set; } + public bool TelegramLinked { get; set; } +} + +public class GitStatusSummary +{ + public int Add { get; set; } + public int Edit { get; set; } + public int Delete { get; set; } + public int Outgoing { get; set; } +} diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs index e08e291..400f675 100644 --- a/Services/DatabaseService.cs +++ b/Services/DatabaseService.cs @@ -419,13 +419,13 @@ public class DatabaseService "SELECT * FROM sessions WHERE user_id = @userId AND is_deleted = 0 ORDER BY updated_at DESC", new { userId }); } - public async Task> GetSessionsWithStats(string userId) + public async Task> GetSessionsWithStats(string userId) { using var conn = CreateConnection(); var sessions = await conn.QueryAsync( "SELECT * FROM sessions WHERE user_id = @userId AND is_deleted = 0 ORDER BY updated_at DESC", new { userId }); - var results = new List(); + var results = new List(); foreach (var s in sessions) { var activeCount = await conn.ExecuteScalarAsync(@" @@ -440,26 +440,9 @@ public class DatabaseService WHERE c.session_id = @sessionId AND m.type = 'text'", new { sessionId = s.SessionId }); - results.Add(new + results.Add(new SessionWithStats { - s.SessionId, - s.Name, - s.Description, - s.Image, - s.AgentId, - s.Workspace, - s.Context, - s.CustomCommands, - s.AvatarScale, - s.AvatarOffsetX, - s.AvatarOffsetY, - s.ReviewAgentId, - s.MaxReviewIterations, - s.FinalizerAgentId, - s.PreparerAgentId, - s.IsDeleted, - s.CreatedAt, - s.UpdatedAt, + Session = s, MessageCount = activeCount, ArchiveMessageCount = totalCount - activeCount, TotalMessageCount = totalCount, diff --git a/wwwroot/js/pages/dashboard.js b/wwwroot/js/pages/dashboard.js index 39ef7a8..5c4a753 100644 --- a/wwwroot/js/pages/dashboard.js +++ b/wwwroot/js/pages/dashboard.js @@ -35,12 +35,13 @@ registerPage('dashboard', { // Telegram-Config parallel pro Session laden (sessions werden gleich geladen) - const agents = await apiGetAgents(); + const [agents, sessions, cronjobs, shares] = await Promise.all([ + apiGetAgents(), apiGetSessions(), apiGetCronjobs(), apiListShares() + ]); const agentMap = {}; for (const a of agents) agentMap[a.agentId] = a; // Sessions - const sessions = await apiGetSessions(); const grid = document.getElementById('sessionCards'); grid.innerHTML = ''; @@ -48,6 +49,37 @@ registerPage('dashboard', { const agent = s.agentId ? agentMap[s.agentId] : null; const card = buildSessionCard(s, agent); + // Git-Status aus der Session-Response rendern (inline vom Backend) + if (s.gitStatus) { + const gs = s.gitStatus; + const metaEl = card.querySelector('.card-meta'); + if (metaEl) { + const parts = []; + if (gs.add > 0) { + const tip = `${gs.add} ${gs.add === 1 ? 'Datei' : 'Dateien'} hinzugefügt`; + parts.push(`+${gs.add}`); + } + if (gs.edit > 0) { + const tip = `${gs.edit} ${gs.edit === 1 ? 'Datei' : 'Dateien'} geändert`; + parts.push(`~${gs.edit}`); + } + if (gs.delete > 0) { + const tip = `${gs.delete} ${gs.delete === 1 ? 'Datei' : 'Dateien'} gelöscht`; + parts.push(`-${gs.delete}`); + } + if (gs.outgoing > 0) { + const tip = `${gs.outgoing} ${gs.outgoing === 1 ? 'Commit muss' : 'Commits müssen'} noch gepusht werden`; + parts.push(` ${gs.outgoing}`); + } + if (parts.length) { + const gitEl = document.createElement('span'); + gitEl.className = 'card-meta-item card-git-status'; + gitEl.innerHTML = parts.join(' '); + metaEl.appendChild(gitEl); + } + } + } + // Menü zum title-row hinzufügen const titleRow = card.querySelector('.card-title-row'); titleRow.insertAdjacentHTML('beforeend', `
@@ -105,44 +137,6 @@ registerPage('dashboard', { navigateTo('session', { id: s.sessionId }); }); grid.appendChild(card); - - // Git-Status asynchron laden - if (s.workspace) { - apiGitStatus(s.sessionId).then(status => { - if (!status || status.error) return; - const addCount = (status.add || []).length; - const editCount = (status.edit || []).length; - const deleteCount = (status.delete || []).length; - const outgoing = status.outgoing || 0; - if (addCount + editCount + deleteCount + outgoing === 0) return; - - const metaEl = card.querySelector('.card-meta'); - if (!metaEl) return; - - const parts = []; - if (addCount > 0) { - const tip = `${addCount} ${addCount === 1 ? 'Datei' : 'Dateien'} hinzugefügt`; - parts.push(`+${addCount}`); - } - if (editCount > 0) { - const tip = `${editCount} ${editCount === 1 ? 'Datei' : 'Dateien'} geändert`; - parts.push(`~${editCount}`); - } - if (deleteCount > 0) { - const tip = `${deleteCount} ${deleteCount === 1 ? 'Datei' : 'Dateien'} gelöscht`; - parts.push(`-${deleteCount}`); - } - if (outgoing > 0) { - const tip = `${outgoing} ${outgoing === 1 ? 'Commit muss' : 'Commits müssen'} noch gepusht werden`; - parts.push(` ${outgoing}`); - } - - const gitEl = document.createElement('span'); - gitEl.className = 'card-meta-item card-git-status'; - gitEl.innerHTML = parts.join(' '); - metaEl.appendChild(gitEl); - }); - } } @@ -154,14 +148,8 @@ registerPage('dashboard', { const agentGrid = document.getElementById('agentCards'); agentGrid.innerHTML = ''; - // Skills pro Agent parallel laden - const agentSkillsMap = {}; - await Promise.all(agents.map(async a => { - agentSkillsMap[a.agentId] = await apiGetAgentSkills(a.agentId); - })); - for (const a of agents) { - const skills = agentSkillsMap[a.agentId] || []; + const skills = a.skills || []; const card = buildAgentCard(a, skills); // Menü zum title-row hinzufügen @@ -213,7 +201,6 @@ registerPage('dashboard', { document.getElementById('newAgentBtn').addEventListener('click', () => navigateTo('agent')); // Cronjobs - const cronjobs = await apiGetCronjobs(); const cronjobGrid = document.getElementById('cronjobCards'); cronjobGrid.innerHTML = ''; @@ -303,7 +290,6 @@ registerPage('dashboard', { document.getElementById('newCronjobBtn').addEventListener('click', () => navigateTo('cronjob')); // Shared Links - const shares = await apiListShares(); const sharedSection = document.getElementById('sharedLinksSection'); const sharedGrid = document.getElementById('sharedLinksCards'); if (shares.length > 0) { diff --git a/wwwroot/js/pages/session-page.js b/wwwroot/js/pages/session-page.js index a7615a4..86ad8af 100644 --- a/wwwroot/js/pages/session-page.js +++ b/wwwroot/js/pages/session-page.js @@ -89,9 +89,9 @@ registerPage('session', { for (const a of agents) agentMap[a.agentId] = a; const agentSkillsMap = {}; - await Promise.all(agents.map(async a => { - agentSkillsMap[a.agentId] = await apiGetAgentSkills(a.agentId); - })); + for (const a of agents) { + agentSkillsMap[a.agentId] = a.skills || []; + } function renderAgentInCard(cardEl, agent, emptyIcon, emptyText) { cardEl.innerHTML = '';