PoPunkouterSoftware is a .NET 10 Blazor Web App with a server host, a WebAssembly client, and operational endpoints for the Azure inventory report, the app portfolio, and diagnostics.
src/PoPunkouterSoftware.API/: ASP.NET Core host + BFF. Feature slices underFeatures/<Area>/(Config, Diag, Portfolio, SignIns); host-level plumbing that belongs to no slice lives inHost/. Hosts the Blazor WASM shell.src/PoPunkouterSoftware.Client/: WASM UI, layouts, pages, and static assets.src/PoPunkouterSoftware.Shared/: contracts and models shared between the host and client.src/PoPunkouterSoftware.Infrastructure/: Azure and integration adapters.tests/PoPunkouterSoftware.Unit/: xUnit, strictly no-I/O.tests/PoPunkouterSoftware.Integration/:WebApplicationFactory+ Azurite Testcontainers.tests/PoPunkouterSoftware.E2EAPI/: pure-HTTP contract tests against a live instance.tests/PoPunkouterSoftware.E2EUI/: C# Playwright browser tests (run on demand against a live instance).SCRIPTS/: setup and automation helpers.
Prerequisites:
- .NET SDK from
global.json - Node.js for Playwright tests
- Docker Desktop for Azurite-backed integration scenarios
Common commands:
dotnet build .\PoPunkouterSoftware.sln
dotnet test .\tests\PoPunkouterSoftware.Unit
dotnet test .\tests\PoPunkouterSoftware.IntegrationAvailable VS Code tasks in this workspace include:
buildtest-unitstart-azuritestart-api-serverdeep-clean(removes everybin/andobj/;dotnet cleanleavesobj/behind)
The app runs locally on HTTP port 8000.
/healthis the deep probe (Key Vault, Table Storage);/health/liveis a static liveness ping./api/diag/*(summary, report, refresh, cancel-refresh, restart/{name}, ai) expose diagnostics;/diagitself was removed 2026-09-04 — diagnostics surface in the/azureAdvanced diagnostics drawer.restart/{name}restarts (or starts) one scanned App Service and sits behind the same management gate asrefresh; in Azure it also needs Website Contributor, which infra does not grant.- Each scan also reads Azure Resource Health (for down apps), Azure Advisor, 24h exception types per app from the shared Application Insights component (
SignIns:ResourceId), and open Dependabot alerts per matched repo (needs Dependabot enabled on the repo and a PAT with "Dependabot alerts: read"). - Four keyless public APIs feed the scan through the
public-apiHttpClient: each matched repo'sDirectory.Packages.props(read with the GitHub PAT) is checked against nuget.org for newer versions and OSV.dev for known advisories; the Azure Retail Prices API prices B1/S1/P0v3 in each app's region; RDAP reads the registration expiry of any custom domain. Each catalog entry inapps.jsonmay setexpect, text the live page must contain; a page that answers 2xx without it is flagged. Each entry may also carry astackblock of short labels (frontend,backend,realtime,data,extras= the app's unique tech,ai,signIn,delivery) plus adetailsmap of the same keys holding the full wording, and anaiServiceslist (cloud,name,use,optional) for the AI column. An entry may also setandroidPackage; the nightly scan checks anonymously whether its Google Play page and itsgithubRepoare public, and the home page links each only once it is — the home page's matrix of how each app is built. - Push alerts go through ntfy (
Alerts:NtfyTopic): app down/up, failed scan, spend spike, an SSL certificate crossing 14 days, the month-end forecast crossing the budget, a page losing itsexpecttext, a custom domain crossing 30 days to expiry.weekly-digest.ymlsends a Monday summary to the same topic.deploy.yml(rollback) andscreenshots.yml(Lighthouse drop of 15+ points) send to the same topic via the optionalNTFY_TOPICGitHub secret, falling back to the Key Vault secret. deploy.ymlbacks up the live site, smoke-tests/health/live,/healthand/api/diag/summaryafter deploying, and redeploys the backup if the smoke test fails.- Azure behavior prefers Key Vault and managed identity when available, with local fallback configuration for development.
- The client dashboard consumes the shared Azure report models generated by the server and infrastructure layers.
/settingsholds every visitor preference — theme, density, sound and details — inlocalStorage, each defaulting to the simplest option.
appsettings.json holds values only; what each section means lives here. Secrets come from Key
Vault kv-poshared2 under the prefix PoPunkouterSoftware-- and are never committed.
| Key | What it does |
|---|---|
AzureBlobStorage:Endpoint |
Public endpoint of the account holding the app-screenshots container, used on the managed-identity path. screenshots.yml is the only writer; the app only reads. Without it stored screenshots never serve, which looks exactly like none were ever captured. Azurite wins in Development. |
ApplicationInsights:ConnectionString |
Left blank; supplied by the vault secret ApplicationInsights--ConnectionString or APPLICATIONINSIGHTS_CONNECTION_STRING. |
FeatureFlags:EnableManagementActions |
On everywhere so uptime-scan.yml can POST /api/diag/refresh. Production still demands Security:ManagementApiKey, and /api/config reports false there, so no visitor sees a button that would 401. |
FeatureFlags:EnableAutoInventoryRefresh |
A portfolio load with a report older than 12h starts a background rescan (30-min cooldown). Defaults on outside Development/Testing. |
FeatureFlags:EnableAiSummary |
The AI status paragraph on /azure; false falls back to the rule-based narrative. |
StatusNarrator:* |
Azure AI Foundry chat completions on po-aiservices-shared2 (gpt-5.4-nano). Deliberately NOT named AzureAi: the vault's estate-wide AzureAI--* secrets would be picked up case-insensitively and 401 every call. Blank ApiKey means Entra ID (needs Cognitive Services OpenAI User), falling back to the vault's AzureOpenAI--ApiKey. |
SignIns:* |
The /users roster, read live from the shared Application Insights component's traces (UserSignedIn). Needs Monitoring Reader; anything missing degrades to "unavailable", never a 500. WindowDays is capped at 90 (the component's retention). |
Budget:MonthlyUsd |
Monthly ceiling for the /azure forecast. 0 or absent shows the projection with no comparison. |
Alerts:* |
ntfy push alerts. Off while NtfyTopic is blank; on ntfy.sh the topic IS the credential, so set it only through the vault secret PoPunkouterSoftware--Alerts--NtfyTopic. SiteUrl is where a tapped alert opens. |
RateLimits:* |
Global (not per-IP) ceilings on /api/signins and POST /api/diag/ai, the two anonymous endpoints that cost money per call. |
Retention:HistoryDays |
Days before history rows are pruned (0 disables). Blob lifecycle is in infra/main.bicep. |
Pinger:* |
Background sweep that warms cold starts and records daily reachability for the uptime grid. Off means the grid uses scan observations alone. |
Security:ManagementApiKey |
Required in Production when management actions are on — the filter fails closed without it. Vault secret PoPunkouterSoftware--Security--ManagementApiKey, same value as the MANAGEMENT_API_KEY GitHub secret. |
- Unit tests (
PoPunkouterSoftware.Unit) cover pure logic and API helpers, no I/O. - Integration tests (
PoPunkouterSoftware.Integration) cover host configuration, diagnostics endpoints, and Azure Table Storage behavior viaWebApplicationFactory+ Testcontainers Azurite. - E2E tests (
PoPunkouterSoftware.E2EAPI,PoPunkouterSoftware.E2EUI) cover the HTTP contract and browser flows against a live instance; on demand, not in CI. E2EUI runs axe-core's WCAG 2.1 A/AA rules over every page (Deque.AxeCore.Playwright) and fails on serious or critical violations. - Each tier has a hard test budget — 100 Unit, 50 Integration, 25 E2E-API, 25 E2E-UI. Adding a test means removing one; prefer widening an existing test's assertions.
- Read
CLAUDE.mdbefore broad changes — it is the single context map. (AGENT.MDwas merged into it and deleted;AGENTS.mdonly points there.) - Keep
/api/diag/*available but do not link them from the UI — diagnostics surface only in the/azureAdvanced diagnostics drawer. - Preserve Po-prefixed naming and strict build quality defaults from
Directory.Build.props.