Files
shopdb-flask/docs/INSTALL-WINDOWS-IIS.md
cproudlock b8c22244a1
Some checks failed
CI / backend (push) Failing after 2s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 7s
Multi-site distribution readiness: settings-driven site config, security closeout, release engineering, v0.5.0
Make the app distributable to other GE Aerospace sites (one self-hosted
instance per site, ADR-004). GE values remain the shipped defaults; every
site-specific behavior is now a Setting an admin can change in the UI.

Settings-driven site config:
- Branding: site/QR/badge logos, favicon, primary color (upload endpoints
  mirror the map-blueprint pattern; new Settings > Branding section).
- ServiceNow: search/incident/change URL templates ({ticket}), ticket
  prefixes, enable toggle. Defaults point at the current
  geaerospaceqa.service-now.com global search. Disabled = plain-text tickets.
- Employee-id regex (employeeid_pattern), printer hostname template,
  QR label targets (qr_target_printer / qr_target_usb, blank = asset page,
  else URL template with placeholders), usb_label_style (barcode|qr).
- West Jefferson floor-plan PNGs removed from the tree; generic placeholder
  ships as the map default and sites upload their own blueprint.

Security closeout:
- dashboarddefaults writes now require admin.
- Collector: generic error messages (no str(exc) leak); API key accepted
  via X-API-Key header only (BREAKING: querystring api_key removed).
- IP-based login rate limiting (AUTH_RATELIMIT_* knobs) atop account lockout.
- Setting.set() creation race fixed (IntegrityError retry).

Release engineering and docs:
- __version__ 0.5.0 (distinct from __contract_version__, ADR-007),
  CHANGELOG.md, Gitea Actions CI config, frontend version aligned.
- One wizard-first install story across README/DEPLOY; new CONFIG.md,
  UPGRADE.md, BACKUP-RESTORE.md; CLAUDE.md and ROADMAP de-staled.
- Dockerfile multi-stage build now bundles the frontend; compose binds
  MySQL to 127.0.0.1; stale database/schema.sql and one-off SQL removed.

Debt and fixes:
- .query.get() -> db.session.get() sweep; datetime.utcnow() removed
  (naive-UTC via timezone-aware now); users.py on authz decorators.
- Fixed 4 stale tests (slides feed shape, shopfloor splitperemployee,
  plugin contract purity) and the USB label page field mapping (both usb
  modes emit the cmmc shape: device_id/device_desc).
- Health endpoint reports the real version.

248 tests pass; naming/style check green; frontend builds; fresh-DB
flask db upgrade + seeds verified; QR targets verified by decoding
rendered codes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 15:02:07 -04:00

7.2 KiB

ShopDB - Windows + IIS install runbook

A step-by-step, tested install for a new site on Windows Server / Windows 11 with IIS in front of the Flask app (HttpPlatformHandler -> waitress), backed by MySQL. This runbook was validated end to end on a win11 + IIS + MySQL 5.6 box.

APP_ROOT below = the deploy folder, e.g. C:\shopdb-flask (where wsgi.py lives). Run PowerShell as Administrator.


0. Prerequisites

Need Notes
Python 3.12 (64-bit) python --version
IIS with HttpPlatformHandler https://www.iis.net/downloads/microsoft/httpplatformhandler (direct MSI: download.microsoft.com/download/8/1/3/813AC4E6-9203-4F7A-8DD5-F3D54D10C5CD/httpPlatformHandler_amd64.msi)
MySQL 5.7+/8.0 (or 5.6 with the flags in step 1) reachable from the app host
URL Rewrite (optional) only for the real-client-IP rule; skip it and the app still runs

The app itself pulls in waitress and tzdata from requirements.txt (step 4).


1. MySQL: flags (5.6 only) + database + user

On MySQL 5.6 only, add to my.ini/my.cnf under [mysqld] and restart MySQL (5.7+/8.0 need none of this):

innodb_file_per_table = 1
innodb_file_format    = Barracuda
innodb_large_prefix   = 1

Without them, flask db upgrade fails with error 1071 ("key too long") - the migrations use ROW_FORMAT=DYNAMIC, which needs the 3072-byte prefix these unlock.

Then create the database (utf8mb4) and an app user:

CREATE DATABASE shopdb_flask CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'shopdb'@'%' IDENTIFIED BY 'CHANGE_ME';
GRANT ALL PRIVILEGES ON shopdb_flask.* TO 'shopdb'@'%';
FLUSH PRIVILEGES;

2. Deploy the app files

Copy the release (the repo minus venv/, .git/, node_modules/, frontend/src/) to APP_ROOT. It must contain wsgi.py, shopdb/, plugins/, migrations/, requirements.txt, and the pre-built frontend/dist/.


3. Virtual env + dependencies

cd APP_ROOT
python -m venv venv
venv\Scripts\python -m pip install -r requirements.txt

This installs Flask, SQLAlchemy, PyMySQL, waitress (the WSGI server IIS launches) and tzdata (Windows has no IANA tz database; without it the notifications plugin fails with "No time zone found with key America/New_York").


4. Secrets + connection (.env)

Create APP_ROOT\.env (read by wsgi.py via load_dotenv()). Lock its ACLs to the app-pool identity + admins.

FLASK_ENV=production
SECRET_KEY=<64+ random chars>
JWT_SECRET_KEY=<another 64+ random chars>
DATABASE_URL=mysql+pymysql://shopdb:CHANGE_ME@<mysql-host>:3306/shopdb_flask?charset=utf8mb4
CORS_ORIGINS=http://<the site's own hostname-or-ip:port>

Generate a key: venv\Scripts\python -c "import secrets;print(secrets.token_urlsafe(64))". Production refuses to boot if any of SECRET_KEY, JWT_SECRET_KEY, DATABASE_URL, CORS_ORIGINS is missing or a dev default.


5. Preflight (catch problems before installing)

$env:FLASK_APP="shopdb"
venv\Scripts\flask db-utils preflight

Checks Python, required env, DB connectivity, and the MySQL 5.6 index flags, and prints exactly what to fix. Fix any FAIL before continuing.


6. Schema + data + plugins + admin

$env:FLASK_APP="shopdb"

venv\Scripts\flask db upgrade                # creates every table (to head)
venv\Scripts\flask seed reference-data       # statuses, machine/location/rel types
venv\Scripts\flask seed permissions
venv\Scripts\flask seed settings

# enable the plugins this site tracks (registry is empty on a fresh box).
# usb + employees install DISABLED by default - enable them later in the wizard
# if the site wants those (they create extra tables).
foreach ($p in "computers","equipment","network","notifications","printers","knowledgebase","slides","warranty") {
  venv\Scripts\flask plugin install $p
}

# first admin (password generated + printed once - store it):
venv\Scripts\flask seed admin --username admin --email admin@yourfacility.example.com

Prefer no CLI? Skip seed admin (and even the seed steps): start the site, and the login page offers to create the first admin on a fresh instance, then the setup wizard can seed reference data. Either path works.


7. IIS site

  1. Copy deploy\windows\web.config to APP_ROOT\web.config. If APP_ROOT is not C:\shopdb-flask, fix the paths inside it. Create APP_ROOT\logs.
  2. Create an app pool with No Managed Code:
    Import-Module WebAdministration
    New-WebAppPool -Name shopdbflask
    Set-ItemProperty IIS:\AppPools\shopdbflask -Name managedRuntimeVersion -Value ""
    
  3. Grant the app-pool identity access:
    icacls APP_ROOT /grant "IIS AppPool\shopdbflask:(OI)(CI)RX" /T
    icacls APP_ROOT\logs /grant "IIS AppPool\shopdbflask:(OI)(CI)M" /T
    
  4. Unlock the handler sections (locked server-wide by default; without this IIS returns HTTP 500.19):
    %windir%\system32\inetsrv\appcmd unlock config /section:system.webServer/handlers
    %windir%\system32\inetsrv\appcmd unlock config /section:system.webServer/httpPlatform
    
  5. Create the site (own port; the classic ASP site can keep 8080):
    New-Website -Name shopdb-flask -Port 8090 -PhysicalPath APP_ROOT -ApplicationPool shopdbflask
    New-NetFirewallRule -DisplayName "shopdb-flask 8090" -Direction Inbound -Protocol TCP -LocalPort 8090 -Action Allow
    Start-Website shopdb-flask
    

IIS launches waitress-serve --port=%HTTP_PLATFORM_PORT% wsgi:app per the web.config and reverse-proxies the site port to it. First request takes ~15s (the app boots + connects to MySQL).

The X-Forwarded-For URL Rewrite rule in web.config is commented out by default. It needs the URL Rewrite module; with it active but the module absent, IIS returns 500.19. Install URL Rewrite, then uncomment the <rewrite> block, to record real client IPs in audit logs.


8. Smoke test + first run

(Invoke-WebRequest http://localhost:8090/ -UseBasicParsing).StatusCode      # 200 (SPA)
Invoke-WebRequest http://localhost:8090/api/auth/login -Method POST `
  -Body '{"username":"admin","password":"<the printed password>"}' `
  -ContentType application/json -UseBasicParsing                            # 200 + token

Browse to http://<host>:8090, sign in as the admin, and the setup wizard walks through site name, features (per-plugin: create tables here vs connect a DB), floor-map upload, and starter data. Multiple Flask apps can share one IIS box - each gets its own site, app pool, port, and venv.


Troubleshooting

Symptom Cause / fix
flask db upgrade -> error 1071 MySQL 5.6 without the step-1 flags (or server not restarted).
IIS 500.19 handler sections not unlocked (step 7.4), or the <rewrite> block active without URL Rewrite.
500 with an empty HttpPlatform log app-pool identity can't read APP_ROOT / run the venv (step 7.3), or .env missing/invalid.
"No time zone found with key America/New_York" tzdata not installed (pip install tzdata).
Nav missing Equipment/PCs/... plugins not installed (step 6 flask plugin install), or site not recycled.
ConfigError on boot a required .env var missing or left at a dev default.