Adds a checkbox to the public submission page that lets a sender opt out of background removal, ticked by default because most items look better cut out and the reverse default would mean almost nobody got it. It only renders when the server reports the sidecar is configured, matching the intake link's new backgroundRemoval flag from Task 5 — an unconfigured environment gets no checkbox rather than one that would do nothing. submitItem now takes removeBackground as a required fourth parameter, sent as the multipart string 'true' or 'false' to match the backend's exact-string opt-out contract. Making the parameter required rather than optional was deliberate, so the compiler would catch any call site left unupdated; the frontend build (which also type-checks tests/ via tsconfig.test.json) confirmed the only call site, in Submit.tsx, was updated. scripts/start-local.ps1 now sets REMBG_URL for the local backend so the checkbox is visible during local and e2e runs; the value need not resolve, since no e2e submission reaches the sidecar without a configured drafting step. Adds two e2e cases to intake-submit.spec.ts: the checkbox appears ticked by default, and a sender can uncheck it and still submit successfully. Both are written per the task-7 brief but not run in this session, since running Playwright requires the full local stack (database, backend, frontend dev server) which was not started. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
400 lines
15 KiB
PowerShell
400 lines
15 KiB
PowerShell
#Requires -Version 7
|
|
<#
|
|
.SYNOPSIS
|
|
Starts the Redefined Designs stack locally for testing and review.
|
|
|
|
.DESCRIPTION
|
|
Brings up a Postgres container, runs migrations, builds and starts the
|
|
backend, and starts the Vite dev server. Safe to run repeatedly: an
|
|
existing container is reused rather than recreated, and processes already
|
|
listening are left alone.
|
|
|
|
Logs and process ids go to .local/ at the repository root.
|
|
|
|
.PARAMETER Fresh
|
|
Drop the database container and its data before starting, so migrations
|
|
run against an empty database.
|
|
|
|
.PARAMETER Stop
|
|
Stop the backend, the dev server and the database container, then exit.
|
|
|
|
.PARAMETER DbPort
|
|
Host port for Postgres. Change it if something already holds the default.
|
|
|
|
.EXAMPLE
|
|
.\scripts\start-local.ps1
|
|
.\scripts\start-local.ps1 -Fresh
|
|
.\scripts\start-local.ps1 -Stop
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
# Deliberately not 55432. That port is reserved by Hyper-V on at least one
|
|
# machine here, and Docker's failure when it cannot bind does not mention
|
|
# reservations, which has cost time more than once.
|
|
[int]$DbPort = 55500,
|
|
[int]$ApiPort = 3000,
|
|
[int]$WebPort = 5173,
|
|
[switch]$Fresh,
|
|
# Runs the stack against the throwaway end-to-end database instead of the
|
|
# development one: a separate container on a separate port, with tmpfs
|
|
# storage, so it starts empty every time.
|
|
#
|
|
# The e2e suite used to run against the development database, and nothing
|
|
# ever truncated it. Every run seeded more fixtures and left them, so the
|
|
# unfiltered storefront grew monotonically — 1,662 items by the time #186
|
|
# was filed — until rendering it outran the assertions' timeout. It failed
|
|
# locally, passed in CI where the database is fresh, and got steadily worse.
|
|
#
|
|
# This does not touch the development database, so anything you have set up
|
|
# there by hand survives.
|
|
[switch]$E2eDb,
|
|
[switch]$Stop,
|
|
# What -Stop puts the machine back to. nvm's default here is 18.16.1, which
|
|
# is too old to run this project's tooling but is what everything else on
|
|
# the machine expects.
|
|
# Resolved after NodeVersion.ps1 is dot-sourced below, not here. A param
|
|
# block runs before anything else in the script, so $script:DEFAULT_NODE_VERSION
|
|
# is still $null at this point and using it as the default would silently
|
|
# restore nothing — leaving the machine on the pinned version, which is the
|
|
# exact failure the restore exists to prevent. See #208.
|
|
[string]$DefaultNodeVersion = ''
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$RepoRoot = Split-Path -Parent $PSScriptRoot
|
|
$StateDir = Join-Path $RepoRoot '.local'
|
|
$PidFile = Join-Path $StateDir 'pids.json'
|
|
|
|
# Whether this run is starting from an empty database. Set below, and read by
|
|
# Start-Backend, which must refuse to reuse a backend that is still serving the
|
|
# previous one. See #257.
|
|
$script:DatabaseIsNew = $false
|
|
# Which database this run uses. Everything downstream reads these four, so the
|
|
# choice is made once here rather than branched at each use.
|
|
if ($E2eDb) {
|
|
$Container = 'redefined-designs-e2e-db'
|
|
$DbUser = 'redefined_e2e'
|
|
$DbPassword = 'redefined_e2e'
|
|
$DbName = 'redefined_e2e'
|
|
# Not 55500 (development) and not 55432 (the integration suite). The three
|
|
# must not collide: the integration suite truncates between tests, so
|
|
# sharing a database with an e2e run would delete that run's fixtures
|
|
# underneath it (#116).
|
|
if (-not $PSBoundParameters.ContainsKey('DbPort')) { $DbPort = 55501 }
|
|
}
|
|
else {
|
|
$Container = 'redefined-designs-local-db'
|
|
$DbUser = 'redefined_local'
|
|
$DbPassword = 'redefined_local'
|
|
$DbName = 'redefined_local'
|
|
}
|
|
|
|
function Write-Step { param([string]$Message) Write-Host "==> $Message" -ForegroundColor Cyan }
|
|
function Write-Note { param([string]$Message) Write-Host " $Message" -ForegroundColor DarkGray }
|
|
function Write-Good { param([string]$Message) Write-Host " $Message" -ForegroundColor Green }
|
|
|
|
# $ErrorActionPreference = 'Stop' does NOT stop the script when a native
|
|
# executable exits non-zero, only when a cmdlet throws. Everything here is
|
|
# node, npm or docker, so without this wrapper a failed migration is a line of
|
|
# red text the script prints and then carries straight past. It did exactly
|
|
# that once, and reported a healthy stack sitting on an empty database.
|
|
function Invoke-Checked {
|
|
param([scriptblock]$Command, [string]$What)
|
|
& $Command
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "$What failed (exit code $LASTEXITCODE)."
|
|
}
|
|
}
|
|
|
|
function Assert-Docker {
|
|
docker info *>$null
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Docker is not running. Start Docker Desktop and try again."
|
|
}
|
|
}
|
|
|
|
. (Join-Path $PSScriptRoot 'NodeVersion.ps1')
|
|
|
|
# The one home for this value is NodeVersion.ps1, beside NODE_VERSION. It cannot
|
|
# be a param default (see the note there), so it is filled in here instead, and
|
|
# an explicit -DefaultNodeVersion still wins.
|
|
if (-not $DefaultNodeVersion) { $DefaultNodeVersion = $script:DEFAULT_NODE_VERSION }
|
|
|
|
# Bound once so the shared switcher reports through this script's own output
|
|
# style rather than printing in a voice of its own.
|
|
$NodeOut = @{ Step = ${function:Write-Step}; Note = ${function:Write-Note} }
|
|
|
|
# Reads back only the ids this script wrote. Killing by port would be shorter
|
|
# and would also kill whatever else happened to be listening.
|
|
function Get-TrackedProcesses {
|
|
if (-not (Test-Path $PidFile)) { return @{} }
|
|
try { return (Get-Content $PidFile -Raw | ConvertFrom-Json -AsHashtable) }
|
|
catch { return @{} }
|
|
}
|
|
|
|
function Stop-Tracked {
|
|
param([string]$Name)
|
|
$tracked = Get-TrackedProcesses
|
|
if (-not $tracked.ContainsKey($Name)) { return }
|
|
$process = Get-Process -Id $tracked[$Name] -ErrorAction SilentlyContinue
|
|
if ($process) {
|
|
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
|
|
Write-Good "stopped $Name (pid $($process.Id))"
|
|
}
|
|
}
|
|
|
|
function Set-Tracked {
|
|
param([string]$Name, [int]$ProcessId)
|
|
$tracked = Get-TrackedProcesses
|
|
$tracked[$Name] = $ProcessId
|
|
$tracked | ConvertTo-Json | Set-Content $PidFile
|
|
}
|
|
|
|
function Test-Listening {
|
|
param([int]$Port)
|
|
return [bool](Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue)
|
|
}
|
|
|
|
function Wait-For {
|
|
param(
|
|
[scriptblock]$Condition,
|
|
[string]$What,
|
|
[int]$TimeoutSeconds = 60
|
|
)
|
|
for ($i = 1; $i -le $TimeoutSeconds; $i++) {
|
|
if (& $Condition) {
|
|
Write-Good "$What ready after ${i}s"
|
|
return
|
|
}
|
|
Start-Sleep -Seconds 1
|
|
}
|
|
throw "$What did not become ready within ${TimeoutSeconds}s."
|
|
}
|
|
|
|
function Stop-Environment {
|
|
Write-Step 'Stopping'
|
|
Stop-Tracked 'backend'
|
|
Stop-Tracked 'frontend'
|
|
docker stop $Container *>$null
|
|
if ($LASTEXITCODE -eq 0) { Write-Good "stopped container $Container" }
|
|
else { Write-Note "container $Container was not running" }
|
|
Remove-Item $PidFile -ErrorAction SilentlyContinue
|
|
Write-Host ''
|
|
Write-Host 'Stopped.' -ForegroundColor Green
|
|
}
|
|
|
|
function Start-Database {
|
|
if ($Fresh) {
|
|
Write-Step 'Removing the existing database (-Fresh)'
|
|
docker rm -f $Container *>$null
|
|
Write-Good 'removed'
|
|
$script:DatabaseIsNew = $true
|
|
}
|
|
|
|
# The e2e container stores its data on tmpfs, so it comes up empty whether it
|
|
# is created or merely restarted. There is no case where it carries anything
|
|
# over, which is the point of it (#186) — and it means a backend from a
|
|
# previous run is always stale against it.
|
|
if ($E2eDb) { $script:DatabaseIsNew = $true }
|
|
|
|
$existing = (docker ps -a --filter "name=^/$Container$" --format '{{.Names}}')
|
|
if ($existing -eq $Container) {
|
|
Write-Step "Reusing the database container"
|
|
docker start $Container *>$null
|
|
}
|
|
else {
|
|
$script:DatabaseIsNew = $true
|
|
Write-Step "Creating the database container on port $DbPort"
|
|
# tmpfs for the e2e database only: its contents are worthless the
|
|
# moment the run ends, and storing nothing is what makes it start empty
|
|
# every time rather than accumulating fixtures the way the development
|
|
# database did (#186). Migrations run on every start below, so an empty
|
|
# volume is a working one. The development database keeps its storage.
|
|
$storage = if ($E2eDb) { @('--tmpfs', '/var/lib/postgresql/data') } else { @() }
|
|
|
|
docker run -d --name $Container `
|
|
-e "POSTGRES_USER=$DbUser" `
|
|
-e "POSTGRES_PASSWORD=$DbPassword" `
|
|
-e "POSTGRES_DB=$DbName" `
|
|
-p "${DbPort}:5432" `
|
|
@storage `
|
|
postgres:16 *>$null
|
|
|
|
if ($LASTEXITCODE -ne 0) {
|
|
# The message Docker gives for a reserved port does not say
|
|
# "reserved", so name the likely cause and the way out of it.
|
|
throw @"
|
|
Could not start Postgres on port $DbPort.
|
|
|
|
If the port is in use, or reserved by Hyper-V (which silently claims ranges on
|
|
Windows), pick another one:
|
|
|
|
.\scripts\start-local.ps1 -DbPort 55600
|
|
|
|
Reserved ranges: netsh interface ipv4 show excludedportrange protocol=tcp
|
|
"@
|
|
}
|
|
}
|
|
|
|
Wait-For -What 'Postgres' -Condition {
|
|
docker exec $Container pg_isready -U $DbUser -d $DbName *>$null
|
|
$LASTEXITCODE -eq 0
|
|
}
|
|
}
|
|
|
|
function Install-IfMissing {
|
|
param([string]$Directory)
|
|
$name = Split-Path -Leaf $Directory
|
|
if (Test-Path (Join-Path $Directory 'node_modules')) {
|
|
Write-Note "$name dependencies already installed"
|
|
return
|
|
}
|
|
Write-Step "Installing $name dependencies"
|
|
Push-Location $Directory
|
|
try { Invoke-Checked { npm install } "$name npm install" } finally { Pop-Location }
|
|
}
|
|
|
|
function Start-Backend {
|
|
$backend = Join-Path $RepoRoot 'backend'
|
|
|
|
# The six the backend refuses to boot without, plus the two that make a
|
|
# local run behave. Set in this session so the child process inherits them.
|
|
$env:PGHOST = 'localhost'
|
|
$env:PGPORT = "$DbPort"
|
|
$env:PGUSER = $DbUser
|
|
$env:PGPASSWORD = $DbPassword
|
|
$env:PGDATABASE = $DbName
|
|
$env:UPLOADS_DIR = (Join-Path $StateDir 'uploads')
|
|
$env:PORT = "$ApiPort"
|
|
# No PayPal credentials locally. DEMO_MODE lets the whole cart and checkout
|
|
# path run without them and with no way to reach live PayPal.
|
|
$env:DEMO_MODE = 'true'
|
|
# Needs to be set for the submission page's checkbox to appear at all, but
|
|
# not to resolve: no e2e submission reaches the sidecar, because the
|
|
# worker only cuts out after a draft and drafting is not configured
|
|
# locally.
|
|
$env:REMBG_URL = 'http://127.0.0.1:7000'
|
|
New-Item -ItemType Directory -Force -Path $env:UPLOADS_DIR *>$null
|
|
|
|
Write-Step 'Running migrations'
|
|
Push-Location $backend
|
|
try {
|
|
Invoke-Checked { node migrate.js up } 'Migrations'
|
|
Write-Step 'Building the backend'
|
|
Invoke-Checked { npm run build } 'Backend build'
|
|
}
|
|
finally { Pop-Location }
|
|
|
|
if (Test-Listening -Port $ApiPort) {
|
|
# Leaving a stranger's backend alone is only safe when the database has
|
|
# not just changed underneath it. When it has, that process is serving a
|
|
# database it no longer owns, and its in-memory state describes rows that
|
|
# no longer exist.
|
|
#
|
|
# That is not theoretical. The rate limiter keys on customer id and keeps
|
|
# its buckets in memory for an hour; a recreated database restarts ids at
|
|
# 1, so a new customer inherits a previous run's spent allowance. It
|
|
# reproduced exactly the intermittent resend-verification failure in #257
|
|
# — three refusal toasts where one was expected — and the control (same
|
|
# id restart, restarted backend) passed. Whatever else is stale, a
|
|
# process serving the wrong database gives wrong answers confidently,
|
|
# which is the worst kind.
|
|
#
|
|
# So this refuses rather than continuing, and says what to do. A run that
|
|
# stops loudly is recoverable; one that quietly tests the wrong thing is
|
|
# what cost two wrong measurements in this project already.
|
|
$tracked = Get-TrackedProcesses
|
|
$ours = $tracked.ContainsKey('backend') -and
|
|
(Get-Process -Id $tracked['backend'] -ErrorAction SilentlyContinue)
|
|
|
|
if ($script:DatabaseIsNew -and -not $ours) {
|
|
throw @"
|
|
Something is already listening on $ApiPort, and this run just created a new database.
|
|
|
|
That process is serving the previous database. Its in-memory state — rate-limit
|
|
buckets keyed on customer id, most obviously — describes rows that no longer
|
|
exist, and recycled ids will inherit them (#257).
|
|
|
|
Stop it and run this again:
|
|
|
|
.\scripts\start-local.ps1 -Stop
|
|
"@
|
|
}
|
|
|
|
Write-Note "something is already listening on $ApiPort; leaving it alone"
|
|
return
|
|
}
|
|
|
|
Write-Step "Starting the backend on $ApiPort"
|
|
# Separate files: Start-Process cannot redirect both streams to one path.
|
|
$process = Start-Process -FilePath 'node' -ArgumentList 'dist/server.js' `
|
|
-WorkingDirectory $backend `
|
|
-RedirectStandardOutput (Join-Path $StateDir 'backend.log') `
|
|
-RedirectStandardError (Join-Path $StateDir 'backend.err.log') `
|
|
-WindowStyle Hidden -PassThru
|
|
Set-Tracked 'backend' $process.Id
|
|
|
|
Wait-For -What 'Backend' -Condition {
|
|
try {
|
|
$null = Invoke-WebRequest "http://localhost:$ApiPort/api/config" -TimeoutSec 2 -UseBasicParsing
|
|
$true
|
|
}
|
|
catch { $false }
|
|
}
|
|
}
|
|
|
|
function Start-Frontend {
|
|
if (Test-Listening -Port $WebPort) {
|
|
Write-Note "something is already listening on $WebPort; leaving it alone"
|
|
return
|
|
}
|
|
|
|
Write-Step "Starting the dev server on $WebPort"
|
|
$process = Start-Process -FilePath 'npm.cmd' -ArgumentList 'run', 'dev' `
|
|
-WorkingDirectory (Join-Path $RepoRoot 'frontend') `
|
|
-RedirectStandardOutput (Join-Path $StateDir 'frontend.log') `
|
|
-RedirectStandardError (Join-Path $StateDir 'frontend.err.log') `
|
|
-WindowStyle Hidden -PassThru
|
|
Set-Tracked 'frontend' $process.Id
|
|
|
|
Wait-For -What 'Dev server' -Condition { Test-Listening -Port $WebPort }
|
|
}
|
|
|
|
New-Item -ItemType Directory -Force -Path $StateDir *>$null
|
|
|
|
if ($Stop) {
|
|
Stop-Environment
|
|
Restore-Node -Version $DefaultNodeVersion @NodeOut
|
|
return
|
|
}
|
|
|
|
Use-PinnedNode @NodeOut
|
|
|
|
# Anything after the switch reverts on the way out of a failure. Without this a
|
|
# run that dies in migrations leaves the machine on the new version with nothing
|
|
# started, and the -Stop that would put it back is never reached.
|
|
try {
|
|
Assert-Docker
|
|
Start-Database
|
|
Install-IfMissing (Join-Path $RepoRoot 'backend')
|
|
Install-IfMissing (Join-Path $RepoRoot 'frontend')
|
|
Start-Backend
|
|
Start-Frontend
|
|
}
|
|
catch {
|
|
Restore-Node -Version $DefaultNodeVersion @NodeOut
|
|
throw
|
|
}
|
|
|
|
Write-Host ''
|
|
Write-Host 'Running.' -ForegroundColor Green
|
|
Write-Host " Storefront http://localhost:$WebPort"
|
|
Write-Host " Admin http://localhost:$WebPort/admin"
|
|
Write-Host " API http://localhost:$ApiPort/api/config"
|
|
Write-Host " Postgres localhost:$DbPort ($DbUser / $DbPassword / $DbName)"
|
|
Write-Host ''
|
|
Write-Host " Logs $StateDir"
|
|
Write-Host " Stop .\scripts\start-local.ps1 -Stop (also restores Node $DefaultNodeVersion)"
|
|
Write-Host ''
|