diff --git a/.claude/backlog/001-create-backlog-in-github.md b/.claude/backlog/001-create-backlog-in-github.md index d05c381..cae5627 100644 --- a/.claude/backlog/001-create-backlog-in-github.md +++ b/.claude/backlog/001-create-backlog-in-github.md @@ -27,4 +27,4 @@ As a product owner, I want the backlog represented in GitHub so that work can be ## Notes / dependencies -- Use `docs/scripts/create-github-issues.ps1` to batch-create issues from backlog files via `gh` CLI. +- Use `scripts/create-github-issues.ps1` to batch-create issues from backlog files via `gh` CLI. diff --git a/.claude/skills/cck-testing/SKILL.md b/.claude/skills/cck-testing/SKILL.md index 337f9f3..05fd374 100644 --- a/.claude/skills/cck-testing/SKILL.md +++ b/.claude/skills/cck-testing/SKILL.md @@ -25,7 +25,6 @@ description: > tests/ AHKFlowApp.API.Tests/ # Integration tests — WebApplicationFactory + Testcontainers AHKFlowApp.Application.Tests/ # Validator unit tests + handler unit tests - AHKFlowApp.Domain.Tests/ # Domain entity logic unit tests AHKFlowApp.Infrastructure.Test/ # EF Core + migration tests with Testcontainers AHKFlowApp.UI.Blazor.Tests/ # Blazor component tests (bUnit) ``` diff --git a/.claude/skills/cck-verify/SKILL.md b/.claude/skills/cck-verify/SKILL.md index a5eaf61..cb6fe00 100644 --- a/.claude/skills/cck-verify/SKILL.md +++ b/.claude/skills/cck-verify/SKILL.md @@ -36,7 +36,6 @@ If tests fail: stop and fix before continuing. See the **build-fix** skill. ```bash dotnet test tests/AHKFlowApp.API.Tests --configuration Release --verbosity normal dotnet test tests/AHKFlowApp.Application.Tests --configuration Release --verbosity normal -dotnet test tests/AHKFlowApp.Domain.Tests --configuration Release --verbosity normal ``` ### Run a Single Test by Name diff --git a/AGENTS.md b/AGENTS.md index cb497b1..80954cd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ Blazor WebAssembly PWA frontend + ASP.NET Core Web API backend. Early stage — ## Tech Stack - **.NET 10.0** — all projects target `net10.0`; Microsoft.* packages use 10.x versions -- **EF Core** + SQL Server (LocalDB/Docker Compose/Azure SQL) with `EnableRetryOnFailure()` +- **EF Core** + SQL Server (Windows LocalDB/x64 Docker Compose/Azure SQL) with `EnableRetryOnFailure()` - **Blazor WebAssembly** PWA with MudBlazor 9.x and Azure AD (MSAL) authentication - **MediatR** (Jimmy Bogard) for CQRS — commands, queries, pipeline behaviors - **Ardalis.Result** for typed operation outcomes (handlers only) @@ -33,7 +33,6 @@ src/Frontend/ tests/ AHKFlowApp.API.Tests/ # API integration tests (WebApplicationFactory) AHKFlowApp.Application.Tests/ # Validator + service unit tests - AHKFlowApp.Domain.Tests/ # Domain logic unit tests AHKFlowApp.Infrastructure.Tests/ # Repository integration tests AHKFlowApp.UI.Blazor.Tests/ # Blazor component tests (bUnit) ``` @@ -54,12 +53,12 @@ dotnet test tests/AHKFlowApp.API.Tests --configuration Release --verbosity norma dotnet test tests/AHKFlowApp.API.Tests --filter "FullyQualifiedName~HealthControllerTests" # Run API locally (recommended: Docker SQL on port 1433) -dotnet run --project src/Backend/AHKFlowApp.API --launch-profile "https + Docker SQL (Recommended)" +dotnet run --project src/Backend/AHKFlowApp.API --launch-profile "Docker SQL (Recommended)" # Run Blazor frontend (separate terminal) dotnet run --project src/Frontend/AHKFlowApp.UI.Blazor -# Full stack via Docker Compose (SQL Server + API) +# Full stack via Docker Compose (API + SQL Server on x64/amd64 hosts) docker compose up --build # EF Core migrations @@ -214,7 +213,7 @@ GitHub Actions workflows in `.github/workflows/`: **Environments:** - **DEV:** Local development environment (`ASPNETCORE_ENVIRONMENT=Development`) - - LocalDB or Docker SQL Server + - Windows LocalDB or x64 Docker SQL Server - No Azure resources required - Run locally with `dotnet run` - **TEST:** Azure pre-production environment (`ASPNETCORE_ENVIRONMENT=Test`) diff --git a/AHKFlowApp.slnx b/AHKFlowApp.slnx index f1a2878..6b630df 100644 --- a/AHKFlowApp.slnx +++ b/AHKFlowApp.slnx @@ -12,7 +12,6 @@ - diff --git a/README.md b/README.md index f6d37f6..61b81fb 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,16 @@ ### Prerequisites - [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) -- SQL Server LocalDB (included with Visual Studio) or Docker +- One local database path: + - Windows + SQL Server LocalDB, or + - Docker on an x64/amd64 host for the bundled SQL Server compose stack +- Optional for Azure provisioning only: Windows PowerShell 5.1, Azure CLI, GitHub CLI + +> The bundled Docker stack is **not supported on Raspberry Pi / ARM64** because `docker-compose.yml` uses SQL Server 2022. ### Running Locally -**Option 1 — LocalDB:** +**Option 1 — Windows + LocalDB** ```bash # Apply migrations (after backlog item 007) @@ -18,13 +23,20 @@ dotnet ef database update \ --startup-project src/Backend/AHKFlowApp.API # Start API (http://localhost:5600, OpenAPI at /swagger/v1/swagger.json) -dotnet run --project src/Backend/AHKFlowApp.API --launch-profile "Docker SQL (Recommended)" +dotnet run --project src/Backend/AHKFlowApp.API --launch-profile "LocalDB SQL" # Start frontend in a separate terminal (http://localhost:5601) dotnet run --project src/Frontend/AHKFlowApp.UI.Blazor ``` -**Option 2 — Docker Compose (recommended):** +**Option 2 — Windows/x64 Docker SQL + local frontend** + +```bash +dotnet run --project src/Backend/AHKFlowApp.API --launch-profile "Docker SQL (Recommended)" +dotnet run --project src/Frontend/AHKFlowApp.UI.Blazor +``` + +**Option 3 — Docker Compose (API + SQL only, x64/amd64):** See `docs/development/docker-setup.md`. @@ -42,7 +54,7 @@ The application supports three distinct environments: | Environment | Description | ASPNETCORE_ENVIRONMENT | Deployment | |-------------|-------------|------------------------|------------| -| **DEV** | Local development | `Development` | Local machine (LocalDB or Docker SQL) | +| **DEV** | Local development | `Development` | Local machine (Windows LocalDB or x64 Docker SQL) | | **TEST** | Pre-production testing | `Test` | Azure (auto-deploy from `main` branch) | | **PROD** | Production | `Production` | Azure (manual deployment via workflow) | diff --git a/docs/deployment/entra-setup.md b/docs/deployment/entra-setup.md index a351639..559ce45 100644 --- a/docs/deployment/entra-setup.md +++ b/docs/deployment/entra-setup.md @@ -28,11 +28,10 @@ Handled automatically by the full provisioning script: Phase 3 sets up or updates the per-env app registration via `setup-entra-app.ps1` before Bicep runs. Later in the deployment, the script re-runs that setup only to refresh redirect URIs and write all three `AZURE_AD_*_{TEST|PROD}` GitHub Variables once the final app endpoints are known. The deploy workflows substitute these into `appsettings.{Test|Production}.json` at build time and inject them into App Service configuration on every deploy. -After `deploy.ps1` finishes, retrigger the API and frontend deploy workflows (they don't auto-run on `scripts/**` changes): +After `deploy.ps1` finishes, the script already dispatches `deploy-frontend.yml` for the selected environment. The API workflow still runs on the next push to `main`, or you can trigger it manually if you need to redeploy the current image: ```powershell gh workflow run deploy-api.yml --ref main -f environment=test -gh workflow run deploy-frontend.yml --ref main -f environment=test ``` ### Manual fallback diff --git a/docs/deployment/getting-started.md b/docs/deployment/getting-started.md index 599d7a5..87dca0f 100644 --- a/docs/deployment/getting-started.md +++ b/docs/deployment/getting-started.md @@ -2,7 +2,7 @@ This guide walks you through provisioning your own AHKFlowApp instance on Azure. -**Prerequisites:** An Azure subscription. The deploy script will guide you through any additional tooling. +**Prerequisites:** Azure subscription, Windows PowerShell 5.1 or newer, .NET 10 SDK, Azure CLI, and GitHub CLI. `sqlcmd` is optional. ## Quick Start (Recommended) @@ -16,17 +16,19 @@ cd AHKFlowApp The script will: -1. Check that required tools are installed (Azure CLI, GitHub CLI) +1. Check that required tools are installed (.NET 10 SDK, Azure CLI, GitHub CLI) 2. Prompt for your environment name (`test` or `prod`), Azure region, and GitHub repository 3. Create an Azure resource group and provision all resources via Bicep -4. Set up Entra ID (SQL access, OIDC for GitHub Actions) +4. Set up Entra ID (app registration, SQL access, OIDC for GitHub Actions) 5. Create the SQL database user for the application 6. Configure GitHub secrets and variables so CI/CD works automatically -7. Save your configuration to `scripts/.env.{environment}` for future use +7. Trigger the frontend deploy workflow on GitHub +8. Save your configuration to `scripts/.env.{environment}` for future use -When done, push to `main` — GitHub Actions will deploy the API container and Blazor frontend automatically. +When done, the script queues the frontend deployment automatically. -> **Auth app registration not included in `deploy.ps1`.** After provisioning, run the Entra app registration setup separately — see [Entra ID Setup](#entra-id-app-registration-setup) below. +- **TEST:** the API deploys when `deploy-api.yml` is triggered by a qualifying push to `main` (`src/Backend/**`, `tests/**`, `Directory.*.props`, `global.json`, or `.github/workflows/deploy-api.yml`). +- **PROD:** the API deploy is manual — run `deploy-api.yml` with `environment=prod`. ## What Gets Provisioned @@ -49,6 +51,8 @@ Estimated cost for the `test` environment: **~$15–25/month** (B1 App Service P The deploy script checks for these and will tell you how to install them if missing: +- **Windows PowerShell 5.1+** — included on supported Windows versions +- **.NET 10 SDK** — [install](https://dotnet.microsoft.com/download/dotnet/10.0) - **Azure CLI** — [install](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) - **GitHub CLI** (`gh`) — [install](https://cli.github.com) - **sqlcmd** (optional) — [install](https://learn.microsoft.com/en-us/sql/tools/sqlcmd/sqlcmd-utility). If absent, the script prints a SQL snippet to run in the Azure Portal instead. @@ -100,14 +104,14 @@ If you need to re-provision from GitHub Actions (e.g., after a Bicep change): ## Entra ID App Registration Setup -The `deploy.ps1` script provisions infrastructure but does **not** create the Entra ID app registration used for user authentication. Run this once per environment after provisioning: +`deploy.ps1` already creates or updates the Entra ID app registration for `test` and `prod`. Use `setup-entra-app.ps1` only as a manual fallback when you need to refresh redirect URIs or repair app-registration state without re-running the full provisioning flow: ```powershell .\scripts\setup-entra-app.ps1 -Environment test .\scripts\setup-entra-app.ps1 -Environment prod ``` -The script outputs the `ClientId`, `TenantId`, and `DefaultScope` values. Set them as GitHub Variables so the deploy workflows can inject them: +The script outputs the `ClientId`, `TenantId`, and `DefaultScope` values. If you run it manually, update the matching GitHub Variables so the deploy workflows can inject them: ```bash gh variable set AZURE_AD_TENANT_ID_TEST --body "" diff --git a/docs/development/docker-setup.md b/docs/development/docker-setup.md index 3969795..5ceaf7d 100644 --- a/docs/development/docker-setup.md +++ b/docs/development/docker-setup.md @@ -11,6 +11,10 @@ Access API at: http://localhost:5600/swagger SQL Server is available on: `localhost:1433` +> The bundled compose stack runs **API + SQL Server only**. Run the Blazor frontend separately with `dotnet run --project src/Frontend/AHKFlowApp.UI.Blazor`. +> +> The bundled compose stack is **x64/amd64 only**. It is not supported on Raspberry Pi / ARM64 because it uses `mcr.microsoft.com/mssql/server:2022-latest`. + ## Visual Studio Launch Profiles Launch profiles are defined in `src/Backend/AHKFlowApp.API/Properties/launchSettings.json`. diff --git a/docs/development/github-setup.md b/docs/development/github-setup.md index d00c89a..48d84fb 100644 --- a/docs/development/github-setup.md +++ b/docs/development/github-setup.md @@ -143,7 +143,7 @@ GitHub Projects v2 has limited built-in automation. Manage workflow manually: ### Batch Script -See [`docs/scripts/create-github-issues.ps1`](../scripts/create-github-issues.ps1) — reads backlog files from `.claude/backlog/`, detects type/epic/interface labels, and creates GitHub Issues via `gh` CLI. Supports dry-run (default) and `-Execute` flag. +See [`scripts/create-github-issues.ps1`](../../scripts/create-github-issues.ps1) — reads backlog files from `.claude/backlog/`, detects type/epic/interface labels, and creates GitHub Issues via `gh` CLI. Supports dry-run (default) and `-Execute` flag. ### Run diff --git a/docs/environments.md b/docs/environments.md index 1c7ce16..2aa69b7 100644 --- a/docs/environments.md +++ b/docs/environments.md @@ -2,289 +2,25 @@ ## Overview -AHKFlowApp supports three distinct environments with environment-specific configuration: +| Environment | ASPNETCORE_ENVIRONMENT | Hosting | Database | Deployment path | +|-------------|------------------------|---------|----------|-----------------| +| **DEV** | `Development` | Local machine | Windows LocalDB or x64 Docker SQL Server | Manual `dotnet run` / Docker Compose | +| **TEST** | `Test` | Azure | Azure SQL Database | Auto on push to `main` | +| **PROD** | `Production` | Azure | Azure SQL Database | Manual workflow dispatch | -| Environment | Name | ASPNETCORE_ENVIRONMENT | Location | Database | Deployment | -|-------------|------|------------------------|----------|----------|------------| -| **DEV** | Development | `Development` | Local machine | LocalDB or Docker SQL | Manual (`dotnet run`) | -| **TEST** | Test | `Test` | Azure | Azure SQL Database | Auto (push to `main`) | -| **PROD** | Production | `Production` | Azure | Azure SQL Database | Manual (workflow dispatch) | +## DEV -## DEV Environment (Local Development) +- **Windows LocalDB path:** `dotnet run --project src/Backend/AHKFlowApp.API --launch-profile "LocalDB SQL"` +- **Windows/x64 Docker SQL path:** `dotnet run --project src/Backend/AHKFlowApp.API --launch-profile "Docker SQL (Recommended)"` +- **Docker Compose path:** `docker compose up -d --build` starts **API + SQL Server only**; run the frontend separately with `dotnet run --project src/Frontend/AHKFlowApp.UI.Blazor` +- **Raspberry Pi / ARM64:** not supported for the bundled Docker stack because `docker-compose.yml` uses SQL Server 2022 -### Configuration +See [docs/development/docker-setup.md](development/docker-setup.md) and [docs/development/configuration-strategy.md](development/configuration-strategy.md). -- **ASPNETCORE_ENVIRONMENT**: `Development` -- **Database**: LocalDB (`(localdb)\mssqllocaldb`) or Docker SQL Server (port 1433) -- **Connection String**: Configured in `appsettings.Development.json` (not committed) or Docker Compose -- **CORS**: Allows `https://localhost:7601` and `http://localhost:5601` -- **Logging**: Debug level for application code, Information for EF Core queries +## TEST / PROD -### Running Locally +- Provision Azure resources with `.\scripts\deploy.ps1 -Environment test|prod` +- `deploy.ps1` creates or updates the Entra app registration, configures GitHub secrets/variables, and dispatches `deploy-frontend.yml` +- `deploy-api.yml` publishes the API container on push to `main` for **TEST** and on manual dispatch for **PROD** -**Option 1 — LocalDB:** -```bash -dotnet run --project src/Backend/AHKFlowApp.API --launch-profile https -``` - -**Option 2 — Docker SQL (Recommended):** -```bash -dotnet run --project src/Backend/AHKFlowApp.API --launch-profile "https + Docker SQL (Recommended)" -``` - -**Option 3 — Full Stack (Docker Compose):** -```bash -docker compose up --build -``` - -### URLs -- API: `http://localhost:5600` (single port for VS, docker-compose, Docker-only scenarios) -- Frontend: `http://localhost:5601` - -## TEST Environment (Azure Pre-Production) - -### Configuration - -- **ASPNETCORE_ENVIRONMENT**: `Test` (set via App Service application setting) -- **Database**: Azure SQL Database (`ahkflowapp-sql-test.database.windows.net`) -- **Authentication**: Entra ID (Managed Identity) — no SQL passwords -- **Connection String**: Set via App Service connection string configuration -- **CORS**: Configured to allow Static Web App URL -- **Logging**: Information level for application, Warning for framework - -### Azure Resources - -All resources are in the `rg-ahkflowapp-test` resource group: -- App Service: `ahkflowapp-api-test` -- SQL Server: `ahkflowapp-sql-test` -- SQL Database: `ahkflowapp-db` -- Static Web App: `ahkflowapp-swa-test` -- Key Vault: `ahkflowapp-kv-test` -- User-Assigned Managed Identity (deployer): `ahkflowapp-uami-deployer-test` -- User-Assigned Managed Identity (runtime): `ahkflowapp-uami-runtime-test` - -### Provisioning - -Run the Azure provisioning scripts with `ENVIRONMENT=test`: - -```bash -cd scripts/azure - -# Step 1: Verify prerequisites -cat 00-prerequisites.md - -# Step 2: Provision Azure resources -# Set ENVIRONMENT=test in 01-provision-azure.md and run commands - -# Step 3: Configure GitHub OIDC -# Set ENVIRONMENT=test in 02-configure-github-oidc.md and run commands -``` - -### Deployment - -**Automatic:** Push to `main` branch triggers `deploy-api.yml` and `deploy-frontend.yml` workflows. - -**Manual:** Use workflow dispatch in GitHub Actions. - -### GitHub Secrets & Variables - -**Secrets** (shared): -- `AZURE_TENANT_ID` -- `AZURE_SUBSCRIPTION_ID` -- `AZURE_CLIENT_ID_TEST` (deployer managed identity) -- `AZURE_STATIC_WEB_APPS_API_TOKEN_TEST` - -**Variables** (TEST-specific): -- `AZURE_RESOURCE_GROUP_TEST=rg-ahkflowapp-test` -- `APP_SERVICE_NAME_TEST=ahkflowapp-api-test` -- `SQL_SERVER_NAME_TEST=ahkflowapp-sql-test` -- `SQL_SERVER_FQDN_TEST=ahkflowapp-sql-test.database.windows.net` -- `SQL_DATABASE_NAME_TEST=ahkflowapp-db` - -### URLs -- API: `https://ahkflowapp-api-test.azurewebsites.net` -- API Health: `https://ahkflowapp-api-test.azurewebsites.net/health` -- Frontend: Get from `az staticwebapp show --name ahkflowapp-swa-test --query defaultHostname -o tsv` - -## PROD Environment (Azure Production) - -### Configuration - -- **ASPNETCORE_ENVIRONMENT**: `Production` (set via App Service application setting) -- **Database**: Azure SQL Database (`ahkflowapp-sql-prod.database.windows.net`) -- **Authentication**: Entra ID (Managed Identity) — no SQL passwords -- **Connection String**: Set via App Service connection string configuration -- **CORS**: Configured to allow Static Web App URL -- **Logging**: Warning level (minimal logging for performance) - -### Azure Resources - -All resources are in the `rg-ahkflowapp-prod` resource group: -- App Service: `ahkflowapp-api-prod` -- SQL Server: `ahkflowapp-sql-prod` -- SQL Database: `ahkflowapp-db` -- Static Web App: `ahkflowapp-swa-prod` -- Key Vault: `ahkflowapp-kv-prod` -- User-Assigned Managed Identity (deployer): `ahkflowapp-uami-deployer-prod` -- User-Assigned Managed Identity (runtime): `ahkflowapp-uami-runtime-prod` - -### Provisioning - -Run the Azure provisioning scripts with `ENVIRONMENT=prod`: - -```bash -cd scripts/azure - -# Step 1: Verify prerequisites -cat 00-prerequisites.md - -# Step 2: Provision Azure resources -# Set ENVIRONMENT=prod in 01-provision-azure.md and run commands - -# Step 3: Configure GitHub OIDC -# Set ENVIRONMENT=prod in 02-configure-github-oidc.md and run commands -``` - -### Deployment - -**Manual Only:** Use workflow dispatch in GitHub Actions: -1. Go to Actions → Deploy API (or Deploy Frontend) -2. Click "Run workflow" -3. Select environment: `prod` -4. Confirm and run - -### GitHub Secrets & Variables - -**Secrets** (shared): -- `AZURE_TENANT_ID` -- `AZURE_SUBSCRIPTION_ID` -- `AZURE_CLIENT_ID_PROD` (deployer managed identity) -- `AZURE_STATIC_WEB_APPS_API_TOKEN_PROD` - -**Variables** (PROD-specific): -- `AZURE_RESOURCE_GROUP_PROD=rg-ahkflowapp-prod` -- `APP_SERVICE_NAME_PROD=ahkflowapp-api-prod` -- `SQL_SERVER_NAME_PROD=ahkflowapp-sql-prod` -- `SQL_SERVER_FQDN_PROD=ahkflowapp-sql-prod.database.windows.net` -- `SQL_DATABASE_NAME_PROD=ahkflowapp-db` - -### URLs -- API: `https://ahkflowapp-api-prod.azurewebsites.net` -- API Health: `https://ahkflowapp-api-prod.azurewebsites.net/health` -- Frontend: Get from `az staticwebapp show --name ahkflowapp-swa-prod --query defaultHostname -o tsv` - -## Environment-Specific Configuration Files - -### API Backend - -``` -src/Backend/AHKFlowApp.API/ - appsettings.json # Base configuration (committed) - appsettings.Development.json # DEV overrides (committed, CORS only) - appsettings.Test.json # TEST overrides (committed) - appsettings.Production.json # PROD overrides (committed) -``` - -**Load order:** `appsettings.json` → `appsettings.{Environment}.json` → Environment variables → Azure App Configuration (future) - -### Frontend Blazor - -``` -src/Frontend/AHKFlowApp.UI.Blazor/wwwroot/ - appsettings.json # Base config (committed) - appsettings.Development.json # DEV overrides — localhost API (committed) - appsettings.Test.json # TEST overrides — TEST Azure API URL (committed) - appsettings.Production.json # PROD overrides — PROD Azure API URL (committed) -``` - -**Load order:** `appsettings.json` → `appsettings.{BlazorEnvironment}.json` - -The active environment is controlled by the `Blazor-Environment` HTTP header: -- TEST SWA: set to `Test` via `staticwebapp.config.json` -- PROD SWA: patched to `Production` by `deploy-frontend.yml` before publishing -- Local dev: set to `Development` automatically by the ASP.NET Core dev server - -No secrets are stored in frontend configuration — all values are public. - -## Switching Between Environments - -### Local Development → TEST -1. Ensure TEST environment is provisioned in Azure -2. Ensure GitHub secrets/variables are configured for TEST -3. Push to `main` branch or manually trigger workflow with environment=test - -### TEST → PROD -1. Provision PROD environment in Azure -2. Configure GitHub secrets/variables for PROD -3. Manually trigger Deploy API workflow with environment=prod -4. Manually trigger Deploy Frontend workflow with environment=prod - -## Environment Variable Reference - -### All Environments - -| Variable | DEV | TEST | PROD | -|----------|-----|------|------| -| `ASPNETCORE_ENVIRONMENT` | `Development` | `Test` | `Production` | -| `AZURE_CLIENT_ID` | (not set) | UAMI runtime client ID | UAMI runtime client ID | -| `WEBSITES_PORT` | (not set) | `8080` | `8080` | - -### Connection Strings - -**DEV:** -``` -Server=(localdb)\mssqllocaldb;Database=AHKFlowApp;Trusted_Connection=True;MultipleActiveResultSets=true -``` -or (Docker): -``` -Server=localhost,1433;Database=AHKFlowAppDb;User Id=sa;Password=AHKFlow_Dev!2026;TrustServerCertificate=True -``` - -**TEST/PROD (Azure, Entra ID auth):** -``` -Server=tcp:{SQL_SERVER_FQDN},1433;Database={SQL_DATABASE_NAME};Authentication=Active Directory Default;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30; -``` - -## Troubleshooting - -### DEV: Database connection fails -- Ensure SQL Server LocalDB is installed or Docker is running -- Run `dotnet ef database update` to apply migrations - -### TEST/PROD: Deployment fails -- Check GitHub secrets and variables are set correctly with `_TEST` or `_PROD` suffix -- Verify Azure UAMI has correct RBAC permissions -- Check SQL firewall rules allow GitHub Actions runner IP - -### TEST/PROD: API returns 500 errors -- Check App Service logs: `az webapp log tail --name {APP_SERVICE_NAME} --resource-group {RESOURCE_GROUP}` -- Verify `ASPNETCORE_ENVIRONMENT` is set correctly in App Service configuration -- Check Application Insights for exceptions - -### Environment not loading correct appsettings -- Verify `ASPNETCORE_ENVIRONMENT` environment variable is set -- Check file exists: `appsettings.{Environment}.json` -- ASP.NET Core is case-sensitive for environment names (use `Development`, `Test`, `Production`) - -## Security Considerations - -### DEV -- SQL password in `docker-compose.yml` is for local development only -- Never commit real secrets to `appsettings.Development.json` -- Use `dotnet user-secrets` for sensitive local configuration - -### TEST/PROD -- All secrets stored in Azure Key Vault or App Service configuration -- SQL authentication uses Entra ID only (no SQL logins) -- Managed identities eliminate long-lived secrets -- HTTPS enforced via HSTS -- Connection strings never committed to source control - -## Next Steps - -1. **Set up DEV:** Clone repo, run `dotnet run` or `docker compose up` -2. **Provision TEST:** Run `scripts/azure/01-provision-azure.md` with `ENVIRONMENT=test` -3. **Configure CI/CD for TEST:** Run `scripts/azure/02-configure-github-oidc.md` with `ENVIRONMENT=test` -4. **Deploy to TEST:** Push to `main` or manually trigger workflow -5. **Provision PROD:** Repeat steps 2-3 with `ENVIRONMENT=prod` -6. **Deploy to PROD:** Manually trigger workflow with `environment=prod` +See [docs/deployment/getting-started.md](deployment/getting-started.md) and [docs/deployment/entra-setup.md](deployment/entra-setup.md). diff --git a/docs/scripts/create-github-issues.ps1 b/docs/scripts/create-github-issues.ps1 deleted file mode 100644 index 5a2f781..0000000 --- a/docs/scripts/create-github-issues.ps1 +++ /dev/null @@ -1,108 +0,0 @@ -# Create GitHub Issues from Backlog -# Usage: -# .\scripts\create-github-issues.ps1 -BacklogPath "" # Dry run (default) -# .\scripts\create-github-issues.ps1 -BacklogPath "" -Execute # Create issues -# -# Example: -# .\create-github-issues.ps1 -BacklogPath "..\..\.claude\backlog" -# .\create-github-issues.ps1 -BacklogPath "..\..\.claude\backlog" -Execute - -param( - [switch]$Execute, - [Parameter(Mandatory=$true)] - [string]$BacklogPath -) - -# Epic name to label mapping -$epicMapping = @{ - "Backlog setup" = "epic: backlog setup" - "Initial project / solution" = "epic: initial project" - "Foundation" = "epic: foundation" - "Versioning" = "epic: versioning" - "Logging" = "epic: logging" - "Observability" = "epic: observability" - "CI/CD" = "epic: ci/cd" - "Authentication and authorization" = "epic: authentication" - "Hotstrings" = "epic: hotstrings" - "Hotkeys" = "epic: hotkeys" - "Profiles" = "epic: profiles" - "Script generation & download" = "epic: script generation" -} - -# Ensure a GitHub label exists, creating it if missing -function Ensure-Label { - param([string]$Name, [string]$Color, [string]$Description) - - $existing = gh label list --search $Name --json name | ConvertFrom-Json - if ($existing | Where-Object { $_.name -eq $Name }) { - return - } - - Write-Host " Creating label: $Name" -ForegroundColor DarkGray - if ($Execute) { - gh label create $Name --color $Color --description $Description 2>$null - } -} - -Write-Host "Ensuring labels exist..." -ForegroundColor Yellow - -foreach ($label in $epicMapping.Values) { - Ensure-Label -Name $label -Color "7B61FF" -Description "Epic grouping" -} -Ensure-Label -Name "api" -Color "0075ca" -Description "API layer" -Ensure-Label -Name "ui" -Color "e4e669" -Description "UI layer" -Ensure-Label -Name "cli" -Color "d93f0b" -Description "CLI layer" -Ensure-Label -Name "enhancement" -Color "a2eeef" -Description "New feature or request" - -Write-Host "" - -$backlogPath = $BacklogPath - -$files = Get-ChildItem -Path $backlogPath -Filter "*.md" | - Where-Object { $_.Name -ne "000-backlog-item-template.md" } | - Sort-Object Name - -foreach ($file in $files) { - $content = Get-Content $file.FullName -Raw - $firstLine = (Get-Content $file.FullName -First 1) -replace "^#\s*", "" - - $labels = @() - - # Type label - if ($content -match '\*\*Type\*\*:\s*Feature') { $labels += "enhancement" } - - # Epic label - if ($content -match '\*\*Epic\*\*:\s*(.+)') { - $epicName = $Matches[1].Trim() - if ($epicMapping.ContainsKey($epicName)) { - $labels += $epicMapping[$epicName] - } - } - - # Interface labels - if ($content -match '\*\*Interfaces\*\*:.*API') { $labels += "api" } - if ($content -match '\*\*Interfaces\*\*:.*UI') { $labels += "ui" } - if ($content -match '\*\*Interfaces\*\*:.*CLI') { $labels += "cli" } - - $labelArgs = ($labels | ForEach-Object { "--label `"$_`"" }) -join " " - - Write-Host "Creating: $firstLine" -ForegroundColor Cyan - Write-Host " Labels: $($labels -join ', ')" -ForegroundColor Gray - - $command = "gh issue create --title `"$firstLine`" --body-file `"$($file.FullName)`" $labelArgs" - - if ($Execute) { - Invoke-Expression $command - Write-Host " ✓ Created" -ForegroundColor Green - } - else { - Write-Host " [DRY RUN] $command" -ForegroundColor DarkGray - } -} - -if (-not $Execute) { - Write-Host "`nDry run complete. Run with -Execute to create issues." -ForegroundColor Yellow -} -else { - Write-Host "`nAll issues created!" -ForegroundColor Green -} diff --git a/docs/superpowers/specs/2026-04-21-code-simplification-design.md b/docs/superpowers/specs/2026-04-21-code-simplification-design.md new file mode 100644 index 0000000..57cb9b3 --- /dev/null +++ b/docs/superpowers/specs/2026-04-21-code-simplification-design.md @@ -0,0 +1,259 @@ +# Code simplification (design) + +## Context + +The codebase is still small, which makes this a good time to simplify it before early structure hardens into long-term maintenance cost. + +The current architecture is directionally sound: controller-based ASP.NET Core API, MediatR in the application layer, EF Core in infrastructure, Blazor WebAssembly on the frontend, and a healthy test surface across API, application, infrastructure, UI, and E2E layers. + +The risk is not that the codebase is already deeply overengineered. The risk is that small pockets of ceremony accumulate faster than domain complexity: + +- startup and composition code in `src\Backend\AHKFlowApp.API\Program.cs` is dense and mixes several concerns inline, +- some abstractions are very thin and may only rename framework concepts without creating a meaningful seam, +- the frontend API client layer contains repeated request/result plumbing, +- tests include helpers and fixtures that may be useful, but should be pruned where they obscure intent more than they improve reuse. + +This first code-focused spec should simplify code in `src\` and noisy supporting code in `tests\`, while preserving the boundaries that still clearly support testability, clarity, or architecture. + +## Goal + +Reduce low-value complexity in the codebase without destabilizing the architectural boundaries that are already serving the project well. + +Success means: + +1. Common code paths are easier to read end-to-end. +2. Thin abstractions that do not add real value are removed or collapsed. +3. Startup, HTTP client, and test helper code becomes shorter and more obvious. +4. Any patterns kept or introduced are lightweight and reduce code rather than multiply it. +5. Small structure changes inside `src\` and `tests\` are allowed when they improve discoverability, but broad repo layout work stays out of scope. + +Non-goals: + +- redesigning the project’s Clean Architecture baseline, +- changing deployment or documentation workflows in this spec, +- chasing coverage percentages as the main objective, +- rewriting stable code just to make it look different. + +## Options considered + +### Option 1 - Targeted simplification *(chosen)* + +Remove low-value indirection, simplify hotspots, and allow small internal structure cleanup while preserving the boundaries that still clearly help with testing or architecture. + +Pros: + +- Delivers meaningful simplification without destabilizing the whole codebase. +- Fits the current repo maturity: enough code exists to trim ceremony, but not so much that a broad rewrite is justified. +- Keeps the cleanup focused on readability, not ideology. + +Cons: + +- Requires judgment calls on which abstractions still pay for themselves. +- Some inconsistencies may remain temporarily if they are lower-value than the main hotspots. + +### Option 2 - Conservative cleanup only + +Focus mainly on naming, formatting, short methods, and tiny refactors while preserving nearly all current abstractions and structure. + +Pros: + +- Lowest change risk. +- Easy to review. + +Cons: + +- Would leave the main sources of friction in place. +- Likely to disappoint the stated goal of making the code more concise. + +### Option 3 - Aggressive abstraction collapse + +Collapse most wrappers, interfaces, and intermediate layers unless they are absolutely required at runtime. + +Pros: + +- Maximum immediate reduction in moving parts. + +Cons: + +- Too risky for an early codebase that is still settling. +- Likely to erase boundaries that still support testing and future feature growth. +- Creates needless churn before the scripts and docs passes. + +**Decision: Option 1.** + +## Design + +### Simplification principles + +This cleanup should follow a few strict principles: + +- Prefer deleting code over moving code when the behavior is redundant. +- Prefer one obvious flow over multiple small indirections. +- Keep abstractions only when they create a real boundary, not when they merely wrap a framework type. +- Keep explicit mappings and explicit behavior where they improve correctness or readability. +- Avoid introducing new patterns unless they remove duplication and make the code easier to explain. + +In short: concise, but not clever. + +### Cleanup category 1 - Startup and composition + +`Program.cs` is a likely simplification hotspot because it currently mixes: + +- bootstrap logging, +- conditional Application Insights setup, +- controller and ProblemDetails configuration, +- service registration, +- development-only database migration logic, +- request pipeline setup, +- development-only browser launch behavior. + +The goal is not to hide all of this behind many extension methods. The goal is to make the composition root easier to scan. + +The preferred direction is: + +- keep `Program.cs` as the composition root, +- extract only cohesive setup blocks that materially improve readability, +- keep behavior discoverable near startup rather than scattering it across many files. + +Good candidates are small, single-purpose setup methods or extensions for areas that already have a natural boundary, such as API/OpenAPI setup or development-only startup behavior. Poor candidates are wrappers that just move a few lines without clarifying anything. + +### Cleanup category 2 - Abstraction pruning + +The codebase should review interfaces and wrappers with a high bar: + +- keep them when they protect a meaningful application boundary, +- remove them when they only rename a concrete framework dependency without real substitution value. + +Likely review targets include thin abstractions such as development environment wrappers and other one-property or one-method interfaces that exist only to pass framework state through a layer. + +This does **not** mean removing all abstractions. Some seams are still justified: + +- current-user access is a real boundary between HTTP context and application logic, +- database access boundaries may still be justified if they meaningfully isolate the application layer from infrastructure details, +- service abstractions that materially simplify testing or keep layers clean should remain. + +The rule is selective pruning, not a purity exercise. + +### Cleanup category 3 - Frontend service simplification + +The Blazor client layer should aim for a small, easy-to-follow shape: + +- page code should call clear client methods, +- shared HTTP and error handling should not be duplicated, +- the number of service interfaces should stay proportional to the actual complexity. + +The likely target is the request/result plumbing around `AhkFlowAppApiHttpClient` and `HotstringsApiClient`. This area may benefit from a small shared helper or a modest facade pattern, but only if it removes duplication without obscuring status handling or error mapping. + +What to avoid: + +- adding a generic service hierarchy, +- introducing a large base class just to share a few lines, +- splitting the API client surface into many tiny interfaces without need. + +### Cleanup category 4 - Mapping and DTO friction + +The project should keep explicit mapping rather than introducing mapper libraries. + +However, explicit mapping should still feel lightweight. This cleanup should review: + +- duplicate DTO shapes between frontend and backend where the duplication adds maintenance noise, +- mapping helpers that are justified versus ones that are too trivial to carry their own indirection cost, +- places where small mapping or response-shape cleanup would make the code easier to follow. + +The goal is not necessarily to unify all DTOs across the app, but to reduce friction where duplication no longer pays for the separation. + +### Cleanup category 5 - Test code simplification + +Test code is part of the codebase and should also stay concise. + +This spec should allow pruning or simplifying: + +- builders that do not save meaningful repetition, +- fixtures that hide test setup more than they clarify it, +- helper layers that make tests harder to read than inline setup would. + +At the same time, this cleanup should preserve helpers that clearly improve integration and end-to-end testing, especially where infrastructure setup is expensive or repetitive. + +The desired outcome is tests that read more directly while retaining the support code that materially reduces boilerplate. + +### Cleanup category 6 - Small internal structure cleanup + +This spec may include small structure changes inside `src\` and `tests\` when they simplify the code: + +- moving a file to a more obvious folder, +- co-locating tightly related classes, +- removing folders that contain only one trivial concept if the nesting adds noise. + +This spec should not do broad repo-layout work. That remains a separate follow-on effort for scripts and documentation. + +## Likely first-pass targets + +Based on the current code shape, likely early targets include: + +- `src\Backend\AHKFlowApp.API\Program.cs` +- `src\Backend\AHKFlowApp.Application\Abstractions\IDevEnvironment.cs` +- `src\Backend\AHKFlowApp.API\DevEnvironment.cs` +- `src\Frontend\AHKFlowApp.UI.Blazor\Services\AhkFlowAppApiHttpClient.cs` +- `src\Frontend\AHKFlowApp.UI.Blazor\Services\HotstringsApiClient.cs` +- test helpers or builders that are only lightly used or add indirection without enough payoff + +These are review targets, not automatic deletions. Each should be kept, simplified, or removed based on whether it reduces cognitive load in practice. + +## Lightweight patterns allowed + +This cleanup may use a few lightweight patterns where they reduce code and improve clarity: + +- **composition root cleanup** for startup wiring, +- **small, cohesive extension methods** for setup blocks with a real boundary, +- **facade-style client surface** in the frontend if it collapses duplicate HTTP plumbing, +- **single source of truth helpers** where multiple code paths are currently manually repeating the same transformation or status mapping. + +Patterns that should be avoided in this pass: + +- repository pattern, +- mapper frameworks, +- generic base hierarchies, +- speculative abstractions for future features. + +## Risks and mitigations + +### Risk: oversimplifying useful seams + +Some abstractions may look thin but still protect an important architectural or testing boundary. + +Mitigation: require each abstraction review to answer a simple question: what concrete cost do we pay if this seam disappears? If the answer is meaningful, keep it. + +### Risk: replacing explicit code with hidden indirection + +Startup cleanup in particular can become worse if code is pushed into many extension methods that hide behavior. + +Mitigation: only extract cohesive blocks that make `Program.cs` easier to scan, and keep behavior discoverable. + +### Risk: test readability regresses + +Removing helpers too aggressively can lead to repeated setup noise. + +Mitigation: simplify helpers selectively and optimize for direct, readable tests rather than minimal helper count. + +## Testing and validation + +This spec is about simplification, not only appearance, so implementation should validate behavior rather than assume safe refactors. + +Validation should focus on: + +- existing build and test workflows continuing to pass, +- no user-visible API behavior changing unintentionally, +- frontend pages and API client behavior preserving current error handling semantics, +- simplified tests remaining readable and representative of real behavior. + +Coverage improvement may happen as a byproduct where tests need to be clarified or added around changed code, but coverage thresholds are not the primary driver of this spec. + +## Follow-on specs enabled by this design + +After this spec, the next logical specs are: + +1. Repo simplification and file layout. +2. Script standardization and PowerShell v5 compatibility. +3. Local install and deployment experience. +4. Azure deployment automation and preflight checks. +5. Minimal documentation cleanup and cross-surface alignment. diff --git a/docs/superpowers/specs/2026-04-21-repo-simplification-file-layout-design.md b/docs/superpowers/specs/2026-04-21-repo-simplification-file-layout-design.md new file mode 100644 index 0000000..303800d --- /dev/null +++ b/docs/superpowers/specs/2026-04-21-repo-simplification-file-layout-design.md @@ -0,0 +1,198 @@ +# Repo simplification and file layout (design) + +## Context + +The repository already has a reasonable top-level shape, but it has started to drift in a few ways that make it harder to understand and maintain: + +- Runnable scripts live mostly under `scripts\`, but `docs\scripts\create-github-issues.ps1` introduces a second script location. +- Some documentation still refers to removed or superseded paths such as `scripts\azure\...`, while current operational entrypoints use `scripts\deploy.ps1`, `scripts\update.ps1`, and related root-level scripts. +- The repo contains multiple instruction surfaces (`README.md`, `docs\`, `AGENTS.md`, `.github\`, `.claude\`) that describe overlapping workflows. They are useful, but they can drift if they do not point to the same canonical paths and commands. + +The goal of this first spec is to simplify the repository structure without broad behavioral changes. It should establish a clearer layout that later script and documentation cleanup work can build on. + +## Goal + +Make the repository easier to navigate by giving scripts, documentation, and tool-specific instructions one clear home each. + +Success means: + +1. Every runnable script lives under one canonical `scripts\` folder. +2. `docs\` contains documentation only, not operational scripts. +3. `README.md`, `docs\`, `AGENTS.md`, `.github\`, and `.claude\` reference the same canonical paths and workflow names. +4. Stale references to removed structures such as `scripts\azure\...` are either updated or explicitly marked as historical content in archived design material. + +Non-goals: + +- Redesigning the application architecture. +- Changing deployment behavior in this spec. +- Rewriting all documentation content in one pass. +- Introducing a heavy taxonomy of script subfolders before the script count justifies it. + +## Options considered + +### Option 1 - One flat canonical `scripts\` folder *(chosen)* + +Keep all runnable scripts in the existing root `scripts\` folder. Move stray scripts into it. Documentation points to those scripts instead of embedding or hosting them. + +Pros: + +- Smallest change from current reality. +- Easiest rule for contributors: "if it runs, it lives in `scripts\`." +- Lowest churn while the repo is still early-stage. + +Cons: + +- The folder may become busy later if script count grows significantly. + +### Option 2 - Canonical `scripts\` folder with immediate subfolders by purpose + +Reorganize now into `scripts\deploy\`, `scripts\dev\`, `scripts\tooling\`, and similar categories. + +Pros: + +- Better long-term scaling if the script inventory grows rapidly. +- Slightly clearer categorization at a glance. + +Cons: + +- Premature structure for the current repo size. +- Creates more rename churn now, with little immediate user value. + +### Option 3 - Keep split script locations but document ownership rules + +Allow both `scripts\` and `docs\scripts\` as long as the distinction is documented. + +Pros: + +- Minimal file movement. + +Cons: + +- Preserves the ambiguity that this cleanup is meant to remove. +- Makes discoverability and maintenance worse over time. + +**Decision: Option 1.** + +## Design + +### Repository ownership rules + +The repository should use these simple ownership boundaries: + +- `scripts\` - all runnable scripts, whether for deployment, local setup, maintenance, coverage, or tooling. +- `docs\` - human-readable guidance only. Documentation may describe scripts, but must not be the canonical home for them. +- `docs\deployment\` - deployment and environment setup guidance. +- `docs\development\` - contributor setup, local development, testing, and tooling guidance. +- `docs\architecture\` - enduring architecture and system-shape documentation. +- `docs\superpowers\` - AI-generated specs and plans; useful as project history, but not the primary end-user documentation surface. +- `.github\` - GitHub-specific automation, templates, and instructions only. +- `.claude\` - Claude-specific overlays, local tooling, and backlog artifacts only. + +This keeps the repo easy to explain: code in `src\` and `tests\`, scripts in `scripts\`, docs in `docs\`, platform-specific overlays in `.github\` and `.claude\`. + +### Canonical script rule + +Every runnable script should live under `scripts\`. + +That includes: + +- deployment and teardown helpers, +- local development helpers, +- setup/bootstrap scripts, +- maintenance/tooling helpers such as issue creation or coverage generation. + +`docs\scripts\` should be removed as an active location. The current `docs\scripts\create-github-issues.ps1` should move to `scripts\create-github-issues.ps1`, and all references should be updated to point there. + +This is a lightweight **single source of truth** pattern for operational entrypoints: one script location, one canonical path, many docs that reference it. + +### Documentation layering + +Documentation should be intentionally layered instead of duplicated: + +- `README.md` should stay short and answer: what this project is, what is required to run it locally, and where to go next. +- `docs\deployment\` should hold deployment-specific details, including Azure and local container-based deployment guidance. +- `docs\development\` should hold contributor workflows such as local setup, GitHub setup, testing, and coverage. +- `AGENTS.md` should remain contributor/agent guidance for coding conventions and repo rules, but should not become the only place where operational workflows are documented. + +When a process needs both concise guidance and deeper guidance, `README.md` should link to the detailed doc instead of repeating it. + +### Alignment rules for `.github`, `.claude`, and docs + +This spec includes layout and alignment rules now, while deferring broader content cleanup to a later documentation-focused spec. + +The alignment rule is: + +1. Operational paths and script names are canonical in the repo tree itself. +2. Human-facing docs (`README.md`, `docs\`) should reference those canonical paths directly. +3. Tool-specific instruction files (`AGENTS.md`, `.github\copilot-instructions.md`, `.claude\CLAUDE.md`, related backlog notes) should reference the same paths and should not invent alternate structure descriptions. +4. Historical design docs under `docs\superpowers\` may mention old structures, but current user-facing docs must not. + +This means current drift such as references to `scripts\azure\...` should be removed from live docs. Historical specs and plans may remain as historical artifacts unless they are actively misleading a current workflow. + +### What this spec changes vs what it leaves for later + +This spec should drive: + +- moving stray scripts into `scripts\`, +- removing active duplicate script locations, +- updating current docs and instruction surfaces to reflect one canonical layout, +- defining ownership boundaries for repo surfaces. + +This spec should not yet decide: + +- whether `scripts\` eventually needs `deploy\`, `dev\`, or `tooling\` subfolders, +- how Azure deployment scripts should be functionally redesigned, +- how local deployment should be packaged for Raspberry Pi or other Docker hosts, +- how preflight checks and prerequisite validation should work inside `deploy.ps1`. + +Those are valid next specs, but they depend on this simplified layout first. + +## Planned file-level outcomes + +Expected outcomes from implementing this spec: + +- Move `docs\scripts\create-github-issues.ps1` to `scripts\create-github-issues.ps1`. +- Update `docs\development\github-setup.md` and any related references to point at the canonical script path. +- Audit current user-facing docs for references to old `scripts\azure\...` structure and replace them with current supported workflows. +- Update `README.md`, `AGENTS.md`, `.github\` guidance, and `.claude\` guidance where needed so they describe the same layout. +- Leave historical `docs\superpowers\specs\` and `docs\superpowers\plans\` content intact unless a specific file is being reused as active guidance. + +## Risks and mitigations + +### Risk: over-structuring too early + +Adding many new folders now could make the repo look cleaner on paper while making it harder to remember where things go. + +Mitigation: keep the rule simple now - one `scripts\` folder - and only introduce subfolders later if script volume creates real pain. + +### Risk: hidden references to old paths + +Docs and instruction files may still reference outdated paths even after the obvious files are updated. + +Mitigation: include a targeted cross-reference pass across `README.md`, `docs\`, `AGENTS.md`, `.github\`, and `.claude\` as part of implementation. + +### Risk: historical specs confuse contributors + +Older design docs may still mention removed structures. + +Mitigation: treat `docs\superpowers\` as historical design record, not canonical runtime documentation, and avoid linking current onboarding docs to outdated historical specs. + +## Testing and validation + +This structural cleanup does not require new testing tools. + +Validation for implementation should focus on: + +- all moved scripts remaining invocable from their new canonical paths, +- all user-facing documentation pointing to valid paths, +- no active documentation continuing to describe deprecated folder layouts. + +## Follow-on specs enabled by this design + +After this spec, the next logical specs are: + +1. Script standardization and PowerShell v5 compatibility. +2. Local install and deployment experience, including Docker-first local hosting guidance. +3. Azure `deploy.ps1` improvements, including prerequisite checks and workflow triggering. +4. Minimal documentation cleanup and prerequisite clarity. +5. Cross-surface consistency audit for repo guidance and automation instructions. diff --git a/scripts/Common.ps1 b/scripts/Common.ps1 new file mode 100644 index 0000000..fa26e44 --- /dev/null +++ b/scripts/Common.ps1 @@ -0,0 +1,71 @@ +#Requires -Version 5.1 + +Set-StrictMode -Version Latest + +function Write-Step([string]$Message) +{ + Write-Host "`n==> $Message" -ForegroundColor Cyan +} + +function Write-Success([string]$Message) +{ + Write-Host " [OK] $Message" -ForegroundColor Green +} + +function Write-Warn([string]$Message) +{ + Write-Host " ! $Message" -ForegroundColor Yellow +} + +function Write-Fail([string]$Message) +{ + Write-Host "`n [FAIL] $Message" -ForegroundColor Red +} + +function Confirm-Command([string]$Name, [string]$InstallUrl) +{ + if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) + { + Write-Fail "$Name is not installed." + Write-Host " Install from: $InstallUrl" -ForegroundColor Yellow + exit 1 + } + + Write-Success "$Name found" +} + +function Assert-AzureLogin() +{ + $null = az account show 2>&1 + if ($LASTEXITCODE -ne 0) + { + Write-Fail "Not logged into Azure. Run: az login" + exit 1 + } + + Write-Success "Azure login verified" +} + +function Assert-GitHubAuth() +{ + $null = gh auth status 2>&1 + if ($LASTEXITCODE -ne 0) + { + Write-Fail "GitHub CLI not authenticated. Run: gh auth login" + exit 1 + } + + Write-Success "GitHub CLI authenticated" +} + +function Read-KeyValueFile([string]$Path) +{ + $config = @{} + + Get-Content $Path | Where-Object { $_ -match '^\s*[^#]' -and $_ -match '=' } | ForEach-Object { + $parts = $_ -split '=', 2 + $config[$parts[0].Trim()] = $parts[1].Trim() + } + + return $config +} diff --git a/scripts/create-github-issues.ps1 b/scripts/create-github-issues.ps1 new file mode 100644 index 0000000..912b260 --- /dev/null +++ b/scripts/create-github-issues.ps1 @@ -0,0 +1,160 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Creates GitHub issues from backlog markdown files. + +.DESCRIPTION + Reads backlog files from .claude\backlog, ensures the expected labels exist, + and creates GitHub issues via the gh CLI. Runs as a dry run by default. + +.PARAMETER Execute + Creates issues for real instead of printing the gh commands. + +.PARAMETER BacklogPath + Optional path to the backlog folder. Defaults to .claude\backlog at the repo root. + +.EXAMPLE + .\scripts\create-github-issues.ps1 + +.EXAMPLE + .\scripts\create-github-issues.ps1 -Execute +#> +[CmdletBinding()] +param( + [switch]$Execute, + [string]$BacklogPath +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +. (Join-Path $PSScriptRoot 'Common.ps1') + +$repoRoot = Split-Path $PSScriptRoot -Parent +$resolvedBacklogPath = if ($BacklogPath) +{ + $BacklogPath +} +else +{ + Join-Path $repoRoot '.claude\backlog' +} + +if (-not (Test-Path $resolvedBacklogPath)) +{ + Write-Fail "Backlog path not found: $resolvedBacklogPath" + exit 1 +} + +Confirm-Command 'gh' 'https://cli.github.com/' +Assert-GitHubAuth + +$epicMapping = @{ + 'Backlog setup' = 'epic: backlog setup' + 'Initial project / solution' = 'epic: initial project' + 'Foundation' = 'epic: foundation' + 'Versioning' = 'epic: versioning' + 'Logging' = 'epic: logging' + 'Observability' = 'epic: observability' + 'CI/CD' = 'epic: ci/cd' + 'Authentication and authorization' = 'epic: authentication' + 'Hotstrings' = 'epic: hotstrings' + 'Hotkeys' = 'epic: hotkeys' + 'Profiles' = 'epic: profiles' + 'Script generation & download' = 'epic: script generation' +} + +function Ensure-Label +{ + param( + [string]$Name, + [string]$Color, + [string]$Description + ) + + $existing = gh label list --search $Name --json name | ConvertFrom-Json + if ($existing | Where-Object { $_.name -eq $Name }) + { + return + } + + Write-Host " Creating label: $Name" -ForegroundColor DarkGray + if ($Execute) + { + gh label create $Name --color $Color --description $Description 2>$null | Out-Null + } +} + +Write-Step 'Ensuring labels exist...' + +foreach ($label in $epicMapping.Values) +{ + Ensure-Label -Name $label -Color '7B61FF' -Description 'Epic grouping' +} + +Ensure-Label -Name 'api' -Color '0075ca' -Description 'API layer' +Ensure-Label -Name 'ui' -Color 'e4e669' -Description 'UI layer' +Ensure-Label -Name 'cli' -Color 'd93f0b' -Description 'CLI layer' +Ensure-Label -Name 'enhancement' -Color 'a2eeef' -Description 'New feature or request' + +$files = Get-ChildItem -Path $resolvedBacklogPath -Filter '*.md' | + Where-Object { $_.Name -ne '000-backlog-item-template.md' } | + Sort-Object Name + +foreach ($file in $files) +{ + $content = Get-Content $file.FullName -Raw + $firstLine = ($content -split '\r?\n', 2)[0] -replace '^#\s*', '' + $labels = @() + + if ($content -match '\*\*Type\*\*:\s*Feature') + { + $labels += 'enhancement' + } + + if ($content -match '\*\*Epic\*\*:\s*(.+)') + { + $epicName = $Matches[1].Trim() + if ($epicMapping.ContainsKey($epicName)) + { + $labels += $epicMapping[$epicName] + } + } + + if ($content -match '\*\*Interfaces\*\*:.*API') { $labels += 'api' } + if ($content -match '\*\*Interfaces\*\*:.*UI') { $labels += 'ui' } + if ($content -match '\*\*Interfaces\*\*:.*CLI') { $labels += 'cli' } + + Write-Host "Creating: $firstLine" -ForegroundColor Cyan + Write-Host " Labels: $($labels -join ', ')" -ForegroundColor Gray + + $issueArgs = @('issue', 'create', '--title', $firstLine, '--body-file', $file.FullName) + foreach ($label in $labels) + { + $issueArgs += @('--label', $label) + } + + if ($Execute) + { + & gh @issueArgs + if ($LASTEXITCODE -ne 0) + { + Write-Fail "Failed to create issue: $firstLine" + exit 1 + } + + Write-Success "Created" + } + else + { + Write-Host " [DRY RUN] gh $($issueArgs -join ' ')" -ForegroundColor DarkGray + } +} + +if (-not $Execute) +{ + Write-Host "`nDry run complete. Run with -Execute to create issues." -ForegroundColor Yellow +} +else +{ + Write-Host "`nAll issues created!" -ForegroundColor Green +} diff --git a/scripts/deploy.ps1 b/scripts/deploy.ps1 index 4f37d39..2d9fad1 100644 --- a/scripts/deploy.ps1 +++ b/scripts/deploy.ps1 @@ -1,4 +1,5 @@ -<# +#Requires -Version 5.1 +<# .SYNOPSIS Provisions an AHKFlowApp Azure environment and configures CI/CD. @@ -26,47 +27,33 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' - -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- - -function Write-Step([string]$Message) { - Write-Host "`n==> $Message" -ForegroundColor Cyan -} - -function Write-Success([string]$Message) { - Write-Host " ✓ $Message" -ForegroundColor Green -} - -function Write-Warn([string]$Message) { - Write-Host " ! $Message" -ForegroundColor Yellow -} - -function Write-Fail([string]$Message) { - Write-Host "`n ✗ $Message" -ForegroundColor Red -} - -function Confirm-Command([string]$Name, [string]$InstallUrl) { - if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) { - Write-Fail "$Name is not installed." - Write-Host " Install from: $InstallUrl" -ForegroundColor Yellow - throw "Missing prerequisite: $Name" - } - Write-Success "$Name found" -} +. (Join-Path $PSScriptRoot 'Common.ps1') function Invoke-Az { - $output = az @args 2>&1 - if ($LASTEXITCODE -ne 0) { - throw "az $($args -join ' ') failed:`n$output" + $prevEap = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + try { + $output = az @args 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "az $($args -join ' ') failed:`n$output" + } + } finally { + $ErrorActionPreference = $prevEap } + return $output } function Invoke-Az-Json { - $raw = az @args --output json 2>&1 - if ($LASTEXITCODE -ne 0) { throw "az $($args -join ' ') failed:`n$raw" } + $prevEap = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + try { + $raw = az @args --output json 2>&1 + if ($LASTEXITCODE -ne 0) { throw "az $($args -join ' ') failed:`n$raw" } + } finally { + $ErrorActionPreference = $prevEap + } + return $raw | ConvertFrom-Json } @@ -100,6 +87,41 @@ function Try-Az-Json { } } +function Assert-DotNetSdkVersion([string]$RequiredMajorVersion) { + $installedSdks = dotnet --list-sdks 2>$null + if ($LASTEXITCODE -ne 0) { + Write-Fail 'Unable to determine installed .NET SDK versions.' + exit 1 + } + + $requiredPrefix = "$RequiredMajorVersion." + if (-not ($installedSdks | Where-Object { $_.StartsWith($requiredPrefix) })) { + Write-Fail ".NET SDK $RequiredMajorVersion.x is required." + Write-Host ' Install from: https://dotnet.microsoft.com/download/dotnet/10.0' -ForegroundColor Yellow + exit 1 + } + + Write-Success ".NET SDK $RequiredMajorVersion.x found" +} + +function Start-FrontendDeployment([string]$Repository, [string]$TargetEnvironment) { + Write-Step "Phase 8: Triggering frontend deployment workflow..." + + gh workflow run deploy-frontend.yml ` + --repo $Repository ` + --ref main ` + -f environment=$TargetEnvironment | Out-Null + + if ($LASTEXITCODE -ne 0) { + Write-Warn 'Could not trigger deploy-frontend.yml automatically.' + Write-Host " Run manually: gh workflow run deploy-frontend.yml --repo $Repository --ref main -f environment=$TargetEnvironment" -ForegroundColor Yellow + return $false + } + + Write-Success "Triggered deploy-frontend.yml for '$TargetEnvironment'" + return $true +} + # --------------------------------------------------------------------------- # Phase 1: Prerequisites # --------------------------------------------------------------------------- @@ -113,6 +135,7 @@ Write-Step "Phase 1: Checking prerequisites..." Confirm-Command 'az' 'https://learn.microsoft.com/cli/azure/install-azure-cli' Confirm-Command 'gh' 'https://cli.github.com/' Confirm-Command 'dotnet' 'https://dotnet.microsoft.com/download' +Assert-DotNetSdkVersion '10' # Verify az login try { @@ -120,16 +143,11 @@ try { Write-Success "Logged into Azure as $($account.user.name) (subscription: $($account.name))" } catch { Write-Fail "Not logged into Azure. Run: az login" - throw + Write-Host " Azure CLI account check failed. Resolve the Azure CLI issue above and rerun." -ForegroundColor Yellow + exit 1 } -# Verify gh auth -$ghStatus = gh auth status 2>&1 -if ($LASTEXITCODE -ne 0) { - Write-Fail "GitHub CLI not authenticated. Run: gh auth login" - throw "GitHub CLI not authenticated" -} -Write-Success "GitHub CLI authenticated" +Assert-GitHubAuth # Verify sqlcmd (optional — we fall back to portal instructions) $hasSqlcmd = [bool](Get-Command 'sqlcmd' -ErrorAction SilentlyContinue) @@ -635,7 +653,7 @@ az webapp cors add ` Write-Success "CORS configured for SWA frontend" # --------------------------------------------------------------------------- -# Phase 8: Save config + Summary +# Phase 8: Save config # --------------------------------------------------------------------------- Write-Step "Phase 8: Saving configuration..." @@ -668,6 +686,8 @@ APP_INSIGHTS_NAME=$AppInsightsName "@ | Set-Content -Path $EnvFileOut -Encoding UTF8 Write-Success "Config saved to scripts/.env.$Environment" +$frontendDeploymentTriggered = Start-FrontendDeployment -Repository $GitHubOrgRepo -TargetEnvironment $Environment + Write-Host "" Write-Host "==========================================================" -ForegroundColor Green Write-Host " AHKFlowApp ($Environment) -- Provisioning Complete!" -ForegroundColor Green @@ -678,8 +698,17 @@ Write-Host " Frontend : https://$SwaHostname" Write-Host " Resources : $ResourceGroup" Write-Host "" Write-Host " Next steps:" -ForegroundColor Cyan -Write-Host " 1. Push to 'main' to trigger GitHub Actions deploy" -Write-Host " 2. The first push will build and push the container image to GHCR." +if ($frontendDeploymentTriggered) { + Write-Host " 1. Frontend deploy workflow dispatched for '$Environment'." +} else { + Write-Host " 1. Trigger the frontend deploy workflow manually once ready." +} +if ($Environment -eq 'test') { + Write-Host " 2. Push qualifying backend changes to 'main' to build and publish the API container image." + Write-Host " Auto-trigger paths: src/Backend/**, tests/**, Directory.*.props, global.json, .github/workflows/deploy-api.yml" +} else { + Write-Host " 2. Trigger deploy-api.yml manually with environment=prod to build and publish the API container image." +} Write-Host "" Write-Host " IMPORTANT: GHCR packages are private by default." -ForegroundColor Yellow Write-Host " After the first deploy-api.yml run, make the container image public:" diff --git a/scripts/run-coverage.ps1 b/scripts/run-coverage.ps1 index 5e761b6..2fa307a 100644 --- a/scripts/run-coverage.ps1 +++ b/scripts/run-coverage.ps1 @@ -1,4 +1,4 @@ -#Requires -Version 7.0 +#Requires -Version 5.1 <# .SYNOPSIS Run tests with coverage locally and generate HTML + JSON summary matching CI. diff --git a/scripts/setup-dev-entra.ps1 b/scripts/setup-dev-entra.ps1 index f347ddd..7d67c50 100644 --- a/scripts/setup-dev-entra.ps1 +++ b/scripts/setup-dev-entra.ps1 @@ -1,4 +1,4 @@ -#Requires -Version 7 +#Requires -Version 5.1 <# .SYNOPSIS Configures local dev Entra ID app registration and wires local config. diff --git a/scripts/setup-entra-app.ps1 b/scripts/setup-entra-app.ps1 index 2363713..16d190b 100644 --- a/scripts/setup-entra-app.ps1 +++ b/scripts/setup-entra-app.ps1 @@ -1,4 +1,4 @@ -#Requires -Version 7 +#Requires -Version 5.1 <# .SYNOPSIS Idempotent Entra ID app registration setup for AHKFlowApp. diff --git a/scripts/teardown.ps1 b/scripts/teardown.ps1 index bf92aea..daa43c7 100644 --- a/scripts/teardown.ps1 +++ b/scripts/teardown.ps1 @@ -1,4 +1,5 @@ -<# +#Requires -Version 5.1 +<# .SYNOPSIS Tears down an AHKFlowApp Azure environment. @@ -23,11 +24,7 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' - -function Write-Step([string]$Message) { Write-Host "`n==> $Message" -ForegroundColor Cyan } -function Write-Success([string]$Message) { Write-Host " ✓ $Message" -ForegroundColor Green } -function Write-Warn([string]$Message) { Write-Host " ! $Message" -ForegroundColor Yellow } -function Write-Fail([string]$Message) { Write-Host "`n ✗ $Message" -ForegroundColor Red } +. (Join-Path $PSScriptRoot 'Common.ps1') # Load saved config if (-not $Environment) { @@ -42,11 +39,7 @@ if (-not (Test-Path $EnvFile)) { exit 0 } -$config = @{} -Get-Content $EnvFile | Where-Object { $_ -match '^\s*[^#]' -and $_ -match '=' } | ForEach-Object { - $parts = $_ -split '=', 2 - $config[$parts[0].Trim()] = $parts[1].Trim() -} +$config = Read-KeyValueFile $EnvFile $ResourceGroup = $config['RESOURCE_GROUP'] $SqlAdminGroup = $config['SQL_ADMIN_GROUP'] @@ -71,12 +64,7 @@ if ($confirm -ne $ResourceGroup) { exit 0 } -# Verify az login -$null = az account show 2>&1 -if ($LASTEXITCODE -ne 0) { - Write-Fail "Not logged into Azure. Run: az login" - exit 1 -} +Assert-AzureLogin # --------------------------------------------------------------------------- # 1. Delete Azure resource group diff --git a/scripts/update.ps1 b/scripts/update.ps1 index 83f2979..01722dd 100644 --- a/scripts/update.ps1 +++ b/scripts/update.ps1 @@ -1,4 +1,5 @@ -<# +#Requires -Version 5.1 +<# .SYNOPSIS Updates AHKFlowApp to the latest release by pulling the newest container image. @@ -21,10 +22,7 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' - -function Write-Step([string]$Message) { Write-Host "`n==> $Message" -ForegroundColor Cyan } -function Write-Success([string]$Message) { Write-Host " ✓ $Message" -ForegroundColor Green } -function Write-Fail([string]$Message) { Write-Host "`n ✗ $Message" -ForegroundColor Red } +. (Join-Path $PSScriptRoot 'Common.ps1') # Load saved config if (-not $Environment) { @@ -39,11 +37,7 @@ if (-not (Test-Path $EnvFile)) { exit 1 } -$config = @{} -Get-Content $EnvFile | Where-Object { $_ -match '^\s*[^#]' -and $_ -match '=' } | ForEach-Object { - $parts = $_ -split '=', 2 - $config[$parts[0].Trim()] = $parts[1].Trim() -} +$config = Read-KeyValueFile $EnvFile $ResourceGroup = $config['RESOURCE_GROUP'] $AppServiceName = $config['APP_SERVICE_NAME'] @@ -52,13 +46,7 @@ $AppHostname = $config['APP_SERVICE_HOSTNAME'] Write-Step "Updating AHKFlowApp ($Environment)..." -# Verify az login -$null = az account show 2>&1 -if ($LASTEXITCODE -ne 0) { - Write-Fail "Not logged into Azure. Run: az login" - exit 1 -} -Write-Success "Azure login verified" +Assert-AzureLogin # Fetch the latest image tag from GHCR $Owner = $GitHubOrgRepo -split '/' | Select-Object -First 1 diff --git a/src/Backend/AHKFlowApp.API/DevEnvironment.cs b/src/Backend/AHKFlowApp.API/DevEnvironment.cs deleted file mode 100644 index 20aa977..0000000 --- a/src/Backend/AHKFlowApp.API/DevEnvironment.cs +++ /dev/null @@ -1,5 +0,0 @@ -using AHKFlowApp.Application.Abstractions; - -namespace AHKFlowApp.API; - -internal sealed record DevEnvironment(bool IsDevelopment) : IDevEnvironment; diff --git a/src/Backend/AHKFlowApp.API/Extensions/ApiExtensions.cs b/src/Backend/AHKFlowApp.API/Extensions/ApiExtensions.cs index 561be4e..cb28234 100644 --- a/src/Backend/AHKFlowApp.API/Extensions/ApiExtensions.cs +++ b/src/Backend/AHKFlowApp.API/Extensions/ApiExtensions.cs @@ -71,4 +71,20 @@ internal static WebApplication UseSwaggerDocs(this WebApplication app) return app; } + + internal static WebApplication UseRootRedirect(this WebApplication app, string destination) + { + app.Use(async (context, next) => + { + if (context.Request.Path == "/") + { + context.Response.Redirect(destination); + return; + } + + await next(context); + }); + + return app; + } } diff --git a/src/Backend/AHKFlowApp.API/Program.cs b/src/Backend/AHKFlowApp.API/Program.cs index c8eda8f..28bcadc 100644 --- a/src/Backend/AHKFlowApp.API/Program.cs +++ b/src/Backend/AHKFlowApp.API/Program.cs @@ -31,29 +31,8 @@ WebApplicationBuilder builder = WebApplication.CreateBuilder(args); - // Application Insights — only when a connection string is configured (Test/Prod) string? appInsightsConnectionString = builder.Configuration["ApplicationInsights:ConnectionString"]; - if (!string.IsNullOrWhiteSpace(appInsightsConnectionString)) - { - builder.Services.AddApplicationInsightsTelemetry(options => - options.ConnectionString = appInsightsConnectionString); - } - - // Stage 2: Full logger configured from appsettings.json, with DI integration - builder.Services.AddSerilog((services, lc) => - { - lc.ReadFrom.Configuration(builder.Configuration) - .ReadFrom.Services(services) - .Enrich.FromLogContext() - .Enrich.WithProperty("Application", "AHKFlowApp.API"); - - // Route Serilog events to Application Insights when configured - if (!string.IsNullOrWhiteSpace(appInsightsConnectionString)) - { - TelemetryConfiguration telemetryConfig = services.GetRequiredService(); - lc.WriteTo.ApplicationInsights(telemetryConfig, TelemetryConverter.Traces); - } - }); + ConfigureObservability(builder, appInsightsConnectionString); // Start SQL Server in Docker if requested (for "https + Docker SQL" launch profile) if (builder.Environment.IsDevelopment() && @@ -62,76 +41,15 @@ DevDockerSqlServer.EnsureStarted(builder.Environment.ContentRootPath); } - builder.Services.AddProblemDetails(options => - options.CustomizeProblemDetails = ctx => - ctx.ProblemDetails.Extensions["traceId"] = ctx.HttpContext.TraceIdentifier); - - builder.Services.AddControllers() - .ConfigureApiBehaviorOptions(options => - options.InvalidModelStateResponseFactory = ctx => - { - var pd = new ValidationProblemDetails(ctx.ModelState) - { - Detail = "See the errors field for details.", - Instance = ctx.HttpContext.Request.Path, - Status = StatusCodes.Status422UnprocessableEntity, - Title = "One or more validation errors occurred." - }; - pd.Extensions["traceId"] = ctx.HttpContext.TraceIdentifier; - return new UnprocessableEntityObjectResult(pd) - { - ContentTypes = { "application/problem+json" } - }; - }); - - builder.Services.AddSingleton(TimeProvider.System); - builder.Services.AddSingleton(new DevEnvironment(builder.Environment.IsDevelopment())); - - if (builder.Environment.IsDevelopment()) - { - builder.Services.AddSwaggerDocs(); - } - builder.Services.AddApplication(); - builder.Services.AddInfrastructure(builder.Configuration); - builder.Services.AddHealthChecks() - .AddDbContextCheck( - name: "database", - failureStatus: HealthStatus.Unhealthy); - const string corsPolicyName = "AllowConfiguredOrigins"; string[] allowedOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get() ?? []; - builder.Services.AddConfiguredCors(allowedOrigins, corsPolicyName); - - builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")); - builder.Services.AddAuthorization(); - builder.Services.AddHttpContextAccessor(); - builder.Services.AddScoped(); + ConfigureServices(builder, allowedOrigins, corsPolicyName); WebApplication app = builder.Build(); - // Auto-apply migrations in Development (creates database if it doesn't exist) if (app.Environment.IsDevelopment()) { - await using AsyncServiceScope scope = app.Services.CreateAsyncScope(); - AppDbContext dbContext = scope.ServiceProvider.GetRequiredService(); - ILogger logger = scope.ServiceProvider.GetRequiredService>(); - try - { - logger.LogInformation("Applying database migrations..."); - await dbContext.Database.MigrateAsync(); - logger.LogInformation("Database migrations applied successfully."); - } - catch (SqlException ex) when (ex.Number == 1801) - { - // Database already exists (persisted Docker volume from a previous run) — migrations already applied - logger.LogInformation("Database already exists; skipping migration apply."); - } - catch (Exception ex) - { - logger.LogError(ex, "Error applying database migrations."); - throw; - } + await ApplyDevelopmentMigrationsAsync(app); } app.UseMiddleware(); @@ -159,27 +77,11 @@ if (app.Environment.IsDevelopment()) { app.UseSwaggerDocs(); - app.Use(async (context, next) => - { - if (context.Request.Path == "/") - { - context.Response.Redirect("/swagger"); - return; - } - await next(context); - }); + app.UseRootRedirect("/swagger"); } else { - app.Use(async (context, next) => - { - if (context.Request.Path == "/") - { - context.Response.Redirect("/health"); - return; - } - await next(context); - }); + app.UseRootRedirect("/health"); } if (allowedOrigins.Length > 0) @@ -225,3 +127,100 @@ { await Log.CloseAndFlushAsync(); } + +static void ConfigureObservability(WebApplicationBuilder builder, string? appInsightsConnectionString) +{ + if (!string.IsNullOrWhiteSpace(appInsightsConnectionString)) + { + builder.Services.AddApplicationInsightsTelemetry(options => + options.ConnectionString = appInsightsConnectionString); + } + + builder.Services.AddSerilog((services, lc) => + { + lc.ReadFrom.Configuration(builder.Configuration) + .ReadFrom.Services(services) + .Enrich.FromLogContext() + .Enrich.WithProperty("Application", "AHKFlowApp.API"); + + if (!string.IsNullOrWhiteSpace(appInsightsConnectionString)) + { + TelemetryConfiguration telemetryConfig = services.GetRequiredService(); + lc.WriteTo.ApplicationInsights(telemetryConfig, TelemetryConverter.Traces); + } + }); +} + +static void ConfigureServices(WebApplicationBuilder builder, string[] allowedOrigins, string corsPolicyName) +{ + builder.Services.AddProblemDetails(options => + options.CustomizeProblemDetails = ctx => + ctx.ProblemDetails.Extensions["traceId"] = ctx.HttpContext.TraceIdentifier); + + builder.Services.AddControllers() + .ConfigureApiBehaviorOptions(options => + options.InvalidModelStateResponseFactory = CreateValidationProblemResponse); + + builder.Services.AddSingleton(TimeProvider.System); + builder.Services.AddSingleton(new AppEnvironment(builder.Environment.IsDevelopment())); + + if (builder.Environment.IsDevelopment()) + { + builder.Services.AddSwaggerDocs(); + } + + builder.Services.AddApplication(); + builder.Services.AddInfrastructure(builder.Configuration); + builder.Services.AddHealthChecks() + .AddDbContextCheck( + name: "database", + failureStatus: HealthStatus.Unhealthy); + + builder.Services.AddConfiguredCors(allowedOrigins, corsPolicyName); + builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")); + builder.Services.AddAuthorization(); + builder.Services.AddHttpContextAccessor(); + builder.Services.AddScoped(); +} + +static IActionResult CreateValidationProblemResponse(ActionContext ctx) +{ + var pd = new ValidationProblemDetails(ctx.ModelState) + { + Detail = "See the errors field for details.", + Instance = ctx.HttpContext.Request.Path, + Status = StatusCodes.Status422UnprocessableEntity, + Title = "One or more validation errors occurred." + }; + + pd.Extensions["traceId"] = ctx.HttpContext.TraceIdentifier; + + return new UnprocessableEntityObjectResult(pd) + { + ContentTypes = { "application/problem+json" } + }; +} + +static async Task ApplyDevelopmentMigrationsAsync(WebApplication app) +{ + await using AsyncServiceScope scope = app.Services.CreateAsyncScope(); + AppDbContext dbContext = scope.ServiceProvider.GetRequiredService(); + ILogger logger = scope.ServiceProvider.GetRequiredService>(); + + try + { + logger.LogInformation("Applying database migrations..."); + await dbContext.Database.MigrateAsync(); + logger.LogInformation("Database migrations applied successfully."); + } + catch (SqlException ex) when (ex.Number == 1801) + { + logger.LogInformation("Database already exists; skipping migration apply."); + } + catch (Exception ex) + { + logger.LogError(ex, "Error applying database migrations."); + throw; + } +} diff --git a/src/Backend/AHKFlowApp.Application/Abstractions/IDevEnvironment.cs b/src/Backend/AHKFlowApp.Application/Abstractions/IDevEnvironment.cs deleted file mode 100644 index f44084c..0000000 --- a/src/Backend/AHKFlowApp.Application/Abstractions/IDevEnvironment.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace AHKFlowApp.Application.Abstractions; - -public interface IDevEnvironment -{ - bool IsDevelopment { get; } -} diff --git a/src/Backend/AHKFlowApp.Application/AppEnvironment.cs b/src/Backend/AHKFlowApp.Application/AppEnvironment.cs new file mode 100644 index 0000000..000172a --- /dev/null +++ b/src/Backend/AHKFlowApp.Application/AppEnvironment.cs @@ -0,0 +1,3 @@ +namespace AHKFlowApp.Application; + +public sealed record AppEnvironment(bool IsDevelopment); diff --git a/src/Backend/AHKFlowApp.Application/Commands/Dev/SeedHotstringsCommand.cs b/src/Backend/AHKFlowApp.Application/Commands/Dev/SeedHotstringsCommand.cs index 3aa1b08..1e04eb1 100644 --- a/src/Backend/AHKFlowApp.Application/Commands/Dev/SeedHotstringsCommand.cs +++ b/src/Backend/AHKFlowApp.Application/Commands/Dev/SeedHotstringsCommand.cs @@ -1,3 +1,4 @@ +using AHKFlowApp.Application; using AHKFlowApp.Application.Abstractions; using AHKFlowApp.Application.DTOs; using AHKFlowApp.Application.Mapping; @@ -15,7 +16,7 @@ internal sealed class SeedHotstringsCommandHandler( IAppDbContext db, ICurrentUser currentUser, TimeProvider clock, - IDevEnvironment env) + AppEnvironment env) : IRequestHandler>> { private static readonly (string Trigger, string Replacement, bool Ending, bool InsideWord)[] s_samples = diff --git a/src/Frontend/AHKFlowApp.UI.Blazor/Program.cs b/src/Frontend/AHKFlowApp.UI.Blazor/Program.cs index 623a80a..5e929e8 100644 --- a/src/Frontend/AHKFlowApp.UI.Blazor/Program.cs +++ b/src/Frontend/AHKFlowApp.UI.Blazor/Program.cs @@ -22,18 +22,10 @@ builder.Services.AddScoped(); // No bearer token needed — backend TestAuthHandler authenticates synthetically - builder.Services.AddHttpClient(client => - { - client.BaseAddress = new Uri(new Uri(builder.HostEnvironment.BaseAddress), apiBaseUrl); - client.Timeout = TimeSpan.FromSeconds(30); - }) + AddApiClient(new Uri(new Uri(builder.HostEnvironment.BaseAddress), apiBaseUrl)) .AddStandardResilienceHandler(); - builder.Services.AddHttpClient(client => - { - client.BaseAddress = new Uri(new Uri(builder.HostEnvironment.BaseAddress), apiBaseUrl); - client.Timeout = TimeSpan.FromSeconds(30); - }) + AddApiClient(new Uri(new Uri(builder.HostEnvironment.BaseAddress), apiBaseUrl)) .AddStandardResilienceHandler(); } else @@ -64,21 +56,24 @@ builder.Services.AddTransient(); - builder.Services.AddHttpClient(client => - { - client.BaseAddress = new Uri(apiBaseUrl); - client.Timeout = TimeSpan.FromSeconds(30); - }) + AddApiClient(new Uri(apiBaseUrl)) .AddHttpMessageHandler() .AddStandardResilienceHandler(); - builder.Services.AddHttpClient(client => - { - client.BaseAddress = new Uri(apiBaseUrl); - client.Timeout = TimeSpan.FromSeconds(30); - }) + AddApiClient(new Uri(apiBaseUrl)) .AddHttpMessageHandler() .AddStandardResilienceHandler(); } await builder.Build().RunAsync(); + +IHttpClientBuilder AddApiClient(Uri baseAddress) + where TClient : class + where TImplementation : class, TClient +{ + return builder.Services.AddHttpClient(client => + { + client.BaseAddress = baseAddress; + client.Timeout = TimeSpan.FromSeconds(30); + }); +} diff --git a/tests/AHKFlowApp.Application.Tests/Hotstrings/SeedHotstringsCommandHandlerTests.cs b/tests/AHKFlowApp.Application.Tests/Hotstrings/SeedHotstringsCommandHandlerTests.cs index 91628b1..19bdce5 100644 --- a/tests/AHKFlowApp.Application.Tests/Hotstrings/SeedHotstringsCommandHandlerTests.cs +++ b/tests/AHKFlowApp.Application.Tests/Hotstrings/SeedHotstringsCommandHandlerTests.cs @@ -1,3 +1,4 @@ +using AHKFlowApp.Application; using AHKFlowApp.Application.Abstractions; using AHKFlowApp.Application.Commands.Dev; using AHKFlowApp.Application.DTOs; @@ -6,7 +7,6 @@ using Ardalis.Result; using FluentAssertions; using Microsoft.EntityFrameworkCore; -using NSubstitute; using Xunit; namespace AHKFlowApp.Application.Tests.Hotstrings; @@ -14,12 +14,7 @@ namespace AHKFlowApp.Application.Tests.Hotstrings; [Collection("HotstringDb")] public sealed class SeedHotstringsCommandHandlerTests(HotstringDbFixture fx) { - private static IDevEnvironment DevEnv(bool isDev) - { - IDevEnvironment e = Substitute.For(); - e.IsDevelopment.Returns(isDev); - return e; - } + private static AppEnvironment DevEnv(bool isDev) => new(isDev); [Fact] public async Task Handle_InDevelopment_SeedsSamples() diff --git a/tests/AHKFlowApp.Domain.Tests/AHKFlowApp.Domain.Tests.csproj b/tests/AHKFlowApp.Domain.Tests/AHKFlowApp.Domain.Tests.csproj deleted file mode 100644 index 3dbf262..0000000 --- a/tests/AHKFlowApp.Domain.Tests/AHKFlowApp.Domain.Tests.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - false - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - diff --git a/tests/AHKFlowApp.TestUtilities/AHKFlowApp.TestUtilities.csproj b/tests/AHKFlowApp.TestUtilities/AHKFlowApp.TestUtilities.csproj index 39ff913..42be094 100644 --- a/tests/AHKFlowApp.TestUtilities/AHKFlowApp.TestUtilities.csproj +++ b/tests/AHKFlowApp.TestUtilities/AHKFlowApp.TestUtilities.csproj @@ -2,11 +2,13 @@ false false + true +