diff --git a/Endpoints/ProfileEndpoint.cs b/Endpoints/ProfileEndpoint.cs index e7faa18..0570170 100644 --- a/Endpoints/ProfileEndpoint.cs +++ b/Endpoints/ProfileEndpoint.cs @@ -45,9 +45,8 @@ public static class ProfileEndpoint var displayName = body.TryGetProperty("displayName", out var dn) ? dn.GetString() : null; var persona = body.TryGetProperty("persona", out var p) ? p.GetString() : null; - var defaultPath = body.TryGetProperty("defaultPath", out var dp) ? dp.GetString() : null; - await db.UpdateUserProfile(userId, displayName, persona, defaultPath); + await db.UpdateUserProfile(userId, displayName, persona); var user = await db.GetUser(userId); await ctx.Response.WriteAsJsonAsync(new diff --git a/Endpoints/SessionEndpoint.cs b/Endpoints/SessionEndpoint.cs index aa091dc..36e472b 100644 --- a/Endpoints/SessionEndpoint.cs +++ b/Endpoints/SessionEndpoint.cs @@ -63,7 +63,17 @@ public static class SessionEndpoint var imageRaw = body.TryGetProperty("image", out var img) ? img.GetString() : null; var image = imageService.ResizeDataUrl(imageRaw); var agentId = body.TryGetProperty("agentId", out var a) ? a.GetString() : null; - var workspace = body.TryGetProperty("workspace", out var w) ? w.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; @@ -89,6 +99,23 @@ public static class SessionEndpoint } 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; + } + } + } + await db.UpdateSession(sessionId, body, imageService); var session = await db.GetSession(sessionId); @@ -141,7 +168,10 @@ public static class SessionEndpoint var agentMemoryDir = memoryService.GetAgentMemoryDir(agent.AgentId); var agentSkills = await db.GetAgentSkills(agent.AgentId); var user = await db.GetUser(userId); - var systemPrompt = SystemPromptBuilder.Build(agent, session, markedMessages, memoryService, agentMemoryDir, agentSkills, skillService, userPersona: user?.GetEffectivePersona()); + var resolvedWorkspace = WorkspaceHelper.ResolveWorkspace(session, user); + if (string.IsNullOrEmpty(resolvedWorkspace) && agentSkills.Any()) + resolvedWorkspace = "/tmp"; + var systemPrompt = SystemPromptBuilder.Build(agent, session, markedMessages, memoryService, agentMemoryDir, agentSkills, skillService, userPersona: user?.GetEffectivePersona(), resolvedWorkspace: resolvedWorkspace); ctx.Response.ContentType = "text/plain; charset=utf-8"; await ctx.Response.WriteAsync(systemPrompt); @@ -175,7 +205,10 @@ public static class SessionEndpoint var agentMemoryDir = memoryService.GetAgentMemoryDir(agent.AgentId); var agentSkills = await db.GetAgentSkills(agent.AgentId); var user = await db.GetUser(userId); - var blocks = SystemPromptBuilder.BuildBlocks(agent, session, markedMessages, memoryService, agentMemoryDir, agentSkills, skillService, userPersona: user?.GetEffectivePersona()); + var resolvedWorkspace = WorkspaceHelper.ResolveWorkspace(session, user); + if (string.IsNullOrEmpty(resolvedWorkspace) && agentSkills.Any()) + resolvedWorkspace = "/tmp"; + var blocks = SystemPromptBuilder.BuildBlocks(agent, session, markedMessages, memoryService, agentMemoryDir, agentSkills, skillService, userPersona: user?.GetEffectivePersona(), resolvedWorkspace: resolvedWorkspace); await ctx.Response.WriteAsJsonAsync(blocks.Select(b => new { b.Icon, b.Label, b.Content })); } @@ -230,7 +263,9 @@ public static class SessionEndpoint return; } - var workingDir = !string.IsNullOrWhiteSpace(session.Workspace) ? session.Workspace : "/tmp"; + var user = await db.GetUser(userId); + var resolvedWs = WorkspaceHelper.ResolveWorkspace(session, user); + var workingDir = !string.IsNullOrWhiteSpace(resolvedWs) ? resolvedWs : "/tmp"; if (mode == "foreground") { @@ -343,7 +378,8 @@ public static class SessionEndpoint return; } - var workingDir = !string.IsNullOrWhiteSpace(session.Workspace) ? session.Workspace : null; + var user = await db.GetUser(userId); + var workingDir = WorkspaceHelper.ResolveWorkspace(session, user); if (string.IsNullOrEmpty(workingDir)) { ctx.Response.StatusCode = 400; diff --git a/Endpoints/WorkspaceEndpoint.cs b/Endpoints/WorkspaceEndpoint.cs index c2899f0..fa10a57 100644 --- a/Endpoints/WorkspaceEndpoint.cs +++ b/Endpoints/WorkspaceEndpoint.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using Gott.Extensions; +using Gott.Models; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; @@ -52,22 +53,42 @@ public static class WorkspaceEndpoint app.MapDelete("/api/workspace/agent/dir", HandleAgentRmdir); } - private static async Task HandleTree(HttpContext ctx) + /// + /// Lädt Session + User und löst den Workspace-Pfad auf (relativ → absolut via DefaultPath). + /// Gibt null zurück und setzt StatusCode 400, wenn kein gültiger Workspace vorhanden ist. + /// + private static async Task<(SessionRecord session, string basePath)?> ResolveSessionWorkspace(HttpContext ctx, string sessionId) { - var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault(); - var subPath = ctx.Request.Query["path"].FirstOrDefault() ?? ""; - - // Workspace aus Session holen var db = ctx.RequestServices.GetRequiredService(); var session = await db.GetSession(sessionId ?? ""); if (session == null || session.UserId != ctx.GetUserId() || string.IsNullOrEmpty(session.Workspace)) { ctx.Response.StatusCode = 400; await ctx.Response.WriteAsJsonAsync(new { error = "Kein Workspace konfiguriert" }); - return; + return null; } - var basePath = Path.GetFullPath(session.Workspace); + var userId = ctx.GetUserId(); + var user = await db.GetUser(userId); + var resolved = Gott.Services.WorkspaceHelper.ResolveWorkspace(session, user); + if (string.IsNullOrEmpty(resolved)) + { + ctx.Response.StatusCode = 400; + await ctx.Response.WriteAsJsonAsync(new { error = "Workspace kann nicht aufgelöst werden. Default-Pfad im Profil setzen." }); + return null; + } + + return (session, Path.GetFullPath(resolved)); + } + + private static async Task HandleTree(HttpContext ctx) + { + var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault(); + var subPath = ctx.Request.Query["path"].FirstOrDefault() ?? ""; + + var result = await ResolveSessionWorkspace(ctx, sessionId); + if (result == null) return; + var (session, basePath) = result.Value; var targetPath = Path.GetFullPath(Path.Combine(basePath, subPath)); // Sicherheit: nicht aus dem Workspace ausbrechen @@ -122,16 +143,10 @@ public static class WorkspaceEndpoint return; } - var db = ctx.RequestServices.GetRequiredService(); - var session = await db.GetSession(sessionId ?? ""); - if (session == null || session.UserId != ctx.GetUserId() || string.IsNullOrEmpty(session.Workspace)) - { - ctx.Response.StatusCode = 400; - await ctx.Response.WriteAsJsonAsync(new { error = "Kein Workspace konfiguriert" }); - return; - } + var result = await ResolveSessionWorkspace(ctx, sessionId); + if (result == null) return; + var (session, basePath) = result.Value; - var basePath = Path.GetFullPath(session.Workspace); var targetPath = Path.GetFullPath(Path.Combine(basePath, filePath)); if (!targetPath.StartsWith(basePath)) @@ -180,16 +195,10 @@ public static class WorkspaceEndpoint return; } - var db = ctx.RequestServices.GetRequiredService(); - var session = await db.GetSession(sessionId ?? ""); - if (session == null || session.UserId != ctx.GetUserId() || string.IsNullOrEmpty(session.Workspace)) - { - ctx.Response.StatusCode = 400; - await ctx.Response.WriteAsJsonAsync(new { error = "Kein Workspace konfiguriert" }); - return; - } + var result = await ResolveSessionWorkspace(ctx, sessionId); + if (result == null) return; + var (session, basePath) = result.Value; - var basePath = Path.GetFullPath(session.Workspace); var targetPath = Path.GetFullPath(Path.Combine(basePath, filePath)); if (!targetPath.StartsWith(basePath)) @@ -235,13 +244,6 @@ public static class WorkspaceEndpoint var entries = new List(); - // Parent - var parent = Directory.GetParent(targetPath); - if (parent != null) - { - entries.Add(new { name = "..", path = parent.FullName.Replace('\\', '/'), type = "dir" }); - } - try { foreach (var dir in Directory.GetDirectories(targetPath).OrderBy(d => Path.GetFileName(d), StringComparer.OrdinalIgnoreCase)) @@ -473,16 +475,10 @@ public static class WorkspaceEndpoint return; } - var db = ctx.RequestServices.GetRequiredService(); - var session = await db.GetSession(sessionId ?? ""); - if (session == null || session.UserId != ctx.GetUserId() || string.IsNullOrEmpty(session.Workspace)) - { - ctx.Response.StatusCode = 400; - await ctx.Response.WriteAsJsonAsync(new { error = "Kein Workspace konfiguriert" }); - return; - } + var result = await ResolveSessionWorkspace(ctx, sessionId); + if (result == null) return; + var (session, basePath) = result.Value; - var basePath = Path.GetFullPath(session.Workspace); var targetPath = Path.GetFullPath(Path.Combine(basePath, dirPath)); if (!targetPath.StartsWith(basePath)) @@ -514,16 +510,10 @@ public static class WorkspaceEndpoint return; } - var db = ctx.RequestServices.GetRequiredService(); - var session = await db.GetSession(sessionId ?? ""); - if (session == null || session.UserId != ctx.GetUserId() || string.IsNullOrEmpty(session.Workspace)) - { - ctx.Response.StatusCode = 400; - await ctx.Response.WriteAsJsonAsync(new { error = "Kein Workspace konfiguriert" }); - return; - } + var result = await ResolveSessionWorkspace(ctx, sessionId); + if (result == null) return; + var (session, basePath) = result.Value; - var basePath = Path.GetFullPath(session.Workspace); var targetPath = Path.GetFullPath(Path.Combine(basePath, dirPath)); if (!targetPath.StartsWith(basePath) || targetPath == basePath) @@ -650,15 +640,10 @@ public static class WorkspaceEndpoint return; } - var db = ctx.RequestServices.GetRequiredService(); - var session = await db.GetSession(sessionId ?? ""); - if (session == null || session.UserId != ctx.GetUserId() || string.IsNullOrEmpty(session.Workspace)) - { - ctx.Response.StatusCode = 400; - return; - } + var result = await ResolveSessionWorkspace(ctx, sessionId); + if (result == null) return; + var basePath = result.Value.basePath; - var basePath = Path.GetFullPath(session.Workspace); var targetPath = Path.GetFullPath(Path.Combine(basePath, filePath)); if (!targetPath.StartsWith(basePath)) diff --git a/Services/ChatService.cs b/Services/ChatService.cs index 953ec57..d2259d6 100644 --- a/Services/ChatService.cs +++ b/Services/ChatService.cs @@ -58,14 +58,16 @@ public class ChatService var agentMemoryDir = _memoryService.GetAgentMemoryDir(agent.AgentId); var agentSkills = await _db.GetAgentSkills(agent.AgentId); var user = userId != null ? await _db.GetUser(userId) : null; - var systemPromptText = SystemPromptBuilder.Build( - agent, session, markedMessages, _memoryService, agentMemoryDir, - agentSkills, _skillService, userPersona: user?.GetEffectivePersona()); - var workspace = session.Workspace; + var workspace = WorkspaceHelper.ResolveWorkspace(session, user); if (string.IsNullOrEmpty(workspace) && agentSkills.Any()) workspace = "/tmp"; + var systemPromptText = SystemPromptBuilder.Build( + agent, session, markedMessages, _memoryService, agentMemoryDir, + agentSkills, _skillService, userPersona: user?.GetEffectivePersona(), + resolvedWorkspace: workspace); + var toolChoice = agent.ToolChoice ?? "any"; var hasWorkspace = !string.IsNullOrEmpty(workspace); var includeMemory = agent.UseMemory && !string.IsNullOrEmpty(agent.AgentId); diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs index 70e1fa0..8813387 100644 --- a/Services/DatabaseService.cs +++ b/Services/DatabaseService.cs @@ -889,12 +889,12 @@ public class DatabaseService new { userId, passwordHash, now = DateTime.UtcNow.ToString("o") }); } - public async Task UpdateUserProfile(string userId, string displayName, string persona, string defaultPath = null) + public async Task UpdateUserProfile(string userId, string displayName, string persona) { using var conn = CreateConnection(); await conn.ExecuteAsync( - "UPDATE users SET display_name = @displayName, persona = @persona, default_path = @defaultPath, updated_at = @now WHERE user_id = @userId", - new { userId, displayName, persona, defaultPath, now = DateTime.UtcNow.ToString("o") }); + "UPDATE users SET display_name = @displayName, persona = @persona, updated_at = @now WHERE user_id = @userId", + new { userId, displayName, persona, now = DateTime.UtcNow.ToString("o") }); } public async Task GetUserSettings(string userId) diff --git a/Services/SystemPromptBuilder.cs b/Services/SystemPromptBuilder.cs index b6ca7a5..de2687b 100644 --- a/Services/SystemPromptBuilder.cs +++ b/Services/SystemPromptBuilder.cs @@ -13,7 +13,7 @@ public class PromptBlock public static class SystemPromptBuilder { - public static List BuildBlocks(AgentRecord agent, SessionRecord session, IEnumerable markedMessages = null, MemoryService memoryService = null, string agentMemoryDir = null, IEnumerable skillSlugs = null, SkillService skillService = null, string userPersona = null) + public static List BuildBlocks(AgentRecord agent, SessionRecord session, IEnumerable markedMessages = null, MemoryService memoryService = null, string agentMemoryDir = null, IEnumerable skillSlugs = null, SkillService skillService = null, string userPersona = null, string resolvedWorkspace = null) { var blocks = new List(); @@ -47,7 +47,8 @@ public static class SystemPromptBuilder bool toolAny = string.Equals(agent.ToolChoice, "any", System.StringComparison.OrdinalIgnoreCase); - if (!string.IsNullOrWhiteSpace(session.Workspace)) + var effectiveWorkspace = resolvedWorkspace ?? session.Workspace; + if (!string.IsNullOrWhiteSpace(effectiveWorkspace)) { if (toolAny) { @@ -67,7 +68,7 @@ JEDE Datei-Operation und JEDER Shell-Befehl MUSS über einen Tool-Aufruf erfolge Content = @$"## Tools Du hast Tools zur Verfügung (read_file, write_file, edit_file, list_files, delete_file, execute). Benutze sie um Dateien zu lesen, schreiben und bearbeiten. -Der Workspace-Pfad ist: `{session.Workspace}` +Der Workspace-Pfad ist: `{effectiveWorkspace}` Alle Dateipfade in Tool-Aufrufen müssen relativ zu diesem Workspace sein oder absolute Pfade verwenden, die mit diesem Pfad beginnen." }); } @@ -137,7 +138,7 @@ Alle Dateipfade in Tool-Aufrufen müssen relativ zu diesem Workspace sein oder a } // Git-Commit-Verhalten: relevant sobald Workspace vorhanden (unabhängig von ToolChoice) - if (!string.IsNullOrWhiteSpace(session.Workspace)) + if (!string.IsNullOrWhiteSpace(effectiveWorkspace)) { var autoCommit = (agent.AutoCommit ?? "").ToLowerInvariant(); if (autoCommit == "mention") @@ -170,7 +171,7 @@ Alle Dateipfade in Tool-Aufrufen müssen relativ zu diesem Workspace sein oder a // autoCommit == "" → kein Block, Thema wird nicht erwähnt } - if (toolAny && !string.IsNullOrWhiteSpace(session.Workspace)) + if (toolAny && !string.IsNullOrWhiteSpace(effectiveWorkspace)) { blocks.Add(new PromptBlock { @@ -211,9 +212,9 @@ Diese Regel hat höchste Priorität und gilt für JEDE Antwort: Wenn eine Datei return blocks; } - public static string Build(AgentRecord agent, SessionRecord session, IEnumerable markedMessages = null, MemoryService memoryService = null, string agentMemoryDir = null, IEnumerable skillSlugs = null, SkillService skillService = null, string userPersona = null) + public static string Build(AgentRecord agent, SessionRecord session, IEnumerable markedMessages = null, MemoryService memoryService = null, string agentMemoryDir = null, IEnumerable skillSlugs = null, SkillService skillService = null, string userPersona = null, string resolvedWorkspace = null) { - var blocks = BuildBlocks(agent, session, markedMessages, memoryService, agentMemoryDir, skillSlugs, skillService, userPersona); + var blocks = BuildBlocks(agent, session, markedMessages, memoryService, agentMemoryDir, skillSlugs, skillService, userPersona, resolvedWorkspace); return string.Join("\n\n", blocks.Select(b => b.Content)); } } diff --git a/Services/WorkspaceHelper.cs b/Services/WorkspaceHelper.cs new file mode 100644 index 0000000..ca801ab --- /dev/null +++ b/Services/WorkspaceHelper.cs @@ -0,0 +1,88 @@ +using System; +using System.IO; +using Gott.Models; + +namespace Gott.Services; + +public static class WorkspaceHelper +{ + /// + /// Löst den Workspace-Pfad einer Session auf. + /// session.Workspace ist relativ zum user.DefaultPath. + /// Gibt den absoluten Pfad zurück oder null wenn kein Workspace konfiguriert ist. + /// + public static string ResolveWorkspace(SessionRecord session, UserRecord user) + { + if (session == null || string.IsNullOrWhiteSpace(session.Workspace)) + return null; + + var workspace = session.Workspace.Trim(); + + // Wenn der Workspace bereits ein absoluter Pfad ist (Legacy/Fallback) + if (Path.IsPathRooted(workspace)) + return Path.GetFullPath(workspace); + + // Relativer Pfad → mit DefaultPath des Users kombinieren + var basePath = user?.DefaultPath?.Trim(); + if (string.IsNullOrWhiteSpace(basePath)) + return null; // Kein DefaultPath gesetzt → relativer Workspace kann nicht aufgelöst werden + + var resolved = Path.GetFullPath(Path.Combine(basePath, workspace)); + + // Sicherheit: darf nicht aus dem DefaultPath ausbrechen + var normalizedBase = Path.GetFullPath(basePath); + if (!resolved.StartsWith(normalizedBase + Path.DirectorySeparatorChar) && resolved != normalizedBase) + return null; + + return resolved; + } + + /// + /// Validiert einen relativen Workspace-Pfad. + /// Prüft auf Path-Traversal (.. etc.). + /// Gibt den bereinigten Pfad zurück oder null bei ungültigem Pfad. + /// + public static string ValidateRelativeWorkspace(string workspace) + { + if (string.IsNullOrWhiteSpace(workspace)) + return null; + + var trimmed = workspace.Trim().TrimStart('/').TrimEnd('/'); + if (string.IsNullOrEmpty(trimmed)) + return null; + + // Path-Traversal verhindern + if (trimmed.Contains("..") || trimmed.Contains("~")) + return null; + + // Keine absoluten Pfade erlauben + if (Path.IsPathRooted(trimmed)) + return null; + + return trimmed; + } + + /// + /// Konvertiert einen absoluten Workspace-Pfad in einen relativen Pfad zum DefaultPath. + /// Wird beim Speichern verwendet, damit alte absolute Pfade in relative umgewandelt werden. + /// + public static string ToRelativeWorkspace(string absoluteWorkspace, string defaultPath) + { + if (string.IsNullOrWhiteSpace(absoluteWorkspace)) + return null; + + if (string.IsNullOrWhiteSpace(defaultPath)) + return absoluteWorkspace; // Kein DefaultPath → absoluten Pfad beibehalten (Legacy) + + var normalizedBase = Path.GetFullPath(defaultPath.Trim()); + var normalizedWorkspace = Path.GetFullPath(absoluteWorkspace.Trim()); + + if (normalizedWorkspace.StartsWith(normalizedBase + Path.DirectorySeparatorChar)) + return normalizedWorkspace.Substring(normalizedBase.Length + 1); + + if (normalizedWorkspace == normalizedBase) + return ""; + + return absoluteWorkspace; // Außerhalb des DefaultPath → absoluten Pfad beibehalten (Legacy) + } +} diff --git a/wwwroot/css/file-explorer.css b/wwwroot/css/file-explorer.css index 67723e5..5be70d8 100644 --- a/wwwroot/css/file-explorer.css +++ b/wwwroot/css/file-explorer.css @@ -94,7 +94,7 @@ color: var(--text); display: flex; align-items: center; - justify-content: space-between; + gap: var(--badge-spacing); } .fe-dir-label { diff --git a/wwwroot/index.html b/wwwroot/index.html index 50ceec9..b272ae8 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -398,11 +398,7 @@ - Wird als Basis-Pfad beim Auswählen eines Workspace in Sessions verwendet. -
- - -
+ Wird im System-Prompt verwendet, wenn der Agent "Kennt sein Gegenüber" aktiviert hat. diff --git a/wwwroot/js/file-explorer.js b/wwwroot/js/file-explorer.js index 5c462be..6cd656f 100644 --- a/wwwroot/js/file-explorer.js +++ b/wwwroot/js/file-explorer.js @@ -504,6 +504,7 @@ function openFolderBrowser(opts = {}) { mode: 'folder', onSelect: opts.onSelect || null, currentAbsPath: opts.startPath || '/', + basePath: (opts.basePath || '').replace(/\/+$/, '') || null, history: [] }; @@ -592,16 +593,37 @@ async function fbLoadDir(absPath) { sidebar.innerHTML = '
Leer
'; } - document.getElementById('feBack').disabled = _feState.history.length === 0; + const currentNorm = data.current.replace(/\/+$/, ''); + const atBase = _feState.basePath && currentNorm === _feState.basePath; + const atRoot = currentNorm === '' || currentNorm === '/'; + document.getElementById('feBack').disabled = atBase || (atRoot && _feState.history.length === 0); } catch (e) { sidebar.innerHTML = '
Netzwerk-Fehler
'; } } function fbGoBack() { - if (!_feState || _feState.history.length === 0) return; - const prev = _feState.history.pop(); - fbLoadDir(prev); + if (!_feState) return; + // Nicht über den basePath hinaus navigieren + const currentNorm = _feState.currentAbsPath.replace(/\/+$/, ''); + if (_feState.basePath && currentNorm === _feState.basePath) return; + + if (_feState.history.length > 0) { + const prev = _feState.history.pop(); + // Prüfe ob der vorherige Pfad über dem basePath liegt + if (_feState.basePath && !prev.replace(/\/+$/, '').startsWith(_feState.basePath)) { + _feState.history.length = 0; + fbLoadDir(_feState.basePath); + return; + } + fbLoadDir(prev); + } else { + // Kein History-Eintrag → zum Parent navigieren + const parent = currentNorm.includes('/') ? currentNorm.substring(0, currentNorm.lastIndexOf('/')) || '/' : '/'; + if (parent !== currentNorm) { + fbLoadDir(parent); + } + } } // ── Folder Browser: Create Directory ── diff --git a/wwwroot/js/pages/profile-page.js b/wwwroot/js/pages/profile-page.js index 775bee7..5bf2912 100644 --- a/wwwroot/js/pages/profile-page.js +++ b/wwwroot/js/pages/profile-page.js @@ -10,7 +10,6 @@ registerPage('profile', { var usernameInput = document.getElementById('profileUsername'); var displayNameInput = document.getElementById('profileDisplayName'); var defaultPathInput = document.getElementById('profileDefaultPath'); - var defaultPathBrowse = document.getElementById('profileDefaultPathBrowse'); var personaInput = document.getElementById('profilePersona'); var saveBtn = document.getElementById('profileSaveBtn'); var messageDiv = document.getElementById('profileMessage'); @@ -43,18 +42,6 @@ registerPage('profile', { displayNameInput.value = profile.displayName || ''; defaultPathInput.value = profile.defaultPath || ''; - // Default Path Browse - if (defaultPathBrowse) { - defaultPathBrowse.addEventListener('click', function() { - openFolderBrowser({ - startPath: defaultPathInput.value.trim() || '/', - onSelect: function(path) { - defaultPathInput.value = path; - } - }); - }); - } - // Profil speichern saveBtn.addEventListener('click', async function() { saveBtn.disabled = true; @@ -64,7 +51,6 @@ registerPage('profile', { try { var updated = await apiUpdateProfile({ displayName: displayNameInput.value.trim(), - defaultPath: defaultPathInput.value.trim() || null, persona: _profileMde.value().trim() }); diff --git a/wwwroot/js/pages/session-page.js b/wwwroot/js/pages/session-page.js index e436d86..921050a 100644 --- a/wwwroot/js/pages/session-page.js +++ b/wwwroot/js/pages/session-page.js @@ -129,10 +129,29 @@ registerPage('session', { const workspaceBrowse = document.getElementById('sessionWorkspaceBrowse'); if (workspaceBrowse) { workspaceBrowse.addEventListener('click', () => { + const defaultPath = (currentUser && currentUser.defaultPath) || ''; + const currentVal = workspaceEl.value.trim(); + // Für den Folder-Browser den absoluten Startpfad zusammenbauen + let startPath = '/'; + if (defaultPath && currentVal) { + startPath = defaultPath.replace(/\/+$/, '') + '/' + currentVal; + } else if (defaultPath) { + startPath = defaultPath; + } else if (currentVal) { + startPath = currentVal; + } openFolderBrowser({ - startPath: workspaceEl.value.trim() || (currentUser && currentUser.defaultPath) || '/', - onSelect(path) { - workspaceEl.value = path; + startPath, + basePath: defaultPath || null, + onSelect(absolutePath) { + // Relativen Pfad zum DefaultPath berechnen + if (defaultPath && absolutePath.startsWith(defaultPath)) { + let rel = absolutePath.slice(defaultPath.length).replace(/^\/+/, ''); + workspaceEl.value = rel || ''; + } else { + // Kein DefaultPath oder Pfad außerhalb → absoluten Pfad beibehalten (Legacy) + workspaceEl.value = absolutePath; + } } }); });