diff --git a/Endpoints/InfoEndpoint.cs b/Endpoints/InfoEndpoint.cs index ae57b9e..7f17dc5 100644 --- a/Endpoints/InfoEndpoint.cs +++ b/Endpoints/InfoEndpoint.cs @@ -29,7 +29,8 @@ public static class InfoEndpoint else { var dir = AppContext.BaseDirectory; - var exePath = Path.Combine(dir, "gott"); + var exeName = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows) ? "gott.exe" : "gott"; + var exePath = Path.Combine(dir, exeName); if (File.Exists(exePath)) { var lastWrite = File.GetLastWriteTimeUtc(exePath); @@ -37,6 +38,7 @@ public static class InfoEndpoint } } - return Results.Json(new { buildTime }); + var isWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows); + return Results.Json(new { buildTime, isWindows }); } } diff --git a/Endpoints/SessionEndpoint.cs b/Endpoints/SessionEndpoint.cs index df9d64b..d6a297c 100644 --- a/Endpoints/SessionEndpoint.cs +++ b/Endpoints/SessionEndpoint.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Text.Json; using System.Threading; @@ -234,7 +235,7 @@ public static class SessionEndpoint var user = await db.GetUser(userId); var workspace = WorkspaceHelper.ResolveWorkspace(session, user); if (string.IsNullOrEmpty(workspace) && skills.Any()) - workspace = "/tmp"; + workspace = WorkspaceHelper.TempDir; var planMode = ctx.Request.Query.ContainsKey("planMode") && ctx.Request.Query["planMode"] == "true"; return (session, agent, marked, memDir, skills, user, workspace, planMode); @@ -317,7 +318,7 @@ public static class SessionEndpoint var user = await db.GetUser(userId); var resolvedWs = WorkspaceHelper.ResolveWorkspace(session, user); - var workingDir = !string.IsNullOrWhiteSpace(resolvedWs) ? resolvedWs : "/tmp"; + var workingDir = !string.IsNullOrWhiteSpace(resolvedWs) ? resolvedWs : WorkspaceHelper.TempDir; if (mode == "foreground") { @@ -326,14 +327,16 @@ public static class SessionEndpoint { var psi = new ProcessStartInfo { - FileName = "/bin/bash", + FileName = WorkspaceHelper.ShellExecutable, WorkingDirectory = workingDir, - RedirectStandardInput = true, + RedirectStandardInput = !WorkspaceHelper.IsWindows, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; + if (WorkspaceHelper.IsWindows) + psi.Arguments = WorkspaceHelper.ShellArguments(command); using var proc = System.Diagnostics.Process.Start(psi); if (proc == null) @@ -343,8 +346,11 @@ public static class SessionEndpoint return; } - await proc.StandardInput.WriteAsync(command); - proc.StandardInput.Close(); + if (!WorkspaceHelper.IsWindows) + { + await proc.StandardInput.WriteAsync(command); + proc.StandardInput.Close(); + } using var cts = new System.Threading.CancellationTokenSource(TimeSpan.FromSeconds(30)); try @@ -379,22 +385,40 @@ public static class SessionEndpoint } else { - // Fire-and-forget via systemd-run + // Fire-and-forget try { - var scriptPath = $"/tmp/gott-exec-{Guid.NewGuid():N}.sh"; - await System.IO.File.WriteAllTextAsync(scriptPath, $"#!/bin/bash\ncd {workingDir}\n{command}\nrm -f {scriptPath}\n"); - - var psi = new ProcessStartInfo + ProcessStartInfo psi; + if (WorkspaceHelper.IsWindows) { - FileName = "systemd-run", - Arguments = $"--scope --quiet bash {scriptPath}", - WorkingDirectory = workingDir, - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true - }; + 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) diff --git a/Endpoints/WorkspaceEndpoint.cs b/Endpoints/WorkspaceEndpoint.cs index f6260bd..86e6cae 100644 --- a/Endpoints/WorkspaceEndpoint.cs +++ b/Endpoints/WorkspaceEndpoint.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; +using Gott.Services; namespace Gott.Endpoints; @@ -83,7 +84,7 @@ public static class WorkspaceEndpoint var userId = ctx.GetUserId(); var user = await db.GetUser(userId); - var resolved = Gott.Services.WorkspaceHelper.ResolveWorkspace(session, user); + var resolved = WorkspaceHelper.ResolveWorkspace(session, user); if (string.IsNullOrEmpty(resolved)) { ctx.Response.StatusCode = 400; @@ -125,7 +126,7 @@ public static class WorkspaceEndpoint var subPath = ctx.Request.Query["path"].FirstOrDefault() ?? ""; var targetPath = Path.GetFullPath(Path.Combine(basePath, subPath)); - if (!targetPath.StartsWith(basePath)) + if (!WorkspaceHelper.IsSubPathOf(targetPath, basePath)) { ctx.Response.StatusCode = 403; return; @@ -144,7 +145,7 @@ public static class WorkspaceEndpoint { var name = Path.GetFileName(dir); if (IgnoredDirs.Contains(name)) continue; - var relPath = Path.GetRelativePath(basePath, dir).Replace('\\', '/'); + var relPath = WorkspaceHelper.GetRelativePath(basePath, dir); entries.Add(new { name, path = relPath, type = "dir" }); } @@ -152,7 +153,7 @@ public static class WorkspaceEndpoint { var name = Path.GetFileName(file); var ext = Path.GetExtension(file); - var relPath = Path.GetRelativePath(basePath, file).Replace('\\', '/'); + var relPath = WorkspaceHelper.GetRelativePath(basePath, file); var size = new FileInfo(file).Length; var isText = TextExtensions.Contains(ext) || IsLikelyTextFile(name); entries.Add(new { name, path = relPath, type = "file", ext, size, isText }); @@ -173,7 +174,7 @@ public static class WorkspaceEndpoint } var targetPath = Path.GetFullPath(Path.Combine(basePath, filePath)); - if (!targetPath.StartsWith(basePath)) + if (!WorkspaceHelper.IsSubPathOf(targetPath, basePath)) { ctx.Response.StatusCode = 403; return; @@ -218,7 +219,7 @@ public static class WorkspaceEndpoint } var targetPath = Path.GetFullPath(Path.Combine(basePath, filePath)); - if (!targetPath.StartsWith(basePath)) + if (!WorkspaceHelper.IsSubPathOf(targetPath, basePath)) { ctx.Response.StatusCode = 403; return; @@ -271,7 +272,7 @@ public static class WorkspaceEndpoint } var targetPath = Path.GetFullPath(Path.Combine(basePath, dirPath)); - if (!targetPath.StartsWith(basePath)) + if (!WorkspaceHelper.IsSubPathOf(targetPath, basePath)) { ctx.Response.StatusCode = 403; return; @@ -287,7 +288,7 @@ public static class WorkspaceEndpoint Directory.CreateDirectory(targetPath); await ctx.Response.WriteAsJsonAsync(new { - path = Path.GetRelativePath(basePath, targetPath).Replace('\\', '/'), + path = WorkspaceHelper.GetRelativePath(basePath, targetPath), created = true }); } @@ -296,7 +297,18 @@ public static class WorkspaceEndpoint private static async Task HandleBrowse(HttpContext ctx) { - var path = ctx.Request.Query["path"].FirstOrDefault() ?? "/"; + var path = ctx.Request.Query["path"].FirstOrDefault() ?? (WorkspaceHelper.IsWindows ? "" : "/"); + + // Windows: Leerer Pfad oder Root → Laufwerke anzeigen + if (WorkspaceHelper.IsWindows && string.IsNullOrEmpty(path)) + { + var drives = System.IO.DriveInfo.GetDrives() + .Where(d => d.IsReady) + .Select(d => new { name = d.Name.TrimEnd('\\'), path = d.Name.Replace('\\', '/'), type = "dir" }) + .ToList(); + await ctx.Response.WriteAsJsonAsync(new { current = "", entries = drives }); + return; + } var targetPath = Path.GetFullPath(path); if (!Directory.Exists(targetPath)) @@ -368,7 +380,7 @@ public static class WorkspaceEndpoint if (basePath == null) return; var targetPath = Path.GetFullPath(Path.Combine(basePath, filePath)); - if (!targetPath.StartsWith(basePath)) + if (!WorkspaceHelper.IsSubPathOf(targetPath, basePath)) { ctx.Response.StatusCode = 403; return; diff --git a/Services/ChatService.cs b/Services/ChatService.cs index aa60b24..82a77bb 100644 --- a/Services/ChatService.cs +++ b/Services/ChatService.cs @@ -66,7 +66,7 @@ public class ChatService var workspace = WorkspaceHelper.ResolveWorkspace(session, user); if (string.IsNullOrEmpty(workspace) && agentSkills.Any()) - workspace = "/tmp"; + workspace = WorkspaceHelper.TempDir; var promptBlocks = SystemPromptBuilder.BuildBlocks( agent, session, markedMessages, _memoryService, agentMemoryDir, diff --git a/Services/MemoryService.cs b/Services/MemoryService.cs index d371176..33bbaee 100644 --- a/Services/MemoryService.cs +++ b/Services/MemoryService.cs @@ -81,7 +81,7 @@ public class MemoryService ? baseDir : Path.GetFullPath(Path.Combine(baseDir, subPath)); - if (!targetDir.StartsWith(baseDir)) + if (!WorkspaceHelper.IsSubPathOf(targetDir, baseDir)) throw new Exception("Zugriff außerhalb des Memory-Verzeichnisses nicht erlaubt"); if (!Directory.Exists(targetDir)) @@ -89,9 +89,9 @@ public class MemoryService var entries = new List(); foreach (var dir in Directory.GetDirectories(targetDir)) - entries.Add(Path.GetRelativePath(baseDir, dir) + "/"); + entries.Add(WorkspaceHelper.GetRelativePath(baseDir, dir) + "/"); foreach (var file in Directory.GetFiles(targetDir)) - entries.Add(Path.GetRelativePath(baseDir, file)); + entries.Add(WorkspaceHelper.GetRelativePath(baseDir, file)); return entries; } @@ -148,7 +148,7 @@ public class MemoryService { var baseDir = GetAgentMemoryDir(agentId); var full = Path.GetFullPath(Path.Combine(baseDir, fileName)); - if (!full.StartsWith(baseDir)) + if (!WorkspaceHelper.IsSubPathOf(full, baseDir)) return null; return full; } diff --git a/Services/ToolService.cs b/Services/ToolService.cs index 430e981..325d053 100644 --- a/Services/ToolService.cs +++ b/Services/ToolService.cs @@ -300,7 +300,7 @@ public class ToolService return "Fehler: Kein Workspace konfiguriert"; var path = ResolvePath(input.GetProperty("path").GetString() ?? "", workspace); if (!File.Exists(path)) - return $"Fehler: Datei nicht gefunden: {Path.GetRelativePath(workspace, path)}"; + return $"Fehler: Datei nicht gefunden: {WorkspaceHelper.GetRelativePath(workspace, path)}"; return File.ReadAllText(path); } case "write_file": @@ -312,7 +312,7 @@ public class ToolService if (dir != null) Directory.CreateDirectory(dir); var content = input.GetProperty("content").GetString() ?? ""; File.WriteAllText(path, content); - return $"Datei geschrieben: {Path.GetRelativePath(workspace, path)} ({content.Length} Zeichen)"; + return $"Datei geschrieben: {WorkspaceHelper.GetRelativePath(workspace, path)} ({content.Length} Zeichen)"; } case "edit_file": { @@ -320,7 +320,7 @@ public class ToolService return "Fehler: Kein Workspace konfiguriert"; var path = ResolvePath(input.GetProperty("path").GetString() ?? "", workspace); if (!File.Exists(path)) - return $"Fehler: Datei nicht gefunden: {Path.GetRelativePath(workspace, path)}"; + return $"Fehler: Datei nicht gefunden: {WorkspaceHelper.GetRelativePath(workspace, path)}"; var fileContent = File.ReadAllText(path); var oldText = input.GetProperty("old_text").GetString() ?? ""; var newText = input.GetProperty("new_text").GetString() ?? ""; @@ -328,7 +328,7 @@ public class ToolService return "Fehler: old_text nicht in der Datei gefunden. Exakter Match nötig."; fileContent = fileContent.Replace(oldText, newText); File.WriteAllText(path, fileContent); - return $"Datei bearbeitet: {Path.GetRelativePath(workspace, path)}"; + return $"Datei bearbeitet: {WorkspaceHelper.GetRelativePath(workspace, path)}"; } case "list_files": { @@ -336,12 +336,12 @@ public class ToolService return "Fehler: Kein Workspace konfiguriert"; var path = ResolvePath(input.TryGetProperty("path", out var p) ? p.GetString() ?? "" : "", workspace); if (!Directory.Exists(path)) - return $"Fehler: Verzeichnis nicht gefunden: {Path.GetRelativePath(workspace, path)}"; + return $"Fehler: Verzeichnis nicht gefunden: {WorkspaceHelper.GetRelativePath(workspace, path)}"; var entries = new List(); foreach (var dir in Directory.GetDirectories(path)) - entries.Add(Path.GetRelativePath(workspace, dir) + "/"); + entries.Add(WorkspaceHelper.GetRelativePath(workspace, dir) + "/"); foreach (var file in Directory.GetFiles(path)) - entries.Add(Path.GetRelativePath(workspace, file)); + entries.Add(WorkspaceHelper.GetRelativePath(workspace, file)); return entries.Count == 0 ? "(leer)" : string.Join("\n", entries); } case "delete_file": @@ -352,14 +352,14 @@ public class ToolService if (File.Exists(path)) { File.Delete(path); - return $"Datei gelöscht: {Path.GetRelativePath(workspace, path)}"; + return $"Datei gelöscht: {WorkspaceHelper.GetRelativePath(workspace, path)}"; } if (Directory.Exists(path)) { Directory.Delete(path, recursive: false); - return $"Verzeichnis gelöscht: {Path.GetRelativePath(workspace, path)}"; + return $"Verzeichnis gelöscht: {WorkspaceHelper.GetRelativePath(workspace, path)}"; } - return $"Fehler: Nicht gefunden: {Path.GetRelativePath(workspace, path)}"; + return $"Fehler: Nicht gefunden: {WorkspaceHelper.GetRelativePath(workspace, path)}"; } case "execute": { @@ -368,18 +368,23 @@ public class ToolService var command = input.GetProperty("command").GetString() ?? ""; var psi = new ProcessStartInfo { - FileName = "/bin/bash", + FileName = WorkspaceHelper.ShellExecutable, WorkingDirectory = workspace, - RedirectStandardInput = true, + RedirectStandardInput = !WorkspaceHelper.IsWindows, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; + if (WorkspaceHelper.IsWindows) + psi.Arguments = WorkspaceHelper.ShellArguments(command); using var proc = Process.Start(psi); if (proc == null) return "Fehler: Prozess konnte nicht gestartet werden"; - await proc.StandardInput.WriteAsync(command); - proc.StandardInput.Close(); + if (!WorkspaceHelper.IsWindows) + { + await proc.StandardInput.WriteAsync(command); + proc.StandardInput.Close(); + } // Timeout (180s) UND externen CancellationToken verknüpfen using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); cts.CancelAfter(TimeSpan.FromSeconds(180)); @@ -423,7 +428,7 @@ public class ToolService var dirInfo = new DirectoryInfoWrapper(new DirectoryInfo(searchRoot)); var result = matcher.Execute(dirInfo); var matches = result.Files - .Select(f => Path.GetRelativePath(workspace, Path.Combine(searchRoot, f.Path))) + .Select(f => WorkspaceHelper.GetRelativePath(workspace, Path.Combine(searchRoot, f.Path))) .OrderBy(f => f) .ToList(); return matches.Count == 0 ? "(keine Treffer)" : string.Join("\n", matches); @@ -459,7 +464,7 @@ public class ToolService for (var i = 0; i < lines.Length; i++) { if (regex.IsMatch(lines[i])) - results.Add($"{Path.GetRelativePath(workspace, file)}:{i + 1}: {lines[i].Trim()}"); + results.Add($"{WorkspaceHelper.GetRelativePath(workspace, file)}:{i + 1}: {lines[i].Trim()}"); } } if (results.Count == 0) return "(keine Treffer)"; @@ -521,7 +526,7 @@ public class ToolService private string ResolvePath(string relativePath, string workspace) { var full = Path.GetFullPath(Path.Combine(workspace, relativePath ?? "")); - if (!full.StartsWith(workspace)) + if (!WorkspaceHelper.IsSubPathOf(full, workspace)) throw new Exception("Zugriff außerhalb des Workspace nicht erlaubt"); return full; } diff --git a/Services/WorkspaceHelper.cs b/Services/WorkspaceHelper.cs index ca801ab..cd70ec8 100644 --- a/Services/WorkspaceHelper.cs +++ b/Services/WorkspaceHelper.cs @@ -1,11 +1,53 @@ using System; using System.IO; +using System.Runtime.InteropServices; using Gott.Models; namespace Gott.Services; public static class WorkspaceHelper { + /// True wenn die Plattform Windows ist. + internal static bool IsWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + + /// Plattformunabhängiges Temp-Verzeichnis ohne Trailing-Separator. + internal static string TempDir => Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar); + + /// + /// Prüft ob ein Pfad ein Sub-Pfad eines Basis-Pfads ist (oder gleich). + /// Auf Windows case-insensitive, auf Linux case-sensitive. + /// + internal static bool IsSubPathOf(string path, string basePath) + { + var comparison = IsWindows ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; + var normalizedPath = Path.GetFullPath(path); + var normalizedBase = Path.GetFullPath(basePath); + return normalizedPath.Equals(normalizedBase, comparison) + || normalizedPath.StartsWith(normalizedBase + Path.DirectorySeparatorChar, comparison); + } + + /// + /// Gibt den relativen Pfad zurück – immer mit Forward-Slashes (für Agent-Output/API). + /// + internal static string GetRelativePath(string relativeTo, string path) + { + var rel = Path.GetRelativePath(relativeTo, path); + return IsWindows ? rel.Replace('\\', '/') : rel; + } + + /// + /// Gibt den Shell-Dateinamen zurück (cmd.exe auf Windows, /bin/bash auf Linux). + /// + internal static string ShellExecutable => IsWindows ? "cmd.exe" : "/bin/bash"; + + /// + /// Gibt die Shell-Argumente zurück um einen Befehl auszuführen. + /// Windows: /c "command", Linux: via stdin. + /// + internal static string ShellArguments(string command) + => IsWindows ? $"/c \"{command.Replace("\"", "\\\"")}\"" : ""; + + /// /// Löst den Workspace-Pfad einer Session auf. /// session.Workspace ist relativ zum user.DefaultPath. @@ -30,8 +72,7 @@ public static class WorkspaceHelper 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) + if (!IsSubPathOf(resolved, Path.GetFullPath(basePath))) return null; return resolved; @@ -47,7 +88,7 @@ public static class WorkspaceHelper if (string.IsNullOrWhiteSpace(workspace)) return null; - var trimmed = workspace.Trim().TrimStart('/').TrimEnd('/'); + var trimmed = workspace.Trim().TrimStart('/', '\\').TrimEnd('/', '\\'); if (string.IsNullOrEmpty(trimmed)) return null; @@ -76,11 +117,12 @@ public static class WorkspaceHelper var normalizedBase = Path.GetFullPath(defaultPath.Trim()); var normalizedWorkspace = Path.GetFullPath(absoluteWorkspace.Trim()); + var comparison = IsWindows ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; - if (normalizedWorkspace.StartsWith(normalizedBase + Path.DirectorySeparatorChar)) + if (normalizedWorkspace.StartsWith(normalizedBase + Path.DirectorySeparatorChar, comparison)) return normalizedWorkspace.Substring(normalizedBase.Length + 1); - if (normalizedWorkspace == normalizedBase) + if (normalizedWorkspace.Equals(normalizedBase, comparison)) return ""; return absoluteWorkspace; // Außerhalb des DefaultPath → absoluten Pfad beibehalten (Legacy) diff --git a/wwwroot/js/file-explorer.js b/wwwroot/js/file-explorer.js index 3a8612a..1518d20 100644 --- a/wwwroot/js/file-explorer.js +++ b/wwwroot/js/file-explorer.js @@ -535,7 +535,7 @@ function openFolderBrowser(opts = {}) { _feState._keyHandler = (e) => { if (e.key === 'Escape') closeFileExplorer(); }; document.addEventListener('keydown', _feState._keyHandler); - fbLoadDir(opts.startPath || '/'); + fbLoadDir(opts.startPath || (serverIsWindows ? '' : '/')); } async function fbLoadDir(absPath) { @@ -581,7 +581,7 @@ async function fbLoadDir(absPath) { const currentNorm = data.current.replace(/\/+$/, ''); const atBase = _feState.basePath && currentNorm === _feState.basePath; - const atRoot = currentNorm === '' || currentNorm === '/'; + const atRoot = currentNorm === '' || currentNorm === '/' || (serverIsWindows && /^[A-Z]:$/i.test(currentNorm)); document.getElementById('feBack').disabled = atBase || (atRoot && _feState.history.length === 0); } catch (e) { sidebar.innerHTML = '
Netzwerk-Fehler
'; @@ -605,7 +605,14 @@ function fbGoBack() { fbLoadDir(prev); } else { // Kein History-Eintrag → zum Parent navigieren - const parent = currentNorm.includes('/') ? currentNorm.substring(0, currentNorm.lastIndexOf('/')) || '/' : '/'; + let parent; + if (serverIsWindows && /^[A-Z]:$/i.test(currentNorm)) { + parent = ''; // Laufwerk → zurück zur Laufwerksauswahl + } else if (serverIsWindows && /^[A-Z]:\/[^/]*$/i.test(currentNorm)) { + parent = currentNorm.substring(0, 2); // z.B. C:/Users → C: + } else { + parent = currentNorm.includes('/') ? currentNorm.substring(0, currentNorm.lastIndexOf('/')) || (serverIsWindows ? '' : '/') : (serverIsWindows ? '' : '/'); + } if (parent !== currentNorm) { fbLoadDir(parent); } diff --git a/wwwroot/js/title.js b/wwwroot/js/title.js index ebceff5..198fc9f 100644 --- a/wwwroot/js/title.js +++ b/wwwroot/js/title.js @@ -1,7 +1,9 @@ let buildTime = 0; +let serverIsWindows = false; fetch('/api/info').then(r => r.json()).then(data => { buildTime = data.buildTime || 0; + serverIsWindows = !!data.isWindows; updateTitle(); setInterval(updateTitle, 1000); });