using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; 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; using Microsoft.AspNetCore.Routing; namespace Gott.Endpoints; public static class SessionEndpoint { public static void MapSessionEndpoints(this IEndpointRouteBuilder app) { app.MapGet("/api/sessions", HandleList); app.MapGet("/api/sessions/{sessionId}", HandleGet); app.MapPost("/api/sessions", HandleCreate); app.MapPut("/api/sessions/{sessionId}", HandleUpdate); app.MapDelete("/api/sessions/{sessionId}", HandleDelete); app.MapGet("/api/sessions/{sessionId}/system-prompt", HandleSystemPrompt); app.MapGet("/api/sessions/{sessionId}/system-prompt-blocks", HandleSystemPromptBlocks); app.MapGet("/api/sessions/{sessionId}/payload-preview", HandlePayloadPreview); app.MapGet("/api/sessions/{sessionId}/streaming", HandleStreaming); app.MapGet("/api/sessions/{sessionId}/workflow-status", HandleWorkflowStatus); app.MapPost("/api/sessions/{sessionId}/exec", HandleExec); app.MapGet("/api/sessions/{sessionId}/git-status", HandleGitStatus); } private static async Task HandleList(HttpContext ctx, DatabaseService db) { var userId = ctx.GetUserId(); 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, Image = s.ImageSmall ?? 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.UnityMcpUrl, s.UnityMcpToken, 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) { var userId = ctx.GetUserId(); 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 || session.UserId != userId) { ctx.Response.StatusCode = 404; return; } await ctx.Response.WriteAsJsonAsync(session); } private static async Task HandleCreate(HttpContext ctx, DatabaseService db, ImageService imageService) { var userId = ctx.GetUserId(); var body = await JsonSerializer.DeserializeAsync(ctx.Request.Body); var name = body.TryGetProperty("name", out var n) ? n.GetString() : "Neue Session"; var description = body.TryGetProperty("description", out var d) ? d.GetString() : null; var imageRaw = body.TryGetProperty("image", out var img) ? img.GetString() : null; var image = imageService.ResizeDataUrl(imageRaw); var imageSmall = imageService.ResizeDataUrlSmall(imageRaw); var agentId = body.TryGetProperty("agentId", out var a) ? a.GetString() : null; var workspaceRaw = body.TryGetProperty("workspace", out var w) ? w.GetString() : null; // Relativen Workspace validieren (Path-Traversal verhindern) var workspace = !string.IsNullOrWhiteSpace(workspaceRaw) && !System.IO.Path.IsPathRooted(workspaceRaw) ? WorkspaceHelper.ValidateRelativeWorkspace(workspaceRaw) : workspaceRaw; if (!string.IsNullOrWhiteSpace(workspaceRaw) && workspace == null) { ctx.Response.StatusCode = 400; await ctx.Response.WriteAsJsonAsync(new { error = "Ungültiger Workspace-Pfad (.. nicht erlaubt)" }); return; } var context = body.TryGetProperty("context", out var ctxEl) ? ctxEl.GetString() : null; var customCommands = body.TryGetProperty("customCommands", out var cc) && cc.ValueKind != JsonValueKind.Null ? cc.GetRawText() : null; var avatarScale = body.TryGetProperty("avatarScale", out var asc) ? asc.GetDouble() : 1.0; var avatarOffsetX = body.TryGetProperty("avatarOffsetX", out var aox) ? aox.GetDouble() : 0; var avatarOffsetY = body.TryGetProperty("avatarOffsetY", out var aoy) ? aoy.GetDouble() : 0; var reviewAgentId = body.TryGetProperty("reviewAgentId", out var ra) ? ra.GetString() : null; var maxReviewIterations = body.TryGetProperty("maxReviewIterations", out var mri) ? mri.GetInt32() : 3; var finalizerAgentId = body.TryGetProperty("finalizerAgentId", out var fa) ? fa.GetString() : null; var preparerAgentId = body.TryGetProperty("preparerAgentId", out var pra) ? pra.GetString() : null; var multiModelMode = body.TryGetProperty("multiModelMode", out var mmm) ? mmm.GetString() : null; var panelAgentIds = body.TryGetProperty("panelAgentIds", out var pai) && pai.ValueKind != JsonValueKind.Null ? pai.GetString() : null; var moaLayers = body.TryGetProperty("moaLayers", out var mol) && mol.ValueKind == JsonValueKind.Number ? mol.GetInt32() : 2; var session = await db.CreateSession(userId, name, description, image, imageSmall, agentId, workspace, context, customCommands, avatarScale, avatarOffsetX, avatarOffsetY, reviewAgentId, maxReviewIterations, finalizerAgentId, preparerAgentId, multiModelMode, panelAgentIds, moaLayers); await ctx.Response.WriteAsJsonAsync(session); } private static async Task HandleUpdate(HttpContext ctx, DatabaseService db, ImageService imageService) { var userId = ctx.GetUserId(); var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString(); if (string.IsNullOrEmpty(sessionId)) { ctx.Response.StatusCode = 400; return; } var existing = await db.GetSession(sessionId); if (existing == null || existing.UserId != userId) { ctx.Response.StatusCode = 404; return; } var body = await JsonSerializer.DeserializeAsync(ctx.Request.Body); // Workspace-Pfad validieren (Path-Traversal verhindern) if (body.TryGetProperty("workspace", out var wsEl)) { var wsRaw = wsEl.GetString(); if (!string.IsNullOrWhiteSpace(wsRaw) && !System.IO.Path.IsPathRooted(wsRaw)) { var validated = WorkspaceHelper.ValidateRelativeWorkspace(wsRaw); if (validated == null) { ctx.Response.StatusCode = 400; await ctx.Response.WriteAsJsonAsync(new { error = "Ungültiger Workspace-Pfad (.. nicht erlaubt)" }); return; } } } // Session einem anderen Benutzer zuweisen. Jeder Besitzer darf seine EIGENE Session // übergeben – die Ownership-Prüfung oben (existing.UserId == userId) stellt bereits sicher, // dass der Aufrufer der Besitzer ist. Der Ziel-User muss existieren. if (body.TryGetProperty("userId", out var newUid) && newUid.ValueKind == JsonValueKind.String && !string.IsNullOrWhiteSpace(newUid.GetString())) { var targetUser = await db.GetUser(newUid.GetString()); if (targetUser == null) { ctx.Response.StatusCode = 400; await ctx.Response.WriteAsJsonAsync(new { error = "Zielbenutzer nicht gefunden" }); return; } } await db.UpdateSession(sessionId, body, imageService, allowUserReassign: true); var session = await db.GetSession(sessionId); await ctx.Response.WriteAsJsonAsync(session); } private static async Task HandleDelete(HttpContext ctx, DatabaseService db) { var userId = ctx.GetUserId(); var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString(); if (string.IsNullOrEmpty(sessionId)) { ctx.Response.StatusCode = 400; return; } await db.DeleteSession(sessionId, userId); ctx.Response.StatusCode = 204; } /// /// Löst Session, Agent, Workspace und Prompt-Abhängigkeiten auf. /// Gibt null zurück und setzt StatusCode bei Fehler. /// private static async Task<(SessionRecord session, AgentRecord agent, IEnumerable marked, string memDir, IEnumerable skills, UserRecord user, string workspace, bool planMode)?> ResolvePromptContext(HttpContext ctx, DatabaseService db, MemoryService memoryService) { var userId = ctx.GetUserId(); var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString(); if (string.IsNullOrEmpty(sessionId)) { ctx.Response.StatusCode = 400; return null; } var session = await db.GetSession(sessionId); if (session == null || session.UserId != userId) { ctx.Response.StatusCode = 404; return null; } if (string.IsNullOrEmpty(session.AgentId)) { ctx.Response.StatusCode = 400; await ctx.Response.WriteAsJsonAsync(new { error = "Kein Agent ausgewählt" }); return null; } var agent = await db.GetAgent(session.AgentId); if (agent == null) { ctx.Response.StatusCode = 400; await ctx.Response.WriteAsJsonAsync(new { error = "Agent nicht gefunden" }); return null; } var marked = await db.GetMarkedMessages(sessionId); var memDir = memoryService.GetAgentMemoryDir(agent.AgentId); var skills = await db.GetAgentSkills(agent.AgentId); var user = await db.GetUser(userId); var workspace = WorkspaceHelper.ResolveWorkspace(session, user); if (string.IsNullOrEmpty(workspace) && skills.Any()) workspace = WorkspaceHelper.TempDir; var planMode = ctx.Request.Query.ContainsKey("planMode") && ctx.Request.Query["planMode"] == "true"; return (session, agent, marked, memDir, skills, user, workspace, planMode); } private static async Task HandleSystemPrompt(HttpContext ctx, DatabaseService db, MemoryService memoryService, SkillService skillService) { var result = await ResolvePromptContext(ctx, db, memoryService); if (result == null) return; var (session, agent, marked, memDir, skills, user, workspace, planMode) = result.Value; var systemPrompt = SystemPromptBuilder.Build(agent, session, marked, memoryService, memDir, skills, skillService, userPersona: user?.GetEffectivePersona(), resolvedWorkspace: workspace, planMode: planMode); ctx.Response.ContentType = "text/plain; charset=utf-8"; await ctx.Response.WriteAsync(systemPrompt); } private static async Task HandleSystemPromptBlocks(HttpContext ctx, DatabaseService db, MemoryService memoryService, SkillService skillService) { var result = await ResolvePromptContext(ctx, db, memoryService); if (result == null) return; var (session, agent, marked, memDir, skills, user, workspace, planMode) = result.Value; var blocks = SystemPromptBuilder.BuildBlocks(agent, session, marked, memoryService, memDir, skills, skillService, userPersona: user?.GetEffectivePersona(), resolvedWorkspace: workspace, planMode: planMode); await ctx.Response.WriteAsJsonAsync(blocks.Select(b => new { b.Icon, b.Label, b.Content })); } private static async Task HandlePayloadPreview(HttpContext ctx, DatabaseService db, MemoryService memoryService, SkillService skillService, ChatService chatService, ToolService toolService, ServerSettingsService settings) { var result = await ResolvePromptContext(ctx, db, memoryService); if (result == null) return; var (session, agent, marked, memDir, skills, user, workspace, planMode) = result.Value; // System-Prompt var promptBlocks = SystemPromptBuilder.BuildBlocks(agent, session, marked, memoryService, memDir, skills, skillService, userPersona: user?.GetEffectivePersona(), resolvedWorkspace: workspace, planMode: planMode); var systemPromptText = string.Join("\n\n", promptBlocks.Select(b => b.Content)); var systemPrompt = new List(); if (!string.IsNullOrWhiteSpace(systemPromptText)) systemPrompt.Add(new { type = "text", text = systemPromptText }); // Model var modelId = ChatService.ResolveModelId(agent.Model); // Messages aus der DB (exakt wie beim Chat) – includeThinking je nach Provider var previewProvider = OpenAiService.IsOpenAiProvider(modelId); var messages = await chatService.BuildHistoryFromDb(session.SessionId, promptBlocks: promptBlocks, includeThinking: previewProvider); // Tools (exakt wie AddToolsToPayload es machen würde) var hasWorkspace = !string.IsNullOrEmpty(workspace); var includeMemory = agent.UseMemory && !string.IsNullOrEmpty(agent.AgentId); var includeGitHubSearch = agent.UseGitHubSearch && toolService.HasGitHubToken; var includeSerperSearch = agent.UseSerperSearch && toolService.HasSerperKey; var hasTools = hasWorkspace || includeMemory || includeGitHubSearch || includeSerperSearch; var tools = new List(); if (hasTools) { var allTools = toolService.GetToolDefinitions(new ToolDefinitionOptions { IncludeFileTools = hasWorkspace, IncludeMemory = includeMemory, IncludeUserChoice = agent.AllowUserChoice, IncludeGitHubSearch = includeGitHubSearch, IncludeSerperSearch = includeSerperSearch }); if (planMode) allTools = toolService.FilterReadOnlyTools(allTools); tools.AddRange(allTools); } if (!string.IsNullOrEmpty(agent.WebSearchVersion)) tools.Add(new { type = agent.WebSearchVersion, name = "web_search", max_uses = 5 }); var toolChoice = hasTools || !string.IsNullOrEmpty(agent.WebSearchVersion) ? "any" : null; var effectiveChoice = toolChoice == "any" ? "any" : "auto"; // OpenAI-kompatible Provider: Payload im tatsächlichen API-Format anzeigen if (previewProvider) { var (provider, _) = OpenAiService.ParseModelString(modelId); var maxTokens = provider == "minimax" ? 32768 : 16384; // Messages konvertieren (Anthropic → OpenAI-Format) var openAiMessages = OpenAiService.BuildApiMessages(systemPromptText, messages); // Tools konvertieren (input_schema → function/parameters) var openAiTools = tools.Count > 0 ? OpenAiService.ConvertTo_openAiToolDefs(tools.ToArray()) : new List(); var payload = new Dictionary { ["model"] = modelId, ["max_tokens"] = maxTokens, ["messages"] = openAiMessages, ["stream"] = true, ["stream_options"] = new { include_usage = true } }; if (openAiTools.Count > 0) { payload["tools"] = openAiTools; payload["tool_choice"] = "auto"; } if (provider == "minimax") { payload["temperature"] = 1.0; payload["top_p"] = 0.95; payload["parallel_tool_calls"] = false; } if (provider == "nvidia") { var nvidiaModelId = modelId.Contains(":") ? modelId.Split(':')[1] : modelId; if (nvidiaModelId.Contains("kimi-k2", StringComparison.OrdinalIgnoreCase) || nvidiaModelId.Contains("qwq", StringComparison.OrdinalIgnoreCase) || nvidiaModelId.Contains("deepseek-r1", StringComparison.OrdinalIgnoreCase)) payload["chat_template_kwargs"] = new { thinking = true }; } ctx.Response.ContentType = "application/json; charset=utf-8"; await ctx.Response.WriteAsync(JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true })); return; } // Anthropic-Format Payload var anthropicPayload = new Dictionary { ["model"] = modelId, ["max_tokens"] = 16384, ["messages"] = messages, ["stream"] = true }; if (systemPrompt.Count > 0) anthropicPayload["system"] = systemPrompt; if (tools.Count > 0) { anthropicPayload["tools"] = tools; anthropicPayload["tool_choice"] = new { type = effectiveChoice }; } ctx.Response.ContentType = "application/json; charset=utf-8"; await ctx.Response.WriteAsync(JsonSerializer.Serialize(anthropicPayload, new JsonSerializerOptions { WriteIndented = true })); } private static async Task HandleStreaming(HttpContext ctx, DatabaseService db, StreamingTracker streamingTracker) { var userId = ctx.GetUserId(); 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 || session.UserId != userId) { ctx.Response.StatusCode = 404; return; } var isStreaming = streamingTracker.IsStreaming(sessionId); var chainId = streamingTracker.GetChainId(sessionId); await ctx.Response.WriteAsJsonAsync(new { streaming = isStreaming, chainId }); } /// /// Status eines dynamischen Workflows (ultracode) für die Session: LIVE aus dem /// WorkflowProgressTracker, solange ein Lauf aktiv ist – sonst die zuletzt persistierte /// Telemetrie (chains.workflow_info) der jüngsten Chain. Liefert Subagent-Anzahl, /// Tool-Aufrufe (+ Aufschlüsselung) und aggregierte Subagent-Tokens. /// private static async Task HandleWorkflowStatus(HttpContext ctx, DatabaseService db, StreamingTracker streamingTracker, WorkflowProgressTracker workflowTracker) { var userId = ctx.GetUserId(); 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 || session.UserId != userId) { ctx.Response.StatusCode = 404; return; } var streaming = streamingTracker.IsStreaming(sessionId); // 1) Aktueller Lauf registriert: Telemetrie aus den Transcript-Dateien lesen – // live aus journal.jsonl (Counts), nach Abschluss aus wf_.json (reich). var run = workflowTracker.Get(sessionId); if (run != null) { var wp = WorkflowTelemetryReader.Read(run); if (wp != null) { await ctx.Response.WriteAsJsonAsync(new { sessionId, streaming, status = wp.Status, chainId = wp.ChainId, workflow = wp }); return; } } // 2) Sonst: zuletzt persistierte Workflow-Telemetrie aus den aktiven Chains der Session. var chains = await db.GetActiveChains(sessionId); // ORDER BY id DESC var last = chains.FirstOrDefault(c => !string.IsNullOrEmpty(c.WorkflowInfo)); if (last != null) { WorkflowProgress wp = null; try { wp = JsonSerializer.Deserialize(last.WorkflowInfo); } catch { } await ctx.Response.WriteAsJsonAsync(new { sessionId, streaming, status = "completed", chainId = last.ChainId, workflow = wp }); return; } // 3) Für diese Session lief nie ein Workflow. await ctx.Response.WriteAsJsonAsync(new { sessionId, streaming, status = "none", workflow = (object)null }); } private static async Task HandleExec(HttpContext ctx, DatabaseService db) { var userId = ctx.GetUserId(); 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 || session.UserId != userId) { ctx.Response.StatusCode = 404; return; } var body = await JsonSerializer.DeserializeAsync(ctx.Request.Body); var command = body.TryGetProperty("command", out var c) ? c.GetString() : null; var mode = body.TryGetProperty("mode", out var m) ? m.GetString() ?? "background" : "background"; if (string.IsNullOrWhiteSpace(command)) { ctx.Response.StatusCode = 400; await ctx.Response.WriteAsJsonAsync(new { error = "Kein Befehl angegeben" }); return; } var user = await db.GetUser(userId); var resolvedWs = WorkspaceHelper.ResolveWorkspace(session, user); var workingDir = !string.IsNullOrWhiteSpace(resolvedWs) ? resolvedWs : WorkspaceHelper.TempDir; if (mode == "foreground") { // Synchron ausführen, auf Ergebnis warten try { string scriptPath = null; var psi = new ProcessStartInfo { FileName = WorkspaceHelper.ShellExecutable, WorkingDirectory = workingDir, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; if (WorkspaceHelper.IsWindows) { scriptPath = Path.Combine(Path.GetTempPath(), $"gott_exec_{Guid.NewGuid():N}.cmd"); await System.IO.File.WriteAllTextAsync(scriptPath, $"@echo off\r\n{command}\r\n"); psi.Arguments = $"/c \"{scriptPath}\""; } else { psi.RedirectStandardInput = true; } using var proc = System.Diagnostics.Process.Start(psi); if (proc == null) { if (scriptPath != null) try { System.IO.File.Delete(scriptPath); } catch { } ctx.Response.StatusCode = 500; await ctx.Response.WriteAsJsonAsync(new { error = "Prozess konnte nicht gestartet werden" }); return; } if (!WorkspaceHelper.IsWindows) { await proc.StandardInput.WriteAsync(command); proc.StandardInput.Close(); } var stdoutTask = proc.StandardOutput.ReadToEndAsync(); var stderrTask = proc.StandardError.ReadToEndAsync(); using var cts = new System.Threading.CancellationTokenSource(TimeSpan.FromSeconds(30)); try { await proc.WaitForExitAsync(cts.Token); } catch (OperationCanceledException) { proc.Kill(entireProcessTree: true); if (scriptPath != null) try { System.IO.File.Delete(scriptPath); } catch { } await ctx.Response.WriteAsJsonAsync(new { status = "timeout", output = "Timeout nach 30 Sekunden", exitCode = -1 }); return; } if (scriptPath != null) try { System.IO.File.Delete(scriptPath); } catch { } var stdout = await stdoutTask; var stderr = await stderrTask; var output = new System.Text.StringBuilder(); if (!string.IsNullOrEmpty(stdout)) output.Append(stdout); if (!string.IsNullOrEmpty(stderr)) { if (output.Length > 0) output.Append("\n--- stderr ---\n"); output.Append(stderr); } await ctx.Response.WriteAsJsonAsync(new { status = "completed", output = output.ToString(), exitCode = proc.ExitCode }); } catch (Exception ex) { ctx.Response.StatusCode = 500; await ctx.Response.WriteAsJsonAsync(new { error = ex.Message }); } } else { // Fire-and-forget try { ProcessStartInfo psi; if (WorkspaceHelper.IsWindows) { var scriptPath = Path.Combine(WorkspaceHelper.TempDir, $"gott-exec-{Guid.NewGuid():N}.bat"); await System.IO.File.WriteAllTextAsync(scriptPath, $"@echo off\r\ncd /d \"{workingDir}\"\r\n{command}\r\ndel \"%~f0\"\r\n"); psi = new ProcessStartInfo { FileName = "cmd.exe", Arguments = $"/c \"{scriptPath}\"", WorkingDirectory = workingDir, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }; } else { var scriptPath = Path.Combine(WorkspaceHelper.TempDir, $"gott-exec-{Guid.NewGuid():N}.sh"); await System.IO.File.WriteAllTextAsync(scriptPath, $"#!/bin/bash\ncd {workingDir}\n{command}\nrm -f {scriptPath}\n"); psi = new ProcessStartInfo { FileName = "systemd-run", Arguments = $"--scope --quiet bash {scriptPath}", WorkingDirectory = workingDir, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }; } var process = Process.Start(psi); if (process != null) { process.WaitForExit(500); } ctx.Response.StatusCode = 202; await ctx.Response.WriteAsJsonAsync(new { status = "started", command = command }); } catch (Exception ex) { ctx.Response.StatusCode = 500; await ctx.Response.WriteAsJsonAsync(new { error = ex.Message }); } } } private static async Task HandleGitStatus(HttpContext ctx, DatabaseService db) { var userId = ctx.GetUserId(); 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 || session.UserId != userId) { ctx.Response.StatusCode = 404; return; } var user = await db.GetUser(userId); var workingDir = WorkspaceHelper.ResolveWorkspace(session, user); if (string.IsNullOrEmpty(workingDir)) { ctx.Response.StatusCode = 400; await ctx.Response.WriteAsJsonAsync(new { error = "Kein Workspace konfiguriert" }); 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 { 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; int addCount = 0, editCount = 0, deleteCount = 0; var lines = stdout.Split('\n', StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { 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(); var lines = stdout.Split('\n', StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { if (line.Length < 4) continue; var x = line[0]; var y = line[1]; var file = line.Substring(3); 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); } 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 { } return new { add, edit, delete, outgoing }; } catch { return null; } } }