Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docker/docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
137 changes: 137 additions & 0 deletions docs/VERIFICATION_PROOF_REPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions scripts/preview-down.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
43 changes: 2 additions & 41 deletions scripts/preview-status.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
94 changes: 19 additions & 75 deletions scripts/preview-up.ps1
Original file line number Diff line number Diff line change
@@ -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'
Loading