Files
shopdb-flask/tools/shot.py
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

58 lines
2.1 KiB
Python

"""Headless-Chromium screenshot helper for the dev UI.
Logs in once via the API, injects the token into localStorage the same way
the auth store does, then screenshots each path passed on the command line.
venv/bin/python tools/shot.py /printers/1 /reports/toner /printers/1/edit
Images land in the scratchpad dir as shot_<sanitised-path>.png.
"""
import os
import sys
import json
import urllib.request
from playwright.sync_api import sync_playwright
UI = "http://localhost:5173"
API = "http://localhost:5001/api"
# creds from env so no real login lands in source; fall back to dev defaults
USERNAME = os.environ.get("SHOT_USERNAME", "270015376")
PASSWORD = os.environ.get("SHOT_PASSWORD", "changeme")
OUTDIR = "/tmp/claude-1000/-home-camp-projects/effc3424-ed5e-4b09-b83e-d141bee23c42/scratchpad"
def login():
body = json.dumps({"username": USERNAME, "password": PASSWORD}).encode()
loginrequest = urllib.request.Request(f"{API}/auth/login", data=body,
headers={"Content-Type": "application/json"})
data = json.load(urllib.request.urlopen(loginrequest))["data"]
return data["access_token"], data["refresh_token"], data["user"]
def main(paths):
token, refresh, user = login()
seed = f"""
localStorage.setItem('token', {json.dumps(token)});
localStorage.setItem('refreshToken', {json.dumps(refresh)});
localStorage.setItem('user', {json.dumps(json.dumps(user))});
"""
with sync_playwright() as p:
browser = p.chromium.launch()
context = browser.new_context(viewport={"width": 1400, "height": 1000})
context.add_init_script(seed)
page = context.new_page()
for path in paths:
page.goto(f"{UI}{path}", wait_until="networkidle", timeout=30000)
page.wait_for_timeout(1200) # let supply fetch + render settle
name = "shot_" + (path.strip("/").replace("/", "_") or "home") + ".png"
out = f"{OUTDIR}/{name}"
page.screenshot(path=out, full_page=True)
print(out)
browser.close()
if __name__ == "__main__":
main(sys.argv[1:] or ["/printers/1"])