Files
verdicttank/ops-portal/CONTRACT.md
T
root 6ac8a185ef Add demo cases (HarvestLink v1/v2), subscription technical contract, portal + ops-portal HTML
- demo-case-harvestlink.md + v2: fictional staged proposals for product-marketing video (no PII)
- CONTRACT.md: subscription model / contact form / receipt fixes technical contract (read-only analysis 2026-08-19)
- portal/index.html + ops-portal/: customer portal + ops portal single-file builds
- .gitignore: demo-creds*.txt excluded
2026-08-26 02:26:50 -04:00

7.4 KiB

VerdictTank Staff Dashboard — Build Contract (2026-08-18)

Pinned contract for the ops.verdicttank.com staff dashboard. Three teams build against this. Do NOT invent fields; every field below exists in the live data or is explicitly new here.

Goal

Staff (IT Pro Partner internal) can sign in and view ALL VerdictTank submissions and their full results. Customers stay on my.verdicttank.com (product JWT). Staff use auth2 (Stack Auth / Hexclave). Owner-email fallback guarantees the two owners can always sign in.

Topology (unchanged)

  • API: Core, /opt/verdicttank/api.py, 127.0.0.1:8201, systemd verdicttank-api.service.
  • Submissions: one JSON file per review in /opt/verdicttank/data/*.json.
  • auth2: app3, auth2-api.itpropartner.com (API) / auth2.itpropartner.com (dashboard).
  • Python: /root/docker/super-search/venv/bin/python3. PyJWT imported as import jwt as pyjwt.

auth2 (Stack Auth) constants

  • Sign-in: POST https://auth2-api.itpropartner.com/api/v1/auth/password/sign-in body {"email": ..., "password": ...}. Headers: x-hexclave-publishable-client-key: zZhcrUZXs8EqihvXzl7vqvasrRlj7kWn6+eENpX+6Eo=, x-hexclave-access-type: client, x-hexclave-project-id: internal, Content-Type: application/json. Success returns {access_token, refresh_token, user_id}. access_token is OPAQUE (not a JWT).
  • Current user: GET https://auth2-api.itpropartner.com/api/latest/users/me headers x-hexclave-access-token: <access_token>, x-hexclave-access-type: client, x-hexclave-project-id: internal. Returns {id, display_name, primary_email}.
  • Team gate: GET https://auth2-api.itpropartner.com/api/latest/teams?user_id=me&query=verdicttank-it-staff (same access-token headers). Membership enforced server-side: items.length > 0 == member.
  • Publishable key above is the internal project's key (client-visible, not a secret).
  • Owner emails (hardcoded fallback, always admitted): g@germainebrown.com, info@itpropartner.com, shonuff@germainebrown.com. All three exist as auth2 ProjectUsers (usedForAuth=TRUE): g@ = seed admin, info@ = "IT Pro Partner Info", shonuff@ = "Sho'Nuff".

New admin endpoints (all prefixed /api/verdicttank/admin/)

POST /api/verdicttank/admin/login

Body {email, password}.

  1. Call auth2 sign-in (above). 200 -> parse access_token. Non-200 -> 401 {"detail":"Invalid credentials"}.
  2. users/me -> display_name, primary_email.
  3. Staff check: primary_email (or email) in OWNER_EMAILS, OR team gate returns items.length > 0.
  4. If staff: issue admin JWT (below), return {token, user:{email, name}}.
  5. If not staff: 403 {"detail":"Not authorized"}.

GET /api/verdicttank/admin/submissions (admin JWT)

Query: status (optional), q (optional: case-insensitive match on name/email/proposal_name), limit (default 100, max 500), offset (default 0). Returns {submissions:[...], total:N}. Each item: review_id, proposal_name, name, email, tier, tier_label, status, verdict, proposal_strength, investor_readiness, composite, submitted_at, completed_at. Use the same pattern as the existing /reviews handler: for path in sorted(DATA_DIR.glob("*.json"), reverse=True).

GET /api/verdicttank/admin/submissions/{review_id} (admin JWT)

Full detail — every field in /results/{id} PLUS the staff-only fields: review_id, status, proposal_name, name, email, user_id, tier, tier_label, submitted_at, completed_at, verdict, verdict_rationale, executive_summary, proposal_strength, investor_readiness, composite, divergence, dimensions, fatal_flaws, action_plan, kill_criteria, consensus, panel_audit, report_url, receipt_url, error. report_url = /api/verdicttank/report/{id} (only when status == complete, else null). receipt_url = /api/receipt/{id} (only when receipt_path exists).

GET /api/verdicttank/admin/stats (admin JWT)

Returns {total, completed, failed, queued, processing, by_tier:{tier:count,...}, by_status:{...}}. Compute from all *.json in DATA_DIR.

Admin JWT (separate from customer JWT)

  • Secret: VERDICTTANK_ADMIN_JWT_SECRET env, else persist secrets.token_urlsafe(48) to /opt/verdicttank/.admin_jwt_secret (mode 0600). DO NOT reuse the customer .jwt_secret.
  • Algo HS256, TTL 12 hours.
  • Claims: {sub: email, role: "staff", name: name, exp, iat}.
  • Dependency require_staff(authorization: str = Header(None)): decode with the admin secret, require role == "staff", else 401. Reuse the existing import jwt as pyjwt.

Submission JSON fields (already in data files)

review_id, status(queued|processing|complete|failed), proposal_name, name, email, tier, submitted_at, completed_at, verdict, verdict_rationale, executive_summary, proposal_strength, investor_readiness, composite, divergence, dimensions(list of {key,label,score}), fatal_flaws, action_plan, kill_criteria, consensus, panel_audit(list), report_path, receipt_path, progress, error, user_id(optional).

Existing helpers in api.py: load_submission(review_id), save_submission(review_id, sub), DATA_DIR, TIERS, tier_label(tier), current_user(authorization).

Staff frontend (ops.verdicttank.com) — single-file SPA

Served from Core Caddy (see infra). The SPA calls the API SAME-ORIGIN via the /api/* reverse proxy, so base = "" (relative), e.g. fetch('/api/verdicttank/admin/login', ...). No CORS, no cross-origin auth2 calls from the browser — all auth2 interaction is server-side in the API.

  • Login screen: email + password -> POST /api/verdicttank/admin/login -> store {token, user} in localStorage. Send Authorization: Bearer <token> on all admin calls.
  • 401/403 -> show access-denied / re-show login.
  • Dashboard: stats bar from /api/verdicttank/admin/stats; submissions table from /api/verdicttank/admin/submissions; search box (q), status filter (status), pagination.
  • Row click -> /api/verdicttank/admin/submissions/{id} -> detail view: two 0-100 scores, 10 dimensions (0-10 bars), verdict + rationale, executive summary, fatal flaws, action plan, kill criteria, consensus, panel audit (collapsible), links to report_url and receipt_url.
  • Download links: prepend window.location.origin to report_url / receipt_url.

Brand rules (non-negotiable): crimson #dc2626, Inter font (Google Fonts ok), dark/light toggle, NO vendor/model names anywhere, NO em/en dashes (use hyphens), no fabricated numbers (render real values only), footer "VerdictTank, a product of IT Pro Partner". Single-file HTML/CSS/JS, vanilla, no frameworks, no CDNs beyond Google Fonts.

Infra (Core + DNS + auth2)

  1. DNS: ops.verdicttank.com A record -> 152.53.192.33.
  2. Caddy (Core /etc/caddy/Caddyfile) — add block: ops.verdicttank.com { root * /var/www/verdicttank-ops; file_server; handle /api/* { reverse_proxy 127.0.0.1:8201 } header Cache-Control "no-cache" } and in the existing verdicttank.com block add handle /api/receipt/* { reverse_proxy 127.0.0.1:8201 } (fixes the receipt 404 for the customer portal).
  3. auth2 team verdicttank-it-staff + add g@germainebrown.com as member (non-blocking: owner fallback already admits the two owners).
  4. Write HEXCLAVE_PUBLISHABLE_KEY=zZhcrUZXs8EqihvXzl7vqvasrRlj7kWn6+eENpX+6Eo= and HEXCLAVE_PROJECT_ID=internal to /etc/verdicttank.env (API reads via os.getenv).

Deploy order (conductor enforces)

Backend module -> py_compile -> systemctl restart verdicttank-api -> Caddy reload -> DNS propagate -> frontend deployed to /var/www/verdicttank-ops/index.html -> e2e test.