Dashboard-Performance: API-Endpoints konsolidieren

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).
This commit is contained in:
Meleki committed 2026-03-07 19:40:26 +01:00
1 parent 0dcc05ebf9
commit 11acfde081
7 files changed
+265 -119

No files matched your search

+2 -2
View File
@@ -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 |
+44 -2
View File
@@ -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<object>();
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)
+159 -42
View File
@@ -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<int, Task<GitStatusSummary>>();
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<object>();
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);
}
/// <summary>
/// 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.).
/// </summary>
private static async Task<GitStatusSummary> 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;
}
}
/// <summary>
/// Ermittelt den detaillierten Git-Status (mit Dateinamen) für den Einzel-Endpoint.
/// </summary>
private static async Task<object> 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<string>();
var edit = new List<string>();
var delete = new List<string>();
@@ -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;
}
}
}
+18
View File
@@ -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; }
}
+4 -21
View File
@@ -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<IEnumerable<object>> GetSessionsWithStats(string userId)
public async Task<List<SessionWithStats>> GetSessionsWithStats(string userId)
{
using var conn = CreateConnection();
var sessions = await conn.QueryAsync<SessionRecord>(
"SELECT * FROM sessions WHERE user_id = @userId AND is_deleted = 0 ORDER BY updated_at DESC", new { userId });
var results = new List<object>();
var results = new List<SessionWithStats>();
foreach (var s in sessions)
{
var activeCount = await conn.ExecuteScalarAsync<int>(@"
@@ -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,
+35 -49
View File
@@ -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(`<span class="git-badge git-add" title="${esc(tip)}">+${gs.add}</span>`);
}
if (gs.edit > 0) {
const tip = `${gs.edit} ${gs.edit === 1 ? 'Datei' : 'Dateien'} geändert`;
parts.push(`<span class="git-badge git-edit" title="${esc(tip)}">~${gs.edit}</span>`);
}
if (gs.delete > 0) {
const tip = `${gs.delete} ${gs.delete === 1 ? 'Datei' : 'Dateien'} gelöscht`;
parts.push(`<span class="git-badge git-delete" title="${esc(tip)}">-${gs.delete}</span>`);
}
if (gs.outgoing > 0) {
const tip = `${gs.outgoing} ${gs.outgoing === 1 ? 'Commit muss' : 'Commits müssen'} noch gepusht werden`;
parts.push(`<span class="git-badge git-outgoing" title="${esc(tip)}"><i class="fa fa-arrow-up"></i> ${gs.outgoing}</span>`);
}
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', `<div class="card-menu">
@@ -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(`<span class="git-badge git-add" title="${esc(tip)}">+${addCount}</span>`);
}
if (editCount > 0) {
const tip = `${editCount} ${editCount === 1 ? 'Datei' : 'Dateien'} geändert`;
parts.push(`<span class="git-badge git-edit" title="${esc(tip)}">~${editCount}</span>`);
}
if (deleteCount > 0) {
const tip = `${deleteCount} ${deleteCount === 1 ? 'Datei' : 'Dateien'} gelöscht`;
parts.push(`<span class="git-badge git-delete" title="${esc(tip)}">-${deleteCount}</span>`);
}
if (outgoing > 0) {
const tip = `${outgoing} ${outgoing === 1 ? 'Commit muss' : 'Commits müssen'} noch gepusht werden`;
parts.push(`<span class="git-badge git-outgoing" title="${esc(tip)}"><i class="fa fa-arrow-up"></i> ${outgoing}</span>`);
}
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) {
+3 -3
View File
@@ -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 = '';