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

126 lines
7.4 KiB
Markdown

# 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.