──────
Build and Push Docker Images / detect-changes (push) Successful in 11s
Build and Push Docker Images / build-auth-api (push) Successful in 1m0s
Build and Push Docker Images / build-employee-api (push) Successful in 1m2s
Build and Push Docker Images / build-transit-api (push) Successful in 1m1s
Build and Push Docker Images / build-error-log-api (push) Successful in 1m5s
Build and Push Docker Images / build-admin-ui (push) Successful in 1m13s
Build and Push Docker Images / build-monitoring-ui (push) Successful in 1m15s
Build and Push Docker Images / deploy-test-node (push) Successful in 3s

### Summary of Changes
  1. Documentation Consolidation:
      • Relocated the docs/ folder and all repository .md files (README.md, DEPLOYMENT.md, DOCKER_README.md,
      DOMAINS_README.md, GETTING_STARTED.md, CI_CD_PORTAINER_GUIDE.md, ADMIN_DEPLOYMENT_SUMMARY.md,
      SECURITY_PRIVATE_FILES.md, handoff.md, azure-project-plan.md, plus service-specific READMEs) into .
      • Updated document links in README.md.
  2. AI Agent Project Map:
      • Created PROJECT_MAP.md in the project root containing complete architecture diagrams, service specs, ports, DB
      schemas, environment variables, and developer workflows.
      • Added PROJECT_MAP.md to .gitignore.
  3. Monitoring UI & Infrastructure Integration:
      • Added monitoring_ui service to docker-compose.yml (port 5175).
      • Configured Gitea workflow docker-build.yml for monitoring_ui builds.
      • Added CORS middleware headers in index.js.
      • Added nginx.conf copy step in Dockerfile.

  ──────
  ### Recommended Commit Message

    docs: consolidate project documentation into monitoring_ui and add agent project map

    - Move all markdown documentation files and docs/ folder into monitoring_ui/docs/
    - Update documentation hub index (monitoring_ui/docs/README.md) with relative paths
    - Generate comprehensive AI agent reference map (PROJECT_MAP.md) in repository root
    - Add PROJECT_MAP.md and monitoring_ui build output/env overrides to .gitignore
    - Integrate monitoring_ui service into docker-compose.yml and Gitea CI workflow
    - Configure CORS handling in employee_api for monitoring dashboard access
    ──────
  ### Alternative Short Commit Message

    refactor(docs): move docs to monitoring_ui and create AI agent PROJECT_MAP.md
This commit is contained in:
Sven laptop
2026-09-20 23:33:55 +02:00
parent f935bbeb8c
commit 635a130f0b
195 changed files with 28309 additions and 957 deletions
@@ -0,0 +1,256 @@
#!/usr/bin/env pwsh
# ---------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE.md in the project root for license information.
# ---------------------------------------------------------------------------------------------
# Scaffold -> Deploy conformance gate.
# Checks generated IaC against the plan for the semantic defects `az bicep build`
# cannot catch (valid Bicep, wrong values) — the class that causes deploy healing.
#
# Usage: scaffold-conformance.ps1 -SessionPath <.copilot-azure/sessions/{id}> -InfraPath <infra>
# Output: JSON { passed, failures:[{id,detail,file}] } to stdout.
# Exit: 0 = pass, 1 = one or more BLOCK failures.
[CmdletBinding()]
param(
[Parameter(Mandatory)] [string]$SessionPath,
[string]$InfraPath = 'infra'
)
$failures = New-Object System.Collections.Generic.List[object]
function Add-Fail($id, $detail, $file) {
$failures.Add([ordered]@{ id = $id; detail = $detail; file = $file })
}
function Read-Json($p) {
if (Test-Path $p) { try { return (Get-Content $p -Raw | ConvertFrom-Json) } catch { return $null } }
return $null
}
# --- Load artifacts (a missing input skips only the checks that need it) ---
$plan = Read-Json (Join-Path $SessionPath 'prepare-plan.json')
$mainPath = Join-Path $InfraPath 'main.bicep'
$paramsPath = Join-Path $InfraPath 'main.parameters.json'
$mainBicep = if (Test-Path $mainPath) { Get-Content $mainPath -Raw } else { '' }
$paramsRaw = if (Test-Path $paramsPath) { Get-Content $paramsPath -Raw } else { '' }
$iac = ''
if (Test-Path $InfraPath) {
$iac = (Get-ChildItem $InfraPath -Recurse -Filter *.bicep -ErrorAction SilentlyContinue |
ForEach-Object { Get-Content $_.FullName -Raw }) -join "`n"
}
$svcNames = @()
if ($plan -and $plan.services) { $svcNames = @($plan.services | ForEach-Object { $_.name }) }
function Has-Service($re) { return [bool]($svcNames | Where-Object { $_ -match $re }) }
# 1. TAGS-NO-CAMEL — tag keys must be hyphenated, not Bicep camelCase identifiers.
if ($mainBicep -match 'appOnboard(Skill|SessionId)|createdAt:|deployedBy:') {
Add-Fail 'TAGS-NO-CAMEL' "camelCase tag keys found — use hyphenated ('app-onboard-skill' ...)" 'infra/main.bicep'
}
# 12. DIAG-GATED — we don't scaffold diagnostic-settings; if the generator added one it must be gated behind
# enableDiagnostics (default false) or removed. Unconditional wiring blocks the first deploy (target/workspace not ready).
# Match an actual diagnostic MODULE/RESOURCE declaration, not a prose comment mentioning 'diagnostic'.
if ($mainBicep -match 'module\s+\w*[Dd]iagnostic|Microsoft\.Insights/diagnosticSettings' -and $mainBicep -notmatch 'enableDiagnostics') {
Add-Fail 'DIAG-GATED' 'diagnostic-settings module wired without an enableDiagnostics gate — gate it (module ... = if (enableDiagnostics), default false) or remove it; unconditional wiring blocks the first deploy' 'infra/main.bicep'
}
# 2. NO-PLAINTEXT-SECRET — secrets are @secure() params passed at deploy, never literals.
if ($paramsRaw) {
try {
$pj = $paramsRaw | ConvertFrom-Json
foreach ($k in $pj.parameters.PSObject.Properties.Name) {
if ($k -match '(?i)password|secret|connstring|connection') {
$val = $pj.parameters.$k.value
if ($null -ne $val -and "$val".Trim().Length -gt 0) {
Add-Fail 'NO-PLAINTEXT-SECRET' "parameter '$k' has a literal value — must be @secure(), passed at deploy" 'infra/main.parameters.json'
}
}
}
} catch { }
}
# 2b. NO-BICEP-LITERAL-SECRET — secret values must be @secure() params, never quoted literals in Bicep.
# Property name must end in 'password'/'connectionString' right before ':' (so 'secretName' etc. never match).
if ($iac) {
foreach ($m in [regex]::Matches($iac, "(?im)\b([A-Za-z]*(?:password|connectionstring))\s*:\s*'([^']+)'")) {
Add-Fail 'NO-BICEP-LITERAL-SECRET' "property '$($m.Groups[1].Value)' assigned a literal secret in Bicep — use an @secure() param passed at deploy" 'infra/'
}
if ($iac -match "(?im):\s*'[^']*(?:Password=|AccountKey=|SharedAccessKey=|://[^:@/\s]+:[^:@/\s]+@)[^']*'") {
Add-Fail 'NO-BICEP-LITERAL-SECRET' 'quoted literal contains an embedded credential (Password=/AccountKey=/user:pass@) in Bicep — use an @secure() param' 'infra/'
}
}
# 3. SERVICES-COMPLETE — every planned service maps to a resource type present in the IaC.
$typeMap = [ordered]@{
'container apps environment' = 'Microsoft\.App/managedEnvironments'
'container app' = 'Microsoft\.App/containerApps'
'container registry' = 'Microsoft\.ContainerRegistry/registries'
'mysql' = 'Microsoft\.DBforMySQL/flexibleServers'
'postgres' = 'Microsoft\.DBforPostgreSQL/flexibleServers'
'log analytics' = 'Microsoft\.OperationalInsights/workspaces'
'application insights' = 'Microsoft\.Insights/components'
'static web app' = 'Microsoft\.Web/staticSites'
'app service' = 'Microsoft\.Web/sites'
'functions' = 'Microsoft\.Web/sites'
'sql' = 'Microsoft\.Sql/servers'
'cosmos' = 'Microsoft\.DocumentDB/databaseAccounts'
'redis' = 'Microsoft\.Cache/redis'
'storage' = 'Microsoft\.Storage/storageAccounts'
'service bus' = 'Microsoft\.ServiceBus/namespaces'
'event hub' = 'Microsoft\.EventHub/namespaces'
}
if ($iac) {
foreach ($name in $svcNames) {
$lc = "$name".ToLower(); $expected = $null
foreach ($k in $typeMap.Keys) { if ($lc.Contains($k)) { $expected = $typeMap[$k]; break } }
if ($expected -and ($iac -notmatch $expected)) {
Add-Fail 'SERVICES-COMPLETE' "planned service '$name' has no matching resource ($expected) in IaC" 'infra/'
}
}
}
# --- Managed database checks ---
$hasMysql = Has-Service 'MySQL'
$hasPg = Has-Service 'PostgreSQL|Postgres'
if (($hasMysql -or $hasPg) -and $iac) {
# 4. DB-VERSION-MATCH — each server's version must equal its capabilities-verified plan value.
# Scope the match to the DB resource block so an unrelated 'version:' elsewhere isn't picked up.
$dbTypeRe = @{ 'MySQL' = 'Microsoft\.DBforMySQL/flexibleServers'; 'PostgreSQL' = 'Microsoft\.DBforPostgreSQL/flexibleServers' }
foreach ($svc in ($plan.services | Where-Object { $_.name -match 'MySQL|PostgreSQL' -and $_.version })) {
$kind = if ($svc.name -match 'MySQL') { 'MySQL' } else { 'PostgreSQL' }
$m = [regex]::Match($iac, "$($dbTypeRe[$kind])[\s\S]{0,800}?version:\s*'([^']+)'")
if ($m.Success -and $m.Groups[1].Value -ne $svc.version) {
Add-Fail 'DB-VERSION-MATCH' "$kind IaC version '$($m.Groups[1].Value)' != plan '$($svc.version)'" 'infra/modules'
}
}
# 5. DB-TLS-ON (MySQL) — require_secure_transport must be enforced.
if ($hasMysql -and ($iac -notmatch 'require_secure_transport')) {
Add-Fail 'DB-TLS-ON' 'MySQL module missing require_secure_transport config' 'infra/modules'
}
# 6. DB-NAME-PRESENT — the app's named DB must exist in IaC (from prepare-plan.appDbName).
if ($plan.appDbName -and ($iac -notmatch 'flexibleServers/databases')) {
Add-Fail 'DB-NAME-PRESENT' "app DB '$($plan.appDbName)' declared but no flexibleServers/databases resource" 'infra/modules'
}
# 10. MYSQL-NO-NETWORK-BLOCK (MySQL) — the public-access flow must OMIT the network block.
# An empty delegatedSubnetResourceId/privateDnsZoneResourceId is rejected by ARM (LinkedInvalidPropertyId).
if ($hasMysql -and ($iac -match 'delegatedSubnetResourceId|privateDnsZoneResourceId')) {
Add-Fail 'MYSQL-NO-NETWORK-BLOCK' 'MySQL module includes a network block (delegatedSubnetResourceId) — omit it for public access; an empty value is rejected by ARM' 'infra/modules'
}
# 11. DB-NO-LOCAL-AUTH — data services MUST be Entra / managed-identity only. No admin login/password.
if ($iac -match "administratorLogin\s*:") {
Add-Fail 'DB-NO-LOCAL-AUTH' "administratorLogin/administratorLoginPassword present — databases must be Entra-only (authConfig passwordAuth Disabled + Entra admin). Remove ALL admin login/password params." 'infra/modules'
}
if ($iac -match "passwordAuth\s*:\s*'Enabled'") {
Add-Fail 'DB-NO-LOCAL-AUTH' "PostgreSQL authConfig sets passwordAuth: 'Enabled' — must be 'Disabled' (Entra-only)" 'infra/modules'
}
# 11b. DB-ENTRA-ADMIN — PG/MySQL must set an Entra administrator so migrations run token-based.
if (($hasPg -or $hasMysql) -and ($iac -notmatch 'flexibleServers/administrators')) {
Add-Fail 'DB-ENTRA-ADMIN' 'PostgreSQL/MySQL module missing an administrators (Entra admin) child resource — required for Entra-only auth + token-based migrations' 'infra/modules'
}
# 11c. PG-ENTRA-ONLY — PostgreSQL must explicitly disable password auth.
if ($hasPg -and ($iac -notmatch "passwordAuth\s*:\s*'Disabled'")) {
Add-Fail 'PG-ENTRA-ONLY' "PostgreSQL module missing authConfig { passwordAuth: 'Disabled', activeDirectoryAuth: 'Enabled' }" 'infra/modules'
}
# 11c2. MYSQL-ENTRA-UAMI — MySQL AAD admin requires a user-assigned MI; identityResourceId: null fails to deploy.
if ($hasMysql) {
if ($iac -match 'identityResourceId\s*:\s*null') {
Add-Fail 'MYSQL-ENTRA-UAMI' "MySQL administrators sets identityResourceId: null — will not deploy. Set it to a user-assigned managed identity resourceId and add that UAMI to the server's identity block." 'infra/modules'
} elseif ($iac -notmatch 'identityResourceId') {
Add-Fail 'MYSQL-ENTRA-UAMI' "MySQL administrators missing identityResourceId — MySQL Entra admin REQUIRES a user-assigned managed identity (unlike PostgreSQL)." 'infra/modules'
}
}
}
# --- No shared-key / access-key / local-auth on any data service (managed-identity only) ---
if ($iac) {
# 11d. STORAGE-NO-SHARED-KEY
if (($iac -match 'Microsoft\.Storage/storageAccounts') -and ($iac -notmatch 'allowSharedKeyAccess\s*:\s*false')) {
Add-Fail 'STORAGE-NO-SHARED-KEY' 'Storage account must set allowSharedKeyAccess: false (managed-identity only)' 'infra/modules'
}
# 11e. REDIS-NO-KEYS
if (($iac -match 'Microsoft\.Cache/redis') -and ($iac -notmatch 'disableAccessKeyAuthentication\s*:\s*true')) {
Add-Fail 'REDIS-NO-KEYS' 'Redis must set disableAccessKeyAuthentication: true + accessPolicyAssignments (Entra only)' 'infra/modules'
}
# 11f. COSMOS-NO-LOCAL-AUTH
if (($iac -match 'Microsoft\.DocumentDB/databaseAccounts') -and ($iac -notmatch 'disableLocalAuth\s*:\s*true')) {
Add-Fail 'COSMOS-NO-LOCAL-AUTH' 'Cosmos DB must set disableLocalAuth: true (data-plane RBAC only)' 'infra/modules'
}
# 11g. SBUS-EHUB-NO-LOCAL-AUTH
if (($iac -match 'Microsoft\.ServiceBus/namespaces|Microsoft\.EventHub/namespaces') -and ($iac -notmatch 'disableLocalAuth\s*:\s*true')) {
Add-Fail 'MSG-NO-LOCAL-AUTH' 'Service Bus / Event Hubs namespace must set disableLocalAuth: true (managed-identity only)' 'infra/modules'
}
# 11h. NO-FREE-SKU — free/shared compute tiers are not allowed (managed identity is mandatory).
if ($iac -match "name\s*:\s*'(F1|D1)'") {
Add-Fail 'NO-FREE-SKU' "App Service plan uses a free/shared SKU ('$($Matches[1])') — floor is B1 (managed identity is mandatory; the free-tier MI sidecar OOMs)" 'infra/modules'
}
if (($iac -match 'Microsoft\.Web/staticSites') -and ($iac -match "tier\s*:\s*'Free'")) {
Add-Fail 'NO-FREE-SKU' "Static Web App uses the Free tier — floor is Standard" 'infra/modules'
}
}
# --- Key Vault checks ---
# --- No Key Vault (app-internal secrets live on the compute resource) ---
if (($iac -match 'Microsoft\.KeyVault/vaults') -or ($iac -match '@Microsoft\.KeyVault') -or ($iac -match 'keyVaultUrl')) {
Add-Fail 'NO-KEYVAULT' 'Key Vault detected (Microsoft.KeyVault/vaults, @Microsoft.KeyVault(...), or keyVaultUrl) — no Key Vault is allowed. Store app-internal secrets on the compute resource: @secure() param -> App Service appSettings / Container Apps native secrets[].' 'infra/'
}
# --- Container Apps / ACR checks (deterministic ARM-failure invariants) ---
$hasCA = $iac -match 'Microsoft\.App/containerApps'
$hasCAE = $iac -match 'Microsoft\.App/managedEnvironments'
$hasAcr = $iac -match 'Microsoft\.ContainerRegistry/registries'
if ($iac) {
# 13. CAE-APPLOGS — managedEnvironments log config MUST nest under appLogsConfiguration; a bare
# top-level logAnalyticsConfiguration fails deploy with ManagedEnvironmentInvalidSchema.
if ($hasCAE -and ($iac -match 'logAnalyticsConfiguration') -and ($iac -notmatch 'appLogsConfiguration')) {
Add-Fail 'CAE-APPLOGS' "Container Apps Environment uses a bare logAnalyticsConfiguration — nest it under appLogsConfiguration.destination='log-analytics' (else ManagedEnvironmentInvalidSchema at deploy)" 'infra/modules'
}
# 14. CA-NO-REVISION-SUFFIX — a hardcoded revisionSuffix fails Phase 2 redeploy ('revision already exists').
if ($hasCA -and ($iac -match 'revisionSuffix\s*:')) {
Add-Fail 'CA-NO-REVISION-SUFFIX' 'Container App sets revisionSuffix — omit it (ARM auto-generates); a hardcoded value fails Phase 2 redeploy' 'infra/modules'
}
# 15. CA-IMAGE-PARAM — two-phase deploy needs `param containerImage` in main.bicep, else the Phase 2 `--parameters containerImage=` override is silently ignored and the placeholder image persists.
if ($hasCA -and $hasAcr -and ($mainBicep -notmatch 'param\s+containerImage')) {
Add-Fail 'CA-IMAGE-PARAM' 'main.bicep lacks `param containerImage` — the Phase 2 image override is silently ignored and the placeholder persists (MANIFEST_UNKNOWN)' 'infra/main.bicep'
}
# 16. ACRPULL-GUID — near-miss detector: the fixed AcrPull prefix present without the canonical full GUID = wrong last segment (hallucinated) → RoleDefinitionDoesNotExist. Cannot false-positive.
if (($iac -match '7f951dda-4ed3-4680-a7ca-') -and ($iac -notmatch '7f951dda-4ed3-4680-a7ca-43fe172d538d')) {
Add-Fail 'ACRPULL-GUID' 'AcrPull role GUID is wrong — canonical value is 7f951dda-4ed3-4680-a7ca-43fe172d538d (RoleDefinitionDoesNotExist otherwise)' 'infra/modules/role-assignments.bicep'
}
# 17. ACR-NO-PREMIUM-POLICY — retention/trust/quarantine are Premium-only; on Basic/Standard ACR they fail SkuNotSupported. FP-safe: skipped if any 'Premium' SKU appears in the IaC.
if ($hasAcr -and ($iac -match '(retentionPolicy|trustPolicy|quarantinePolicy)\s*:') -and ($iac -notmatch "'Premium'")) {
Add-Fail 'ACR-NO-PREMIUM-POLICY' 'ACR has a Premium-only policy (retention/trust/quarantine) but is not Premium — omit these for Basic/Standard (SkuNotSupported at deploy)' 'infra/modules'
}
}
# 9. WARN-FIXED — prereq warnings flagged fixPhase:"scaffold" must land in the generated IaC.
# Only warnings with a provable signal are enforced; unverifiable ones are skipped (no false BLOCK).
# Extend $warnSignal as new provable scaffold-phase warning IDs are added.
$prereq = Read-Json (Join-Path $SessionPath 'prereq-output.json')
if ($prereq -and $prereq.warnings) {
$warnSignal = @{
'W-PG-SSL' = @{ kind = 'iac'; pattern = 'PGSSLMODE|sslmode' } # SSL-mode env var emitted in IaC
'W-BUILDKIT' = @{ kind = 'file'; pattern = 'Dockerfile.azure' } # ACR-compatible Dockerfile emitted
}
foreach ($w in $prereq.warnings) {
if ($w.fixPhase -ne 'scaffold') { continue }
$sig = $warnSignal[$w.id]
if (-not $sig) { continue } # no deterministic signal — cannot verify, do not block
$present = if ($sig.kind -eq 'file') {
[bool](Get-ChildItem -Recurse -Filter $sig.pattern -ErrorAction SilentlyContinue | Select-Object -First 1)
} else {
[bool]($iac -match $sig.pattern)
}
if (-not $present) {
Add-Fail 'WARN-FIXED' "prereq warning '$($w.id)' (fixPhase:scaffold) fix not found in IaC — $($w.fix)" 'infra/'
}
}
}
$result = [ordered]@{ passed = ($failures.Count -eq 0); failures = $failures }
$result | ConvertTo-Json -Depth 5 -Compress
if ($failures.Count -gt 0) { exit 1 } else { exit 0 }