diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index ba9958f..6c2313b 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -127,6 +127,28 @@ working. It cleans up after itself (the test project and its SOP/WPs are deleted via cascade); a single tagged test comment remains (there's no comment delete endpoint). +### Loadable demo project + +`server/seed_demo.py` populates a realistic **DEMO** project (a complete SOP plus +a spread of Work Packages: issued, gated, a multi-discipline master with split +instances, an overdue one, an over-threshold draft) so there's data to look at. + +```bash +python3 server/seed_demo.py https://wp-suite.company.local --insecure +python3 server/seed_demo.py https://wp-suite.company.local --clean # remove it later +``` + +> **What shows where:** the DEMO **project** is API/SQL-backed, so it appears in +> the home-page project picker right away (this is the visible proof that the +> projects → SQL path works end-to-end). The DEMO **SOP and Work Packages** are +> written to SQL too, but the current front end still reads SOPs/WPs from the +> browser, so they won't render in the Creator/Dashboard until the Phase 2 +> wiring. Inspect them at the SQL layer with `smoketest.py` or: +> ```bash +> docker compose exec db psql -U wpsuite -d wpsuite \ +> -c "select number, subject, status from work_packages order by number;" +> ``` + --- ## What is stored in SQL today diff --git a/server/seed_demo.py b/server/seed_demo.py new file mode 100644 index 0000000..82ca6e2 --- /dev/null +++ b/server/seed_demo.py @@ -0,0 +1,175 @@ +#!/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) + st, projects = call("GET", "/api/projects") + 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())