"""Synthetic enterprise portal (runs ON THE EXECUTOR NODE, loopback only). Generic: driven by the scenario's portal/portal.json.
Serves: login, document library (downloads from inputs/), record tables (HTML + JSON API), forms whose submissions are appended to
out/portal_submissions.json. It never holds information absent from inputs/ (information constancy across arms).
Usage: python -m wp42.portal --workspace
--port [--token ]"""
from __future__ import annotations
import argparse, json, os, secrets, time
from flask import Flask, request, redirect, send_from_directory, session, abort, jsonify, Response
HTML = """{org} — Enterprise Portal
{org}
{body}"""
def make_app(workspace: str) -> Flask:
cfg = json.load(open(os.path.join(workspace, "portal", "portal.json"), encoding="utf-8"))
app = Flask(__name__); app.secret_key = secrets.token_hex(16)
inputs = os.path.join(workspace, "inputs"); out_dir = os.path.join(workspace, "out"); os.makedirs(out_dir, exist_ok=True)
user = cfg.get("user", {"username": "analyst", "password": "analyst"})
records = cfg.get("records", {}); forms = {f["id"]: f for f in cfg.get("forms", [])}
docs = cfg.get("documents", [])
nav = "".join(f'{k.title()}' for k in records)
def page(body): return HTML.format(org=cfg.get("org", "Portal"), nav=nav, body=body)
def authed(): return session.get("u") == user["username"]
@app.before_request
def _gate():
if request.path.startswith(("/login", "/static", "/health")): return None
if not authed(): return redirect("/login")
@app.route("/health")
def health(): return jsonify({"ok": True, "org": cfg.get("org"), "documents": len(docs), "records": {k: len(v) for k, v in records.items()}, "forms": list(forms)})
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
if request.form.get("username") == user["username"] and request.form.get("password") == user["password"]:
session["u"] = user["username"]; return redirect("/")
return page("
")
@app.route("/documents//download")
def download(doc_id):
d = next((x for x in docs if x["id"] == doc_id), None)
if not d: abort(404)
rel = d["path"]; full = os.path.normpath(os.path.join(inputs, rel))
if not full.startswith(os.path.normpath(inputs)): abort(403)
return send_from_directory(os.path.dirname(full), os.path.basename(full), as_attachment=True)
@app.route("/api/documents")
def api_docs(): return jsonify(docs)
@app.route("/records/")
def rec_html(entity):
rows = records.get(entity)
if rows is None: abort(404)
cols = list(rows[0].keys()) if rows else []
body = "".join("
")
@app.route("/forms/", methods=["GET", "POST"])
def form(form_id):
f = forms.get(form_id)
if not f: abort(404)
if request.method == "POST":
data = request.get_json(silent=True) or {k: v for k, v in request.form.items()}
rec = {"form_id": form_id, "submitted_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "data": data}
p = os.path.join(out_dir, "portal_submissions.json")
cur = json.load(open(p)) if os.path.exists(p) else []
cur.append(rec); json.dump(cur, open(p, "w"), indent=1)
if request.is_json: return jsonify({"ok": True, "submission": rec})
return page(f"
{f['title']}
Submission recorded.
{json.dumps(rec, indent=1)}
")
fields = "".join(f"" for x in f["fields"])
return page(f"
{f['title']}
API: POST JSON to this URL.
")
return app
LOGIN = "
Sign in
"
def main():
ap = argparse.ArgumentParser(); ap.add_argument("--workspace", required=True); ap.add_argument("--port", type=int, required=True)
a = ap.parse_args()
app = make_app(a.workspace)
app.run(host="127.0.0.1", port=a.port, debug=False, use_reloader=False, threaded=True)
if __name__ == "__main__":
main()