Files
shopdb-flask/deploy/windows/installer
cproudlock 600c2164ea
Some checks failed
CI / backend (push) Failing after 2m3s
CI / naming (push) Failing after 1s
CI / frontend (push) Successful in 9s
CI / migrations-mysql (push) Failing after 7s
feat(deploy): add the air-gapped Windows installer
Roughly 2500 lines of tested installer had been living in ~/Downloads and an
untracked folder - nothing was under version control.

It goes here rather than in a repo of its own because it depends on application
internals: the `flask plugin` verbs, site-profile.json, MOUNT_PATH, and the
plugin registry. Versioned separately it would drift out of step with the thing
it installs.

Contents: the read-only preflight, the staged installer (bundled MySQL, runtime,
schema, IIS, verify, uninstall), the operator console, the Inno Setup wizard, the
bundle builder and the artwork generator.

bundle/ and Output/ are ignored - regenerable, and ~220MB. plugins.iss is ignored
because build-installer.sh generates it from the staged payload. The artwork IS
committed so a Windows build box does not need Python and cairosvg.

Verified end to end on Windows Server 2025 against a bundled MySQL 8.0 and an
existing MySQL 5.6: fresh install, upgrade with backup and rollback, re-run
idempotency, uninstall, and both deployment methods including switching between
them. Not yet verified: a hypervisor-level air-gapped run, and any load from a
real browser (every HTTP check so far used curl, which sends no Origin header).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 01:47:34 -04:00
..

Windows installer

Builds a single self-contained .exe that installs ShopDB-Flask on an air-gapped Windows Server. Nothing here ever touches the network at install time: Python, the wheels, the SPA and (optionally) MySQL all ship inside it.

Lives with the application on purpose. The installer depends on app internals - flask plugin verbs, site-profile.json, MOUNT_PATH, the plugin registry - so a separate repo would drift out of step with the thing it installs.

Files

File What it is
shopdb-preflight.ps1 Stage 1. Read-only. Changes nothing, reports what this server is missing.
shopdb-install.ps1 Stages 0 and 2-5 plus uninstall. All the actual work.
shopdb-admin.ps1 Operator console installed alongside the app: status, restart, logs, backup, plugins.
ShopDBFlask.iss Inno Setup wizard. A thin wrapper - it collects input and runs the stages.
build-installer.sh Stages the bundle from a site profile.
make-branding.py Generates wizard artwork and the icon from frontend/public/*.svg.
*.bmp, shopdb.ico Generated artwork, committed so a Windows build box needs no Python.

Building

# 1. Stage the bundle for a site's plugin set.
./build-installer.sh ../../site-profile.example.json

# 2. Add the pieces that cannot be built on Linux:
#      bundle/wheels/               ~47 cp314 win_amd64 wheels, built ON Windows:
#                                   pip download -r requirements.txt -d wheels --only-binary=:all:
#      bundle/python/               python-3.14.x-amd64.exe
#      bundle/httpplatformhandler/  httpPlatformHandler_amd64.msi
#      bundle/mysql/                mysql-8.0.x-winx64.msi   (bundled-database option only)

# 3. Compile on Windows.
iscc ShopDBFlask.iss

The wheelhouse is cp314-locked. A different Python minor version means a different wheelhouse; the installer will not use a Python it did not install.

Deployment methods

Chosen in the wizard, and the bundle carries a SPA build for each because Vite compiles the base path in - it cannot be switched at install time.

  • Its own site on a port (default 8090).
  • Subpath under an existing site, e.g. http://<server-fqdn>/shopdb/. Needs no new DNS record. Three things must agree - the IIS application alias, MOUNT_PATH in .env, and the SPA's build-time base - so the alias is fixed per bundle (SUBPATH_ALIAS, default shopdb) and the installer refuses if the bundle's build does not match what was asked for.

Switching between methods removes the other one's IIS artifact and reconciles MOUNT_PATH and CORS_ORIGINS, so a server never ends up with both.

Upgrades

Run a newer installer over an existing install. It:

  • backs the database up first, verifies the dump is complete, and refuses to migrate if it cannot;
  • restores from that backup if migrations fail, and reports honestly that DDL the failed migration committed cannot be undone;
  • refuses to run a bundle older than what is installed;
  • keeps .env unless new credentials are supplied, and copies it aside first;
  • stops the app pool before replacing files, then starts it again.

Whether a run is an upgrade is decided by probing the target database, not by whether the app directory exists - a rebuilt server pointed at an existing database is an upgrade, and treating it as fresh would drop tables.

Testing notes

Verified end to end on Windows Server 2025 against both a bundled MySQL 8.0 and an existing MySQL 5.6: fresh install, upgrade, re-run idempotency, failure and rollback, uninstall, and both deployment methods including switching between them.

Not yet verified: a fully air-gapped run with the network disabled at the hypervisor, and any load in a real browser (all HTTP checks so far used curl, which sends no Origin header - so CORS_ORIGINS is untested in anger).