using Elternbeirat.Contracts; using Elternbeirat.PocketBase; using Elternbeirat.Web.Components; using Elternbeirat.Web.Features.Events; using Microsoft.AspNetCore.HttpOverrides; var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorComponents(); // The typed client reads content from PocketBase over its REST API. The base URL // comes from config: appsettings for local dev, the PocketBase__BaseUrl env var // on the server (see compose.yaml). var pocketBaseUrl = builder.Configuration["PocketBase:BaseUrl"] ?? throw new InvalidOperationException("PocketBase:BaseUrl is not configured."); builder.Services.AddHttpClient(client => { client.BaseAddress = new Uri(pocketBaseUrl); // Every page waits for PocketBase. The default wait is 100 seconds, so if // PocketBase is down the page would hang that long. 5 seconds fails fast instead. client.Timeout = TimeSpan.FromSeconds(5); }); var app = builder.Build(); // NPM sits in front of the app and handles HTTPS. Without this line the app would // think every request is plain HTTP and would see NPM's IP, not the visitor's. // KnownNetworks/KnownProxies are left empty on purpose: only NPM reaches the app. app.UseForwardedHeaders( new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto }); if (app.Environment.IsDevelopment() is false) { app.UseExceptionHandler("/Error", createScopeForErrors: true); // No UseHsts() and no UseHttpsRedirection() here: NPM already handles HTTPS. // Adding them behind NPM would send the browser into a redirect loop. } // Don't add UseStatusCodePagesWithReExecute here. Blazor already shows its own // not-found page. That middleware would catch *every* error code, so when PocketBase // is down (a 503) it would wrongly show "not found" instead of our own message. app.UseAntiforgery(); app.MapStaticAssets(); app.MapRazorComponents(); // Calendar feed people can subscribe to. It returns calendar data, not a web page, // so it is a plain endpoint, not a Razor page. If PocketBase is down it returns an // empty calendar rather than an error. app.MapGet("/events.ics", async (PocketBaseClient pocketBase, CancellationToken token) => { IReadOnlyList events; try { events = await pocketBase.GetEventsAsync(token); } catch (PocketBaseUnavailableException) { events = []; } return Results.Text(IcsCalendar.Build(events), "text/calendar; charset=utf-8"); }); // Status page for an outside monitor (Uptime Kuma) to check. One check tells all // three states apart: no reply = the app is down, 503 = the app runs but PocketBase // is down, 200 = both are fine. It talks to PocketBase directly, not through the // page-rendering code, so it can still report when that code is the thing failing. app.MapGet("/health", async (PocketBaseClient pocketBase, CancellationToken token) => await pocketBase.IsHealthyAsync(token) ? Results.Text("healthy", "text/plain; charset=utf-8") : Results.Text( "PocketBase unreachable", "text/plain; charset=utf-8", statusCode: StatusCodes.Status503ServiceUnavailable)); app.Run();