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>
This commit is contained in:
194
docs/INSTALL-WINDOWS-IIS.md
Normal file
194
docs/INSTALL-WINDOWS-IIS.md
Normal file
@@ -0,0 +1,194 @@
|
||||
# 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:
|
||||
|
||||
```sql
|
||||
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
|
||||
|
||||
```powershell
|
||||
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)
|
||||
|
||||
```powershell
|
||||
$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
|
||||
|
||||
```powershell
|
||||
$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**:
|
||||
```powershell
|
||||
Import-Module WebAdministration
|
||||
New-WebAppPool -Name shopdbflask
|
||||
Set-ItemProperty IIS:\AppPools\shopdbflask -Name managedRuntimeVersion -Value ""
|
||||
```
|
||||
3. Grant the app-pool identity access:
|
||||
```powershell
|
||||
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**):
|
||||
```powershell
|
||||
%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):
|
||||
```powershell
|
||||
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
|
||||
|
||||
```powershell
|
||||
(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. |
|
||||
Reference in New Issue
Block a user