- scripts/screenshots.py + scripts/Dockerfile.shots: playwright-in-compose runner, 7 demo shots against the seeded stack (DinD-safe, output volume) - docker-compose.yml: api + web services (nginx SPA + /api proxy-less via build-arg VITE_API_BASE), shots service with volume output - apps/api: add CORSMiddleware (SPA origin web:80 could not read api:8000), batch scoring now refuses to rewrite applications beyond discovered/scored (kanban page load previously reset sent/interviewing to scored), prompt-aware cv_tailor mock so demo passes hallucination guard, hallucination-guard tests repointed to monkeypatched run_task - apps/web: Applications kanban batches only unscored applications, red flag display falls back to stored rationale - 159 api tests + 14 web tests green after fixes
84 lines
3 KiB
Python
84 lines
3 KiB
Python
"""Screenshot runner: drives the seed demo UI and saves PNGs to /out.
|
|
|
|
Runs inside the compose network; browser resolves 'web' and 'api' directly.
|
|
"""
|
|
import json
|
|
import sys
|
|
import time
|
|
import urllib.request
|
|
|
|
from playwright.sync_api import sync_playwright
|
|
|
|
BASE = "http://web"
|
|
API = "http://api:8000/api"
|
|
OUT = "/out"
|
|
|
|
|
|
def api(method, path, body=None):
|
|
req = urllib.request.Request(
|
|
API + path,
|
|
data=json.dumps(body).encode() if body else None,
|
|
method=method,
|
|
headers={"Content-Type": "application/json"},
|
|
)
|
|
with urllib.request.urlopen(req, timeout=30) as r:
|
|
return json.loads(r.read())
|
|
|
|
|
|
def main():
|
|
# Idempotent: ensures full demo dataset exists
|
|
seed = api("POST", "/concierge/seed-demo")
|
|
print("seed:", json.dumps(seed)[:200])
|
|
|
|
apps = api("GET", "/applications")
|
|
interview = next((a for a in apps if a["state"] == "interviewing"), apps[0])
|
|
detail_id = interview["id"]
|
|
print("detail id:", detail_id)
|
|
|
|
shots = [
|
|
("welcome", "/", {}), # first-run wizard may redirect away; force below
|
|
("welcome", "/welcome", {}),
|
|
("today", "/today", {}),
|
|
("cv", "/cv", {}),
|
|
("research", "/research", {}),
|
|
("applications", "/applications", {}),
|
|
("detail", f"/applications/{detail_id}", {}),
|
|
]
|
|
|
|
with sync_playwright() as p:
|
|
browser = p.chromium.launch(args=["--disable-dev-shm-usage"])
|
|
page = browser.new_page(viewport={"width": 1440, "height": 900},
|
|
device_scale_factor=2)
|
|
for name, path, _opts in shots[1:]: # skip the "/" duplicate
|
|
try:
|
|
page.goto(BASE + path, wait_until="networkidle", timeout=30000)
|
|
page.wait_for_timeout(1200)
|
|
# close possible wizard redirect back
|
|
if name != "welcome" and page.url.endswith("/welcome"):
|
|
page.goto(BASE + path, wait_until="networkidle", timeout=30000)
|
|
page.wait_for_timeout(800)
|
|
page.screenshot(path=f"{OUT}/{name}.png")
|
|
print("shot:", name, "<-", page.url)
|
|
except Exception as e:
|
|
print("FAILED:", name, type(e).__name__, str(e)[:150])
|
|
|
|
# Interaction shot: open the Tailor CV panel on an approved application
|
|
approved = next((a for a in apps if a["state"] in ("approved", "interviewing")), apps[0])
|
|
try:
|
|
page.goto(f"{BASE}/applications/{approved['id']}", wait_until="networkidle", timeout=30000)
|
|
page.wait_for_timeout(1000)
|
|
btn = page.locator('[data-testid="tailor-cv-btn"]')
|
|
btn.click(timeout=8000)
|
|
page.wait_for_selector('[data-testid="tailor-panel"]', timeout=20000)
|
|
page.wait_for_timeout(800)
|
|
page.screenshot(path=f"{OUT}/detail-tailor.png")
|
|
print("shot: detail-tailor")
|
|
except Exception as e:
|
|
print("FAILED: detail-tailor", type(e).__name__, str(e)[:150])
|
|
|
|
browser.close()
|
|
print("DONE")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|