From ac150af4c5b62541d2244850c2d3abf93afbf907 Mon Sep 17 00:00:00 2001 From: Meleki Date: Sat, 28 Feb 2026 23:23:52 +0100 Subject: [PATCH] Git-Status Endpoint mit Outgoing-Commits und Dashboard-Badges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /api/sessions/{id}/git-status: Parsed git status --porcelain in add/edit/delete Listen - Outgoing-Commits: Zählt unpushed Commits via git rev-list --count @{u}..HEAD - Dashboard: Zeigt Git-Badges asynchron pro Session (+N, ~N, -N, ↑N) - CSS: Farbige Badges (grün=add, orange=edit, rot=delete, blau=outgoing) --- Endpoints/SessionEndpoint.cs | 129 ++++++++++++++++++++++++++++++++++ wwwroot/css/dashboard.css | 34 +++++++++ wwwroot/js/api.js | 6 ++ wwwroot/js/pages/dashboard.js | 26 +++++++ 4 files changed, 195 insertions(+) diff --git a/Endpoints/SessionEndpoint.cs b/Endpoints/SessionEndpoint.cs index dff50be..34dfafc 100644 --- a/Endpoints/SessionEndpoint.cs +++ b/Endpoints/SessionEndpoint.cs @@ -23,6 +23,7 @@ public static class SessionEndpoint app.MapGet("/api/sessions/{sessionId}/system-prompt", HandleSystemPrompt); app.MapGet("/api/sessions/{sessionId}/streaming", HandleStreaming); app.MapPost("/api/sessions/{sessionId}/exec", HandleExec); + app.MapGet("/api/sessions/{sessionId}/git-status", HandleGitStatus); } private static async Task HandleList(HttpContext ctx, DatabaseService db) @@ -264,5 +265,133 @@ public static class SessionEndpoint } } + private static async Task HandleGitStatus(HttpContext ctx, DatabaseService db) + { + var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString(); + if (string.IsNullOrEmpty(sessionId)) + { + ctx.Response.StatusCode = 400; + return; + } + var session = await db.GetSession(sessionId); + if (session == null) + { + ctx.Response.StatusCode = 404; + return; + } + + var workingDir = !string.IsNullOrWhiteSpace(session.Workspace) ? session.Workspace : null; + if (string.IsNullOrEmpty(workingDir)) + { + ctx.Response.StatusCode = 400; + await ctx.Response.WriteAsJsonAsync(new { success = false, error = "Kein Workspace konfiguriert" }); + return; + } + + 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) + { + ctx.Response.StatusCode = 500; + await ctx.Response.WriteAsJsonAsync(new { success = false, error = "Git-Prozess konnte nicht gestartet werden" }); + return; + } + + var stdout = await proc.StandardOutput.ReadToEndAsync(); + var stderr = await proc.StandardError.ReadToEndAsync(); + await proc.WaitForExitAsync(); + + if (proc.ExitCode != 0) + { + ctx.Response.StatusCode = 500; + await ctx.Response.WriteAsJsonAsync(new { success = false, error = stderr.Trim() }); + return; + } + + var add = new List(); + var edit = new List(); + var delete = new List(); + + var lines = stdout.Split('\n', StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + if (line.Length < 4) continue; + + var x = line[0]; // index status + var y = line[1]; // working tree status + 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); + } + } + + // Outgoing commits zählen (nur wenn Remote existiert) + 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 count)) + outgoing = count; + } + } + catch + { + // Kein Remote oder kein Upstream – outgoing bleibt 0 + } + + await ctx.Response.WriteAsJsonAsync(new { success = true, add, edit, delete, outgoing }); + } + catch (Exception ex) + { + ctx.Response.StatusCode = 500; + await ctx.Response.WriteAsJsonAsync(new { success = false, error = ex.Message }); + } + } } diff --git a/wwwroot/css/dashboard.css b/wwwroot/css/dashboard.css index 48adf01..0636e28 100644 --- a/wwwroot/css/dashboard.css +++ b/wwwroot/css/dashboard.css @@ -114,3 +114,37 @@ overflow: hidden; text-overflow: ellipsis; } + +.card-git-status { + display: flex; + align-items: center; + gap: var(--badge-spacing); + opacity: 1; +} + +.git-badge { + padding: var(--badge-padding); + font-family: var(--mono); + font-size: var(--mono-size); + border-radius: var(--border-radius); +} + +.git-add { + background: rgba(68, 170, 17, 0.15); + color: var(--secondary); +} + +.git-edit { + background: rgba(255, 170, 0, 0.15); + color: var(--warning); +} + +.git-delete { + background: rgba(255, 102, 102, 0.15); + color: var(--dangerous); +} + +.git-outgoing { + background: rgba(100, 149, 237, 0.15); + color: var(--primary); +} diff --git a/wwwroot/js/api.js b/wwwroot/js/api.js index 7d0ddf5..a0cb08b 100644 --- a/wwwroot/js/api.js +++ b/wwwroot/js/api.js @@ -131,6 +131,12 @@ async function apiGetChain(chainId) { return resp.json(); } +async function apiGitStatus(sessionId) { + const resp = await fetch(`/api/sessions/${sessionId}/git-status`); + if (!resp.ok) return null; + return resp.json(); +} + async function apiBackupDb() { const resp = await fetch('/api/backup', { method: 'POST' }); if (!resp.ok) throw new Error('Backup fehlgeschlagen'); diff --git a/wwwroot/js/pages/dashboard.js b/wwwroot/js/pages/dashboard.js index e495cbf..6f0b1b0 100644 --- a/wwwroot/js/pages/dashboard.js +++ b/wwwroot/js/pages/dashboard.js @@ -36,6 +36,32 @@ registerPage('dashboard', { `; card.addEventListener('click', () => navigateTo('session', { id: s.sessionId })); grid.appendChild(card); + + // Git-Status asynchron laden + if (s.workspace) { + apiGitStatus(s.sessionId).then(status => { + if (!status || !status.success) 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) parts.push(`+${addCount}`); + if (editCount > 0) parts.push(`~${editCount}`); + if (deleteCount > 0) parts.push(`-${deleteCount}`); + if (outgoing > 0) parts.push(` ${outgoing}`); + + const gitEl = document.createElement('span'); + gitEl.className = 'card-meta-item card-git-status'; + gitEl.innerHTML = ` ${parts.join(' ')}`; + metaEl.appendChild(gitEl); + }); + } } // Neue Session Placeholder