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
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user