From 5839dd7acd1ebdd0f1a77c89411af4b15b97e575 Mon Sep 17 00:00:00 2001 From: MD TAZIZUL ISLAM Date: Thu, 19 Feb 2026 02:59:19 -0500 Subject: [PATCH] SERIAL 12: permanent live preview infra (non-breaking) --- docker/docker-compose.dev.yml | 4 +- docs/VERIFICATION_PROOF_REPORT.md | 137 ++++++++++++++++++++++++++++++ scripts/preview-down.ps1 | 4 +- scripts/preview-status.ps1 | 43 +--------- scripts/preview-up.ps1 | 94 +++++--------------- 5 files changed, 162 insertions(+), 120 deletions(-) diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 3e68dcb..b63c48d 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -65,8 +65,8 @@ services: source: ../web target: /workspace/web - web_node_modules:/workspace/web/node_modules - command: > - sh -lc "npm install && npm run dev" + restart: unless-stopped + command: npm run dev ports: - "3000:3000" depends_on: diff --git a/docs/VERIFICATION_PROOF_REPORT.md b/docs/VERIFICATION_PROOF_REPORT.md index 3bb36b1..ca7cca0 100644 --- a/docs/VERIFICATION_PROOF_REPORT.md +++ b/docs/VERIFICATION_PROOF_REPORT.md @@ -386,6 +386,143 @@ NAME IMAGE COMMAND SERVICE CREATED - proof/serial11-patch04-option2/postmerge-root.png - proof/serial11-patch04-option2/postmerge-dashboard.png +## SERIAL 12 — Permanent Live Preview Infrastructure (Non-Breaking, Agent-Only, Proof-First) + +### Scope +- Branch: `feature/serial-12-permanent-preview` +- Infra-only changes: `docker/docker-compose.dev.yml`, `scripts/preview-up.ps1`, `scripts/preview-status.ps1`, `scripts/preview-down.ps1` +- No changes to `web/src/app/*`, dashboard pages, backend API logic, routing, or provisioning. + +### Docker diff summary +>>> CMD: git diff --stat + docker/docker-compose.dev.yml | 4 +- + scripts/preview-down.ps1 | 4 +- + scripts/preview-status.ps1 | 43 +------------------- + scripts/preview-up.ps1 | 94 +++++++++---------------------------------- + 4 files changed, 25 insertions(+), 120 deletions(-) + +### Docker compose config proof (key web fields) +>>> CMD: docker compose -f docker/docker-compose.dev.yml config +web: + command: + - npm + - run + - dev + environment: + NODE_ENV: development + ports: + - mode: ingress + target: 3000 + published: "3000" + protocol: tcp + restart: unless-stopped + +### Compose status + restart policy proof +>>> CMD: docker compose -f docker/docker-compose.dev.yml ps +NAME IMAGE SERVICE STATUS PORTS +factory-dev-web-1 factory-dev-web web Up 29 seconds (healthy) 0.0.0.0:3000->3000/tcp, [::]:3000->3000/tcp + +>>> CMD: docker inspect factory-dev-web-1 --format "{{json .HostConfig.RestartPolicy}}" +{"Name":"unless-stopped","MaximumRetryCount":0} + +### Preview control scripts (final contents) + +#### scripts/preview-up.ps1 +```powershell +param( + [int]$Attempts = 60, + [int]$DelaySeconds = 2 +) + +$ErrorActionPreference = 'Stop' + +$RepoRoot = Split-Path -Parent $PSScriptRoot +$ComposeFile = Join-Path $RepoRoot 'docker\docker-compose.dev.yml' + +docker compose -f $ComposeFile up -d --build web +if ($LASTEXITCODE -ne 0) { + throw 'docker compose up -d --build web failed' +} + +$ready = $false +for ($attempt = 1; $attempt -le $Attempts; $attempt++) { + try { + $response = Invoke-WebRequest -UseBasicParsing -Uri 'http://localhost:3000' -TimeoutSec 5 + if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 500) { + $ready = $true + break + } + } catch { + } + + Start-Sleep -Seconds $DelaySeconds +} + +if (-not $ready) { + throw 'preview did not become reachable on localhost:3000 in time' +} + +Write-Output 'PREVIEW_READY' +``` + +#### scripts/preview-status.ps1 +```powershell +$ErrorActionPreference = 'Stop' + +$RepoRoot = Split-Path -Parent $PSScriptRoot +$ComposeFile = Join-Path $RepoRoot 'docker\docker-compose.dev.yml' + +docker compose -f $ComposeFile ps web +Test-NetConnection localhost -Port 3000 +``` + +#### scripts/preview-down.ps1 +```powershell +$ErrorActionPreference = 'Stop' + +$RepoRoot = Split-Path -Parent $PSScriptRoot +$ComposeFile = Join-Path $RepoRoot 'docker\docker-compose.dev.yml' + +docker compose -f $ComposeFile stop web +if ($LASTEXITCODE -ne 0) { + throw 'docker compose stop web failed' +} +``` + +### Script execution proofs (raw) +>>> CMD: pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/preview-up.ps1 +PREVIEW_READY + +>>> CMD: pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/preview-status.ps1 +NAME IMAGE SERVICE STATUS PORTS +factory-dev-web-1 factory-dev-web web Up 17 seconds (healthy) 0.0.0.0:3000->3000/tcp, [::]:3000->3000/tcp +ComputerName : localhost +RemoteAddress : ::1 +RemotePort : 3000 +TcpTestSucceeded : True + +>>> CMD: pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/preview-down.ps1 +[+] stop 1/1 + ✔ Container factory-dev-web-1 Stopped + +### Runtime verification proof +>>> CMD: Invoke-WebRequest http://localhost:3000 -UseBasicParsing +StatusCode=200 +ContentLength=50054 + +### Windows auto-start (optional) proof +>>> CMD: schtasks /Create /TN FactoryPreviewAutoStart /SC ONLOGON /TR "powershell -NoProfile -ExecutionPolicy Bypass -File C:\Users\vitor\Dev\factory\scripts\preview-up.ps1" /F +ERROR: Access is denied. + +>>> CMD: schtasks /Query /TN FactoryPreviewAutoStart +ERROR: The system cannot find the file specified. + +### Screenshot of root page +- Not captured in this run (terminal-only execution environment). + +### Post-reboot test result +- Not executed in this run. + ## SERIAL 11 — CI/Security Hardening failure analysis (PR #32) ### Failed run details diff --git a/scripts/preview-down.ps1 b/scripts/preview-down.ps1 index 778dbdf..667a288 100644 --- a/scripts/preview-down.ps1 +++ b/scripts/preview-down.ps1 @@ -3,7 +3,7 @@ $ErrorActionPreference = 'Stop' $RepoRoot = Split-Path -Parent $PSScriptRoot $ComposeFile = Join-Path $RepoRoot 'docker\docker-compose.dev.yml' -docker compose -f $ComposeFile down -v +docker compose -f $ComposeFile stop web if ($LASTEXITCODE -ne 0) { - throw 'docker compose down -v failed' + throw 'docker compose stop web failed' } diff --git a/scripts/preview-status.ps1 b/scripts/preview-status.ps1 index 60175a6..9c623ea 100644 --- a/scripts/preview-status.ps1 +++ b/scripts/preview-status.ps1 @@ -3,44 +3,5 @@ $ErrorActionPreference = 'Stop' $RepoRoot = Split-Path -Parent $PSScriptRoot $ComposeFile = Join-Path $RepoRoot 'docker\docker-compose.dev.yml' -function Show-PortStatus { - param( - [int]$Port, - [string]$Name - ) - - $listening = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue - if ($null -eq $listening) { - Write-Output ($Name + '_port_' + $Port + '=NOT_LISTENING') - return - } - - Write-Output ($Name + '_port_' + $Port + '=LISTENING') -} - -Write-Output '=== PREVIEW STATUS: docker compose ps ===' -docker compose -f $ComposeFile ps - -Write-Output '=== PREVIEW STATUS: docker ps ports ===' -docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" - -Write-Output '=== PREVIEW STATUS: listening ports ===' -Show-PortStatus -Port 3000 -Name 'web' -Show-PortStatus -Port 4100 -Name 'orchestrator' -Show-PortStatus -Port 4000 -Name 'api' - -Write-Output '=== PREVIEW STATUS: HTTP checks ===' -try { - $web = Invoke-WebRequest -UseBasicParsing -Uri 'http://localhost:3000' -TimeoutSec 15 - Write-Output ('web_http_status=' + $web.StatusCode) -} catch { - Write-Output ('web_http_error=' + $_.Exception.Message) -} - -try { - $health = Invoke-RestMethod -Uri 'http://localhost:4100/health' -TimeoutSec 15 - Write-Output 'orchestrator_health_json=' - $health | ConvertTo-Json -Depth 6 | Write-Output -} catch { - Write-Output ('orchestrator_health_error=' + $_.Exception.Message) -} +docker compose -f $ComposeFile ps web +Test-NetConnection localhost -Port 3000 diff --git a/scripts/preview-up.ps1 b/scripts/preview-up.ps1 index 173af8f..e1f2704 100644 --- a/scripts/preview-up.ps1 +++ b/scripts/preview-up.ps1 @@ -1,90 +1,34 @@ param( - [switch]$OpenBrowser, - [int]$Attempts = 60 + [int]$Attempts = 60, + [int]$DelaySeconds = 2 ) $ErrorActionPreference = 'Stop' $RepoRoot = Split-Path -Parent $PSScriptRoot $ComposeFile = Join-Path $RepoRoot 'docker\docker-compose.dev.yml' -. (Join-Path $PSScriptRoot 'readiness-retry.ps1') -function Assert-PortListening { - param( - [int]$Port, - [string]$Name - ) - - $listening = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue - if ($null -eq $listening) { - Write-Output ("port_" + $Port + "=NOT_LISTENING") - throw ($Name + ' is not listening on port ' + $Port) - } - Write-Output ("port_" + $Port + "=LISTENING") -} - -function Show-Diagnostics { - Write-Output '=== DIAGNOSTICS: docker compose ps ===' - docker compose -f $ComposeFile ps - - Write-Output '=== DIAGNOSTICS: web logs (tail 120) ===' - docker compose -f $ComposeFile logs web --tail 120 - - Write-Output '=== DIAGNOSTICS: orchestrator logs (tail 120) ===' - docker compose -f $ComposeFile logs orchestrator --tail 120 - - Write-Output '=== DIAGNOSTICS: api logs (tail 120) ===' - docker compose -f $ComposeFile logs api --tail 120 - - Write-Output 'SUGGESTED_FIX=Verify Docker Desktop is running, then rerun: docker compose -f docker/docker-compose.dev.yml up -d --build. If web is still refused, inspect logs above and confirm web port mapping includes 3000:3000 in docker/docker-compose.dev.yml.' -} - -Write-Output '=== STEP 1: compose up -d --build ===' -docker compose -f $ComposeFile up -d --build +docker compose -f $ComposeFile up -d --build web if ($LASTEXITCODE -ne 0) { - Write-Output 'compose_up_result=failed' - Show-Diagnostics - throw 'compose up failed' + throw 'docker compose up -d --build web failed' } -Write-Output '=== STEP 2: wait web http://localhost:3000 ===' -$webReady = Wait-HttpReady -Name 'web' -Url 'http://localhost:3000' -Attempts $Attempts -DelaySec 2 -TimeoutSec 10 -ExpectedStatus 200 -Write-Output ("web_ready=" + $webReady.Ready + " attempt=" + $webReady.Attempt + " status=" + $webReady.StatusCode) - -Write-Output '=== STEP 3: verify published/listening ports ===' -Assert-PortListening -Port 3000 -Name 'web' -Assert-PortListening -Port 4100 -Name 'orchestrator' -Assert-PortListening -Port 4000 -Name 'api' - -$dockerPsText = docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | Out-String -$has3000Mapping = $dockerPsText -match '(:|\[::\]:)3000->3000/tcp' - -if (-not $webReady.Ready -or -not $has3000Mapping) { - if (-not $has3000Mapping) { - Write-Output 'port_3000_mapping=missing' +$ready = $false +for ($attempt = 1; $attempt -le $Attempts; $attempt++) { + try { + $response = Invoke-WebRequest -UseBasicParsing -Uri 'http://localhost:3000' -TimeoutSec 5 + if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 500) { + $ready = $true + break + } + } catch { } - Show-Diagnostics - throw 'dashboard preview is not reachable on localhost:3000' -} - -Write-Output 'CUSTOMER_LANDING_URL=http://localhost:3000/' -Write-Output 'ADMIN_DASHBOARD_URL=http://localhost:3000/dashboard' -$landingStatus = (Invoke-WebRequest -UseBasicParsing -Uri 'http://localhost:3000/' -TimeoutSec 15).StatusCode -$dashboardStatus = (Invoke-WebRequest -UseBasicParsing -Uri 'http://localhost:3000/dashboard' -TimeoutSec 15).StatusCode -Write-Output ("landing_status=" + $landingStatus) -Write-Output ("dashboard_status=" + $dashboardStatus) - -Write-Output '=== STEP 4: orchestrator health ===' -$health = Invoke-RestMethodWithRetry -Name 'orchestrator_health' -Method Get -Uri 'http://localhost:4100/health' -Attempts 20 -DelaySec 2 -TimeoutSec 20 -$health | ConvertTo-Json -Depth 6 | Write-Output - -Write-Output '=== STEP 5: published ports summary ===' -$dockerPsText.TrimEnd() | Write-Output + Start-Sleep -Seconds $DelaySeconds +} -if ($OpenBrowser) { - Start-Process 'http://localhost:3000' | Out-Null - Write-Output 'open_browser=launched' -} else { - Write-Output 'open_browser=skipped' +if (-not $ready) { + throw 'preview did not become reachable on localhost:3000 in time' } + +Write-Output 'PREVIEW_READY'