Commit Graph

3 Commits

Author SHA1 Message Date
Oliver Kieselbach 18b28260a7 feat: portal static export — eliminate the SWA SSR runtime (cold-start root fix)
output:'export' + trailingSlash: the SWA now serves plain static files; the
managed SSR runtime — the portal's last cold-start source ("Backend call
failure" when a cold start exceeded SWA's 45s limit) — no longer exists.

- Detail routes are query-string based (/sessions?id=, /sessions/inspector?id=,
  /diagnosis?id=, /apps/detail?name=, /admin/backups/detail?id=,
  /admin/customs-archive/detail?tenantId=&rowKey=). Single-definition builders
  in lib/routes.ts; all 21 link sites migrated, duplicate sessionHref helpers
  consolidated. Suspense boundaries added everywhere useSearchParams drives
  page identity (hard export-build errors otherwise).
- 9 [section] families prerender via server shells (generateStaticParams from
  the *NavSections registries, dynamicParams=false, client bodies extracted to
  SectionClient) — 37 pages; server redirect() index pages became
  ClientRedirect shells. robots/sitemap/og-image routes force-static.
- Legacy PATH-shaped deep links are preserved FOREVER (sent Teams/Slack/
  webhook notifications cannot be edited): SWA wildcard rewrites serve the
  family page and the globally mounted LegacyPathRedirect rewrites to the
  canonical query URL, preserving query + #event hashes (mapping pinned by
  lib/__tests__/legacyPathRedirect.test.ts).
- middleware.ts deleted → client-side HostRoutingGuard (prefix list moved to
  lib/hostRouting.ts). Legacy /go/[code] route + bootstrapValidation deleted —
  BootstrapSessions table verified free of active pre-cutover sessions.
- staticwebapp.config.json now carries production routing: 19 docs/marketing
  redirects (order-sensitive), legacy rewrites (exact before wildcard), 404
  override, all security headers incl. literalized prod CSP (no unsafe-eval),
  Content-Type routes for the extension-less og images. Guarded by the new
  utils/__tests__/swaConfig.guard.test.ts. Maintenance mode is now a
  first-routes-entry insert, never a whole-file swap (README rewritten).
- Backend notification deep links go through Constants.PortalSessionUrl
  (query shape, escaped, pinned); 3 producers migrated.
- deploy-web.yml uploads out/ with is_static_export.

Export build: 79 static pages, no go/ output. Web: tsc clean, vitest 53
files green (618 incl. new guards; 2 deleted legacy suites). Backend:
solution build clean, 3622/3622 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 01:44:32 +02:00
Oliver Kieselbach 6cbd507b4f fix: client-IP extraction was dead on Flex — RemoteIpAddress fallback + AFD-aware rate-limit key
Post-deploy verification of the go endpoint showed every anonymous caller
keyed as "unknown": Flex Consumption does not surface X-Forwarded-For to
the isolated worker, so ALL per-IP rate-limit buckets (bootstrap-validate,
distress, diag-ticket-download, stats/platform) had silently collapsed
into one shared bucket — confirmed by DistressReports.SourceIp being
"unknown" across the board.

- GetTrustedClientIp falls back to HttpContext.Connection.RemoteIpAddress
  (transport-derived, unspoofable; IPv4-mapped addresses normalized).
  Heals every existing caller without signature changes.
- New GetRateLimitClientIp for AFD-fronted endpoints: honors
  X-Azure-ClientIP only when X-Azure-FDID carries exactly our profile ID
  (FrontDoorProfileId app setting, set on autopilotmonitor-api-eu);
  fail-closed to GetTrustedClientIp on unset/mismatch/multi-value.
  Used by GetBootstrapScriptFunction (traffic arrives via
  go.autopilotmonitor.com, where the trusted hop is AFD egress).
- FdidMatches table tests incl. append/spoof cases.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 00:01:31 +02:00
Oliver Kieselbach 26bcab9054 feat: move OOBE bootstrap script endpoint into the backend (go.autopilotmonitor.com)
The portal's Next.js /go/[code] route was the web app's only request-time
server code and blocked the planned static export. The script generation
now lives in the Function App as GET /api/bootstrap/go/{code}
(GetBootstrapScriptFunction), reached by customers as
irm https://go.autopilotmonitor.com/{code} | iex via a Front Door custom
domain whose /* route rewrites onto the API path.

- New Constants.BootstrapGoBaseUrl; CreateBootstrapSessionFunction now
  hands out the go URL (previously unguarded — CreateBootstrapSessionUrlShapeTests
  pins it now). go host added to both URL-guard enforced lists.
- BootstrapScriptValueValidator ports the bootstrapValidation.ts injection
  invariants (exact GUIDs, https + host allowlist + /agent/*.zip path,
  expiry <= 14d) as defense-in-depth for in-process values.
- OobeBootstrapScriptGenerator ports the PS template as a Replace-token
  raw string; golden test pins byte parity with the TS template
  (TestData/oobe-bootstrap-golden.ps1, captured from route.ts).
- Always-HTTP-200 text/plain contract incl. rate limit (own bucket
  bootstrap-script:{ip} 20/min); disabled tenant indistinguishable from
  unknown code. Catalog: PublicAnonymous; NoStoreCacheMiddleware covers
  /api/bootstrap/go/ (script inlines the bearer token).
- Web: BOOTSTRAP_GO_URL registry entry, sessions list shows the go host,
  legacy route marked LEGACY (alive for pre-migration URLs <= 168h,
  removal in the static-export PR).
- Docs: new OKF backend/oobe-bootstrap-go-flow.md, url-registry go
  section, index/log entries.

Backend tests 3611/3611, web vitest 651/651, tsc clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 21:42:56 +02:00