Git-Status Endpoint mit Outgoing-Commits und Dashboard-Badges
- 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)
This commit is contained in:
1 parent
166465fd54
commit
ac150af4c5
4 files changed
+195
No files matched your search
@@ -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<string>();
|
||||
var edit = new List<string>();
|
||||
var delete = new List<string>();
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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(`<span class="git-badge git-add">+${addCount}</span>`);
|
||||
if (editCount > 0) parts.push(`<span class="git-badge git-edit">~${editCount}</span>`);
|
||||
if (deleteCount > 0) parts.push(`<span class="git-badge git-delete">-${deleteCount}</span>`);
|
||||
if (outgoing > 0) parts.push(`<span class="git-badge git-outgoing"><i class="fa fa-arrow-up"></i> ${outgoing}</span>`);
|
||||
|
||||
const gitEl = document.createElement('span');
|
||||
gitEl.className = 'card-meta-item card-git-status';
|
||||
gitEl.innerHTML = `<i class="fa fa-code-fork"></i> ${parts.join(' ')}`;
|
||||
metaEl.appendChild(gitEl);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Neue Session Placeholder
|
||||
|
||||
Reference in new issue
Block a user