Files
shopdb-flask/scripts/Start-ShopDBDev.ps1
cproudlock 88a683dad3 Act on the five-agent review of the wilm import
Findings the reviewers verified, fixed here. The larger cluster they found is
NOT fixed and is listed at the end.

Frontend. reportImageUrl is called from the template, so it ran on every render
with no in-flight guard: a report whose fetch was pending was re-requested, and
because each resolution reassigns the map the whole grid re-rendered and
re-fired the rest, costing up to N-squared requests for N thumbnails. Two blobs
resolving for one key orphaned the first object URL where revoke could not
reach it, and a failure was never recorded so a broken image retried forever.
Guarded, deduplicated, failures remembered.

Three CSS variables did not exist, so their hardcoded fallbacks always won:
--bg-alt (the "No screenshot" tile, near-black in light mode), and
--warning-text/-bg/-border (dark khaki on a dark modal). Replaced with real
tokens. The DEV badge's literal #c62828 is var(--danger).

Installer and launcher. The generated site installer ran docker compose with no
-p, so the project name defaulted to the package folder - which carries the
version. Installing the next version created a new project with new empty
volumes and the site came up looking wiped, old data stranded under the old
project. Pinned to shopdb-<channel>.

Start-ShopDBDev deleted any existing -TargetDir that lacked a .git, recursively
and without asking. For a script handed round as "save it anywhere and run it",
a mistyped -TargetDir pointed at Documents would have destroyed it. It now only
reclaims a directory that is empty or carries this launcher's own marker, and
refuses otherwise.

Dev secrets came from Get-Random, which is System.Random: a 32-bit seed, and all
four values off one stream, so recovering one recovers the rest. They sign JWTs
for a stack a pilot may point at real imported data. CSPRNG now, and the API
port binds to loopback like the DB rather than every interface.

Two of my own from earlier in this session. The operator-report size check sat
below request.form, and the first access to that parses and spools the whole
multipart - so it ran after the thing it prevents. Moved above. And the channel
remap missed build-channel-installer.ps1 entirely, where it set BOTH channels to
main: a "stable" package built from the trunk, the exact mislabel the remap was
for. stable now resolves the newest v* tag.

Also removed a zero-byte docs/ShopDB-Planner-Tasks-v2.csv that rode in.

NOT FIXED - needs a decision, and it is the biggest thing the review found.
Migrations 0006/0007 freeze the originating site's fleet convention into
product seed: a shipped policy literally named "RPCA Shopfloor App Monitors",
a hardcoded 'Shopfloor PC' type deciding who gets the shipped watch-list, and
shipped rows the API refuses to edit or deactivate - including their launch
paths. A site whose eDNC is on another drive cannot correct it and gets
permanent false drift. Those are stamped migrations, so this gets harder to
undo at every install. See the review notes before anyone upgrades a second
site through 0006-0007.

Suite 2314 passing, frontend 316, all three PowerShell files parse.

Scope: product
2026-09-02 15:33:48 -04:00

192 lines
8.6 KiB
PowerShell

<#
.SYNOPSIS
Save-and-run launcher for a ShopDB dev workstation (stable or edge).
.DESCRIPTION
Download this one file and run it. It clones (or updates) the repo at the
channel branch into a local path, then hands off to scripts/dev-bootstrap.ps1
for the real dependency-check / build / up work.
ge-aero/shopdb-flask is a PRIVATE repo, but this needs no token pasting: the
clone uses a plain https URL and Git Credential Manager supplies your stored
GitHub credential automatically - the same way `git clone` works in the wiki
DEVELOPMENT-SETUP procedure. If you have ever cloned this repo or signed in to
GitHub in Visual Studio / VS Code on this box, GCM already has what it needs.
This is a downloaded FILE, not a web `iex` one-liner, precisely because a
private repo cannot be fetched anonymously from raw.githubusercontent.com. A
saved script sidesteps that entirely - only the git operations need auth, and
GCM handles those.
.PARAMETER Channel
'stable' (the newest release tag) or 'edge' (main, the trunk). Defaults to 'stable'.
.PARAMETER TargetDir
Where to clone. Defaults to %USERPROFILE%\shopdb-dev, a separate working copy
from any repo you already have open.
.EXAMPLE
# Bleeding edge:
powershell -ExecutionPolicy Bypass -File .\Start-ShopDBDev.ps1 -Channel edge
.EXAMPLE
# Stable:
powershell -ExecutionPolicy Bypass -File .\Start-ShopDBDev.ps1 -Channel stable
.NOTES
ASCII only, no emoji or smart punctuation, per repo style. PowerShell 5.1+.
Needs Git (with Credential Manager, the Git-for-Windows default);
dev-bootstrap.ps1 offers to winget-install anything else that is missing.
#>
[CmdletBinding()]
param(
[ValidateSet('stable', 'edge')]
[string]$Channel = 'stable',
[string]$TargetDir
)
$ErrorActionPreference = 'Stop'
# Reset leftovers from an interrupted or timed-out prior run so the retry starts
# clean. Proxy bottlenecks can kill a build mid-flight, leaving orphaned
# containers holding the channel ports, dangling build cache from the partial
# build, and sometimes a half-written clone. This is scoped to the channel and
# is non-destructive to data: the compose down keeps named volumes (the DB), and
# only dangling build cache is pruned - never tagged images or volumes.
function Clear-InterruptedRun {
param(
[Parameter(Mandatory)][string]$Project,
[Parameter(Mandatory)][string]$Target
)
if (Get-Command docker -ErrorAction SilentlyContinue) {
# docker/compose write warnings to stderr (e.g. "No resource found to
# remove"), which would trip $ErrorActionPreference='Stop' and abort the
# whole run. These cleanup steps are best-effort, so soften the preference
# and swallow all streams for their duration.
$previous = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
try {
# Only try compose down when the engine is actually responding, otherwise
# skip quietly - a fresh box may not have Docker started yet.
docker info *> $null
if ($LASTEXITCODE -eq 0) {
Write-Host "==> Cleaning up any interrupted '$Project' stack" -ForegroundColor Cyan
# --remove-orphans clears containers left by a killed build; volumes are
# intentionally kept so the DB survives a retry.
docker compose -p $Project down --remove-orphans *> $null
# Reclaim only dangling build cache from failed/partial builds.
docker builder prune -f *> $null
}
}
finally {
$ErrorActionPreference = $previous
}
}
# A directory that exists but has no valid .git is a broken/partial clone -
# USUALLY. It is also what -TargetDir pointed at somebody's Documents folder
# looks like, and this ran Remove-Item -Recurse -Force on it without asking.
# For a script distributed as "save it anywhere and run it", a mistyped
# -TargetDir must not be able to delete a user's files.
#
# Only reclaim a directory this script can show it owns: empty, or carrying
# the marker written after a successful clone. Anything else stops.
if ((Test-Path $Target) -and -not (Test-Path (Join-Path $Target '.git'))) {
$ours = Test-Path (Join-Path $Target $script:CloneMarker)
$empty = -not (Get-ChildItem -LiteralPath $Target -Force |
Select-Object -First 1)
if ($ours -or $empty) {
Write-Host "==> Removing incomplete clone at $Target" -ForegroundColor Cyan
Remove-Item -Path $Target -Recurse -Force
}
else {
throw ("$Target already exists, is not empty, and was not created " +
"by this launcher. Refusing to delete it. Pass -TargetDir " +
'pointing somewhere else, or remove it yourself if you are sure.')
}
}
# A clone created by an elevated (Administrator) run is owned by
# BUILTIN\Administrators; a later non-elevated run then trips git's
# "detected dubious ownership" guard and every git op fails with exit 128.
# That stale clone is effectively unusable for this user, so detect the
# condition and remove it here for a clean re-clone rather than surfacing a
# misleading auth error later.
if (Test-Path (Join-Path $Target '.git')) {
$previous = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
try { $probe = git -C $Target rev-parse --is-inside-work-tree 2>&1 } catch { $probe = "$_" }
finally { $ErrorActionPreference = $previous }
if ($probe -match 'dubious ownership') {
# Same rule as above: a .git directory proves this is a clone, but
# not that it is OUR clone, and re-cloning discards whatever is in
# it. Only reclaim one this launcher created.
if (Test-Path (Join-Path $Target $script:CloneMarker)) {
Write-Host "==> Removing clone with mismatched ownership at $Target (was it created from an elevated prompt?)" -ForegroundColor Cyan
Remove-Item -Path $Target -Recurse -Force
}
else {
throw ("The git clone at $Target has mismatched ownership and " +
'was not created by this launcher, so it is not being ' +
'deleted. Remove it yourself, or pass -TargetDir elsewhere.')
}
}
}
}
# Plain https URL. No token in the URL: Git Credential Manager injects the
# stored GitHub credential for github.com, so clone/fetch authenticate against
# the private repo without anything sensitive touching this script or disk.
$repoUrl = 'https://github.com/ge-aero/shopdb-flask.git'
# Written into a clone this launcher made, so a later run can tell its own
# working directory apart from one that happens to sit at the same path.
$script:CloneMarker = '.shopdb-launcher'
# Channels follow how this repo ships: a release is a TAG, the trunk is main.
# Upstream this read 'dev-be' for edge, which is that repo's trunk branch and
# does not exist here - the launcher would have cloned a missing branch.
$branch = 'main'
if ($Channel -ne 'edge') {
# Newest release tag, so 'stable' means a thing that was released rather
# than whatever the trunk happens to be today.
$tag = (git ls-remote --tags --refs --sort='-v:refname' $repoUrl 'v*' |
Select-Object -First 1) -replace '.*refs/tags/', ''
if ($tag) { $branch = $tag }
else { Write-Warning "No v* tag found; falling back to main for 'stable'." }
}
$target = if ($TargetDir) { $TargetDir } else { Join-Path $env:USERPROFILE 'shopdb-dev' }
Write-Host "ShopDB dev launcher" -ForegroundColor White
Write-Host " Channel : $Channel ($branch)" -ForegroundColor White
Write-Host " Target : $target" -ForegroundColor White
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
throw 'git is required. Install Git for Windows (winget install Git.Git) and re-run.'
}
$project = if ($Channel -eq 'edge') { 'shopdb-edge' } else { 'shopdb-stable' }
Clear-InterruptedRun -Project $project -Target $target
if (Test-Path (Join-Path $target '.git')) {
Write-Host "==> Updating existing clone" -ForegroundColor Cyan
git -C $target fetch origin $branch
if ($LASTEXITCODE -ne 0) { throw "git fetch failed ($LASTEXITCODE). If this is an auth error, clone the repo once in Visual Studio / VS Code so Credential Manager stores your GitHub login." }
git -C $target checkout $branch
if ($LASTEXITCODE -ne 0) { throw "git checkout failed ($LASTEXITCODE)." }
git -C $target pull --ff-only origin $branch
if ($LASTEXITCODE -ne 0) { throw "git pull failed ($LASTEXITCODE)." }
} else {
Write-Host "==> Cloning $branch" -ForegroundColor Cyan
git clone --branch $branch $repoUrl $target
if ($LASTEXITCODE -ne 0) { throw "git clone failed ($LASTEXITCODE). If this is an auth error, sign in to GitHub once in Visual Studio / VS Code (or run 'git clone $repoUrl' manually) so Credential Manager stores your login, then re-run." }
}
$bootstrap = Join-Path $target 'scripts\dev-bootstrap.ps1'
if (-not (Test-Path $bootstrap)) {
throw "dev-bootstrap.ps1 not found at $bootstrap. Is the '$branch' branch correct?"
}
Write-Host "==> Handing off to dev-bootstrap.ps1" -ForegroundColor Cyan
& $bootstrap -Channel $Channel