using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; var builder = DistributedApplication.CreateBuilder(args); // WEB-2: lokales Verzeichnis für die veröffentlichte öffentliche Seite. Die API schreibt // nach /live (POST /api/publish), der publicsite-nginx serviert genau dieses live/ // — dieselbe Form wie in Produktion (deploy/truenas), nur lokal zum Testen. var publicSiteRoot = Path.GetFullPath(Path.Combine(builder.AppHostDirectory, "..", "deploy", ".local-publicsite")); Directory.CreateDirectory(Path.Combine(publicSiteRoot, "live")); // leeres live/, damit nginx sofort startet var publicSiteConf = Path.GetFullPath(Path.Combine(builder.AppHostDirectory, "..", "deploy", "truenas", "nginx", "publicsite.conf")); var postgres = builder.AddPostgres("postgres") .WithDataVolume(); var gerbilDb = postgres.AddDatabase("gerbilmanager"); // Stable, LAN-facing HTTP port for the API (matches the SPA's default base URL). const int ApiHttpPort = 5179; var webapi = builder.AddProject("webapi") .WithReference(gerbilDb) .WaitFor(gerbilDb) // Pin the port so the phone-facing URL doesn't change between launches. // Proxyless so the app binds the port directly — Program.cs then widens the // host to 0.0.0.0 for LAN access (the DCP proxy binds localhost only). .WithEndpoint("http", e => { e.Port = ApiHttpPort; e.IsProxied = false; }, createIfNotExists: false) // WEB-2: Publish-Ziel der öffentlichen Seite (sonst gibt POST /api/publish 503). .WithEnvironment("PublicSite__RootPath", publicSiteRoot) .WithExternalHttpEndpoints(); // The machine's LAN IP — the SPA runs in the phone's browser, so it must reach // the API at this address, not "localhost" (which would be the phone itself). var lanIp = GetLanIpAddress(); builder.AddViteApp("frontend", "../gerbil-manager-web") .WithReference(webapi) .WaitFor(webapi) // Pin a stable port for the phone bookmark (Vite default 5173). Proxyless so // the Vite dev server binds the port directly; vite.config host:true makes it // bind 0.0.0.0 for LAN access. .WithEndpoint("http", e => { e.Port = 5173; e.IsProxied = false; }, createIfNotExists: false) .WithExternalHttpEndpoints() .WithEnvironment(context => { // VITE_API_BASE_URL is read by the Vite dev server at launch (see // src/api/client.ts). Point it at the API's LAN-reachable URL. context.EnvironmentVariables["VITE_API_BASE_URL"] = $"http://{lanIp}:{ApiHttpPort}"; }); // WEB-2: publicsite-nginx — serviert die veröffentlichte öffentliche Seite lokal (gleiche // nginx-Config wie in Produktion). Liest das live/-Verzeichnis aus dem Publish-Output der API. // Test: in der App veröffentlichen (oder POST /api/publish), dann http://localhost:8081 öffnen. builder.AddContainer("publicsite", "nginx", "alpine") .WithBindMount(publicSiteRoot, "/usr/share/nginx/html", isReadOnly: true) .WithBindMount(publicSiteConf, "/etc/nginx/conf.d/default.conf", isReadOnly: true) .WithHttpEndpoint(port: 8081, targetPort: 80, name: "http") .WithExternalHttpEndpoints(); builder.Build().Run(); // Picks the IPv4 address of the active physical LAN adapter (Wi-Fi/Ethernet), // skipping loopback and virtual adapters (WSL/Hyper-V). Recomputed each launch // so it follows DHCP changes. static string GetLanIpAddress() { var candidates = NetworkInterface.GetAllNetworkInterfaces() .Where(ni => ni.OperationalStatus == OperationalStatus.Up) .Where(ni => ni.NetworkInterfaceType is NetworkInterfaceType.Wireless80211 or NetworkInterfaceType.Ethernet) .Where(ni => !ni.Description.Contains("Hyper-V", StringComparison.OrdinalIgnoreCase) && !ni.Description.Contains("Virtual", StringComparison.OrdinalIgnoreCase) && !ni.Name.Contains("WSL", StringComparison.OrdinalIgnoreCase) && !ni.Name.Contains("vEthernet", StringComparison.OrdinalIgnoreCase)) // Prefer adapters that have a default gateway (i.e. a real network path). .OrderByDescending(ni => ni.GetIPProperties().GatewayAddresses.Count > 0); foreach (var ni in candidates) { var ip = ni.GetIPProperties().UnicastAddresses .FirstOrDefault(a => a.Address.AddressFamily == AddressFamily.InterNetwork && !IPAddress.IsLoopback(a.Address)); if (ip is not null) { return ip.Address.ToString(); } } return "localhost"; }