From e9f88b788965332a285d4cf14bc8821785f6de97 Mon Sep 17 00:00:00 2001 From: Gulum Date: Fri, 5 Jun 2026 23:57:31 +0200 Subject: [PATCH] ARCH-2b: add frontend (Vite) as Aspire resource with LAN wiring - AppHost: AddViteApp(frontend) via Aspire.Hosting.JavaScript 13.4.2, WithReference(webapi)+WaitFor, inject VITE_API_BASE_URL=http://:5179 at launch (computed from the active physical adapter, follows DHCP) - Pin proxyless ports: webapi http=5179 (matches SPA default), vite=5173, both bind 0.0.0.0 for LAN reachability - vite.config: server.host=true so the dev server is LAN-visible Co-Authored-By: Claude Opus 4.8 (1M context) --- GerbilManager.AppHost/AppHost.cs | 65 ++++++++++++++++++- .../GerbilManager.AppHost.csproj | 1 + gerbil-manager-web/vite.config.ts | 6 ++ 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/GerbilManager.AppHost/AppHost.cs b/GerbilManager.AppHost/AppHost.cs index 3f842d4..5a9eea6 100644 --- a/GerbilManager.AppHost/AppHost.cs +++ b/GerbilManager.AppHost/AppHost.cs @@ -1,3 +1,7 @@ +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; + var builder = DistributedApplication.CreateBuilder(args); var postgres = builder.AddPostgres("postgres") @@ -5,8 +9,65 @@ var postgres = builder.AddPostgres("postgres") var gerbilDb = postgres.AddDatabase("gerbilmanager"); -builder.AddProject("webapi") +// 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); + .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) + .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}"; + }); 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"; +} diff --git a/GerbilManager.AppHost/GerbilManager.AppHost.csproj b/GerbilManager.AppHost/GerbilManager.AppHost.csproj index 475f60d..96d6a2b 100644 --- a/GerbilManager.AppHost/GerbilManager.AppHost.csproj +++ b/GerbilManager.AppHost/GerbilManager.AppHost.csproj @@ -9,6 +9,7 @@ + diff --git a/gerbil-manager-web/vite.config.ts b/gerbil-manager-web/vite.config.ts index 8b0f57b..167f24a 100644 --- a/gerbil-manager-web/vite.config.ts +++ b/gerbil-manager-web/vite.config.ts @@ -4,4 +4,10 @@ import react from '@vitejs/plugin-react' // https://vite.dev/config/ export default defineConfig({ plugins: [react()], + server: { + // Bind on all interfaces so the dev server is reachable from other devices + // on the LAN (phone/laptop) when launched via .NET Aspire. Aspire assigns + // the port; this only widens the host from localhost to 0.0.0.0. + host: true, + }, })