#!/usr/bin/env python3 """Seed a realistic DEMO project into the Work Package Suite database via the API. Creates one project, a complete SOP, and a spread of Work Packages that exercise the features and dashboard: an issued package, a gated (open-constraint) package, a multi-discipline master with its split instances (A/B/C), an overdue package, and an over-threshold draft. Use it to prove the SQL + Python layer end-to-end and to have data to inspect. USAGE python3 server/seed_demo.py https://wp-suite.company.local --insecure docker compose exec api python /app/server/seed_demo.py http://localhost:8000 python3 server/seed_demo.py https://wp-suite.company.local --clean # remove DEMO-* projects IMPORTANT — what shows where: * The DEMO **project** is API/SQL-backed, so it appears in the home-page project picker immediately (proves the projects → SQL path in the UI). * The DEMO **SOP and Work Packages** are written to SQL too, but the current front end still reads SOPs/WPs from the browser (localStorage), so they will NOT render in the WP Creator / Dashboard yet — that's the pending Phase 2 wiring. Verify them at the SQL/API layer instead: python3 server/smoketest.py # automated end-to-end check docker compose exec db psql -U wpsuite -d wpsuite \ -c "select number,subject,status from work_packages order by number;" """ import argparse import json import ssl import sys import urllib.error import urllib.request BASE = "" CTX = None DEMO_NUMBER = "DEMO-001" # project number prefix used to find/clean demo data def call(method, path, body=None): url = BASE + path data = json.dumps(body).encode() if body is not None else None req = urllib.request.Request(url, data=data, method=method, headers={"Content-Type": "application/json", "Accept": "application/json"}) try: with urllib.request.urlopen(req, context=CTX, timeout=20) as r: raw = r.read().decode(); status = r.status except urllib.error.HTTPError as e: raw = e.read().decode(); status = e.code try: parsed = json.loads(raw) if raw else None except ValueError: parsed = raw return status, parsed def constraints(open_names=()): base = ["Safety & Permitting", "Quality Control / Inspection", "IFC Drawings & Specs", "Schedule", "Materials (on site, bagged & tagged)", "Work Access & Laydown"] return [{"name": n, "status": ("open" if n in open_names else "cleared"), "comment": ("awaiting delivery" if n in open_names else "")} for n in base] def main(): global BASE, CTX ap = argparse.ArgumentParser(description="Seed a demo project into the Work Package Suite") ap.add_argument("base_url", nargs="?", default="http://localhost:8000", help="Site root, no /api (default: http://localhost:8000)") ap.add_argument("--insecure", action="store_true", help="skip TLS verification") ap.add_argument("--clean", action="store_true", help="delete existing DEMO-* projects and exit") args = ap.parse_args() BASE = args.base_url.rstrip("/") if args.insecure: CTX = ssl.create_default_context(); CTX.check_hostname = False; CTX.verify_mode = ssl.CERT_NONE # health gate try: st, _ = call("GET", "/api/health") except urllib.error.URLError as e: print(f"ABORT: cannot reach {BASE}/api/health — {e}"); return 1 if st != 200: print(f"ABORT: /api/health returned {st}"); return 1 # --clean: remove any prior demo projects (cascade removes their SOP + WPs). # archived=all because /api/projects hides archived projects by default — an # archived DEMO project is still a DEMO project, and --clean has to find it. # (Deleting one is still allowed; only writes to its contents are frozen.) st, projects = call("GET", "/api/projects?archived=all") demos = [p for p in (projects or []) if str(p.get("number", "")).startswith("DEMO-")] if args.clean: for p in demos: call("DELETE", f"/api/projects/{p['id']}") print(f"Removed {len(demos)} DEMO project(s).") return 0 if demos: print(f"Note: {len(demos)} DEMO project(s) already exist. Run with --clean first to avoid duplicates.\n") # 1) Project st, proj = call("POST", "/api/projects", { "name": "DEMO — Micron INC (test data)", "number": DEMO_NUMBER, "client": "Micron Technology, Inc.", "division": "Semiconductor", "site": "Boise, ID — Fab", "created_by": "seed_demo"}) pid = proj["id"] print(f"Project: {proj['name']} ({pid})") # 2) SOP (complete) st, sop = call("POST", "/api/sops", { "project_id": pid, "name": "DEMO SOP", "number": DEMO_NUMBER, "complete": True, "created_by": "seed_demo", "data": {"governance": {"woFormat": "WP##-[Sector]-[TYPE]", "disciplines": ["Mechanical", "Electrical", "Tech"], "discMode": "choice", "instanceSuffix": "letter", "woSize": "Standard — 3–5 days (≈40–80 hrs)", "sizeHoursMax": "80"}}}) sid = sop["id"] print(f"SOP: complete ({sid})") # 3) Work packages def wp(number, subject, typ, status, data, parent_id=None): body = {"project_id": pid, "sop_id": sid, "number": number, "subject": subject, "type": typ, "status": status, "created_by": "seed_demo", "data": data} if parent_id: body["parent_id"] = parent_id st, w = call("POST", "/api/wps", body) print(f" WP {number:<16} {status:<12} {subject}") return w # a) issued, all clear wp("WP01-1P-CONDUIT", "1P horn/strobe conduit", "Conduit Install", "Issued", {"disciplines": ["Electrical"], "hours": "40", "actualHrs": "", "constraints": constraints(), "due": "2026-06-30"}) # b) gated — one open constraint, still Scheduled wp("WP02-1P-WIRE", "1P wire pull", "Wire Pull", "Scheduled", {"disciplines": ["Electrical"], "hours": "60", "actualHrs": "", "constraints": constraints(open_names=["Materials (on site, bagged & tagged)"]), "due": "2026-07-04"}) # c) multi-discipline master + split instances (master excluded from metrics) master_id = "wp_demo_master_chiller" instances = [("WP03-CHILLER_Mech", "Mechanical", "A", "Mechanical Install", "In Progress"), ("WP03-CHILLER_Elec", "Electrical", "B", "Wire Pull", "Scheduled"), ("WP03-CHILLER_Tech", "Tech", "C", "Terminations", "Draft")] child_ids = [] for num, disc, label, typ, status in instances: cid = f"wp_demo_{label.lower()}" child_ids.append(cid) body = {"project_id": pid, "sop_id": sid, "parent_id": master_id, "id": cid, "number": num, "subject": "Chiller skid — " + disc, "type": typ, "status": status, "created_by": "seed_demo", "data": {"disciplines": [disc], "instanceOf": master_id, "instanceLabel": label, "parentNumber": "WP03-CHILLER", "hours": "50", "actualHrs": "", "constraints": constraints(), "due": "2026-07-10"}} call("POST", "/api/wps", body) print(f" WP {num:<16} {status:<12} (instance {label})") wp("WP03-CHILLER", "Chiller skid (multi-discipline master)", "Mechanical Install", "Scheduled", {"disciplines": ["Mechanical", "Electrical", "Tech"], "split": True, "children": child_ids, "hours": "150", "constraints": constraints(), "due": "2026-07-10"}) call("POST", "/api/wps", {"project_id": pid, "sop_id": sid, "id": master_id, "number": "WP03-CHILLER", "subject": "Chiller skid (multi-discipline master)", "type": "Mechanical Install", "status": "Scheduled", "created_by": "seed_demo", "data": {"disciplines": ["Mechanical", "Electrical", "Tech"], "split": True, "children": child_ids, "hours": "150", "constraints": constraints(), "due": "2026-07-10"}}) # d) overdue, in progress wp("WP04-2P-TERM", "2P terminations", "Terminations", "In Progress", {"disciplines": ["Tech"], "hours": "30", "actualHrs": "20", "constraints": constraints(), "due": "2026-06-10"}) # past today (2026-06-16) → overdue # e) over-threshold draft (hours > 80) wp("WP05-3P-PANEL", "3P panel install", "Panel Install", "Draft", {"disciplines": ["Electrical"], "hours": "120", "actualHrs": "", "constraints": constraints(open_names=["Schedule"]), "due": "2026-07-20"}) # metrics readback st, m = call("GET", f"/api/wps/metrics?project_id={pid}") print(f"\nMetrics (masters excluded): {m}") print(f"\nDone. The DEMO project '{proj['name']}' now appears in the home-page picker.") print("SOP/WPs are in SQL (see header note) — verify with smoketest.py or psql.") print("Remove later with: python3 server/seed_demo.py --clean") return 0 if __name__ == "__main__": sys.exit(main())