# ProspectOS web — Phase 9 boundary
Self-contained static frontend for the Prospect Platform API. There is no bundler or runtime dependency: serve this directory with any static HTTP server. The UI supports a manual, tenant-scoped review workflow, Phase 4 MVP job monitor, and Phase 8 scan-result/history presentation when supplied by the API. The browser does not fetch targets, submit forms, execute scan JavaScript, or send outreach; SSRF controls and budgets are server-side.
## Configure and run
The API base is configurable before `app.js` runs:
```html
```
If not set, the UI uses `localStorage.prospect_api_base` when present; otherwise it targets the same origin. The dashboard requires an authenticated API session and shows the login screen until `/api/v1/auth/me` succeeds.
## Phase 3 UI contract
- The explorer requests tenant-scoped business pages from `GET /api/v1/businesses` and sends bounded pagination plus supported search/score/status filters to the API. Filtering is not a substitute for server-side authorization.
- Selecting a row loads the tenant-scoped detail view, including child intelligence/evidence records, provenance/source labels, confidence/freshness, current pipeline state, notes, and relevant audit/activity context when available.
- Add prospect, add intelligence, change pipeline state, and add note are explicit manual actions. The API records the acting user and applies permission, tenant, validation, deduplication, and suppression rules server-side.
- Evidence labels describe stored observations and their provenance. The UI must not present them as the result of automated discovery, DNS lookup, website crawling, or verification unless a future approved integration explicitly supplies that evidence.
- Review and suppressed states remain safety states. The UI shows outreach as unavailable; there is no send button, message composer, sender, or outreach endpoint.
- The CSV control is preview-only and local to the browser. Selecting a file does not persist rows or send them to the API.
The API remains the source of truth for tenant isolation, pagination bounds, filters, pipeline transitions, notes, audit records, and suppression. See `apps/api/README.md` for the route contract.
## Phase 6 normalization and deduplication UI contract
The UI may display the API's normalized SA phone and location values alongside the original observation, normalization version, and any ambiguity warning. It must not silently replace the source value or imply that a canonical form verifies identity. Duplicate candidates must show deterministic score, threshold band (`strong` `>=0.90`, `review` `0.75–0.8999`, or `none` `<0.75`), and explainable matching reasons.
Suggestions are review aids only. A merge flow must identify the surviving record, list all parents/children/evidence/notes/source records that will be preserved, show conflicts, and require an explicit human confirmation before calling an authorized API mutation. The UI must offer rejection and, where implemented, reversal using the merge snapshot; it must never auto-merge based on a score. Candidate, merge, snapshot, and audit data are tenant-scoped by the API, not by hidden UI state.
The current static MVP requests `/matches`, renders a **Human review required** list with confidence/reasons, asks for **Confirm merge**, and displays merge history with **Reverse merge** when the API marks it reversible. The API remains authoritative; these controls are not a substitute for server-side authorization. Existing normalization and match display remain suggestion-only; no merge happens without explicit operator confirmation.
## Phase 8 website-scanning UI contract
The UI may request a tenant-scoped scan through an authenticated API route when enabled and render the returned classification, status, redirect chain, observed time, scanner/policy version, cache freshness, applied budgets, and uncertainty/error reasons. The server permits only `http` and `https` targets. It must label cached data as cached/stale rather than “live,” keep `unknown`, `blocked`, `partial`, `timeout`, and `error` distinct from positive observations, and never turn a conservative classification into identity, ownership, consent, deliverability, or outreach permission.
The browser must not directly fetch arbitrary target URLs, follow redirects for scanning, submit forms, send cookies/credentials, execute target JavaScript, or expose response bodies unnecessarily. Scan history and cache controls are tenant-scoped API capabilities, not hidden client state. A result that is budget-limited or incomplete must remain visibly incomplete; no UI timer may imply that a scan completed.
## Phase 5 source UI contract
The web client may display registered source metadata, query mode, approval/terms state, rate-limit status, retention class, health, and circuit state returned by the API. It must label `dry_run` as a plan/validation result and distinguish operator-supplied CSV/manual references from independently verified evidence. It must not offer a live-source control unless the API reports explicit approval and operational enablement; client visibility is never an authorization control.
CSV and manual reference workflows must show source attribution, adapter/version, observed time, and any retention/redaction status. Raw source payloads should be hidden or minimized in the UI and remain tenant-scoped. A circuit-open or rate-limited source must be presented as unavailable/deferred, not as an empty discovery result. The current static client has no network discovery implementation; these are display and contract requirements for a future approved integration.
## Phase 4 job/live-log UI contract
A future job view should show `queued`, `running`, `succeeded`, `failed`, or `cancelled`, the current attempt, timestamps, safe error text, and a clear terminal state. It should display persisted events in sequence order, resume from the last cursor after refresh/reconnect, and tolerate duplicate events. Create/retry requests should send an idempotency key and show the returned job identity rather than starting duplicate work.
The preferred live path is SSE backed by the persisted event cursor; polling with bounded backoff is the required fallback and should be used for browsers/proxies that do not support SSE. Cancel is a cooperative action with an explicit pending/terminal result; retry is available only when the API authorizes it and must be presented as a new attempt/lineage. The UI must never infer progress from timers or claim work completed because a request was accepted.
The current client renders job status/counts, detail, structured errors, progress, and event timelines, and polls the jobs collection while queued/running work exists. It exposes authorized cancel/retry affordances based on the API response. There is no SSE client yet; polling is the current fallback and should remain available after SSE is introduced. The backend's SQLite/in-process worker is MVP-only. Do not add a Redis/Celery dependency by implication or label that worker production-ready.
## Browser verification
1. Start the API from `apps/api` with `python3 app/main.py`.
2. Serve this directory: `python3 -m http.server 8080 --directory apps/web`.
3. Open `http://127.0.0.1:8080`, with `window.API_BASE` set to `http://127.0.0.1:8000` using a tiny pre-load edit or browser devtools.
4. Sign in and confirm the header changes to **API connected**, tenant metrics populate, and the explorer renders a bounded page with search, score, status, and pagination controls.
5. Select a row and confirm the detail view keeps the business, child intelligence, evidence provenance, confidence/freshness, pipeline, notes, and audit context associated with that tenant.
6. Add or update only through the explicit manual controls. Confirm the refreshed detail/list state reflects the API response and that a viewer cannot mutate records.
7. Confirm review and suppressed rows show **Outreach unavailable** with the appropriate reason. Confirm there is no outreach/send endpoint or button.
8. Select a CSV and confirm a local, preview-only table appears without a network request or persistence.
9. Resize below 700px to verify the collapsible nav, stacked panels, and horizontally scrollable explorer/detail content.
A zero-dependency static smoke page (`smoke-test.html`) checks the key DOM contract in an iframe and reports pass/fail. Browser smoke coverage should treat pagination, detail child records, provenance rendering, pipeline/notes actions, and the disabled outreach state as separate checks.
## Phase 9 contact-observation UI contract
When the API supplies Phase 9 results, the UI may render contacts extracted from the approved/public official site and same-site contact/about pages. Display source URL/page context, extraction method, observed time, extractor/policy version, confidence and reasons, syntax status, role classification, free-mail classification, and independent MX/DNS status/freshness. Use explicit labels such as **Observation**, **Human review required**, and **Unknown**; never label a candidate verified, deliverable, owned, consented, or ready for outreach. The UI must show suppressed contacts as **Do not contact**, preserve the suppression reason, and never hide or override suppression through filters, refreshes, exports, or cached results.
Extraction is not a browser crawler. The browser must not fetch target pages directly, submit contact forms, execute target JavaScript, send credentials/cookies, probe SMTP, send validation messages, or expose an outreach/send control. Render bounded/partial/blocked/timeout/error results distinctly from an empty successful result, including candidate/page/byte/time limit reasons. Do not display false-positive candidates from assets, scripts/styles, example/test/placeholder values, tracking addresses, malformed schemes, or unrelated third-party pages.
The API remains authoritative for official-site scope, tenant isolation, suppression enforcement, limits, retention, provenance, and permissions. A cached extraction must show its observed time and freshness, never “live.” Candidate confidence, role/free-mail labels, syntax, and MX/DNS uncertainty are review metadata only and cannot enable a contact action.
## Phase 10 scoring UI contract
The UI may display server-provided `score`, `priority_band`, and `eligibility` as separate fields. It should show the active rule-set ID/version, calculated time/freshness, explanation factors with their points/weights, exclusions, and stale/uncertain reasons. A band is a triage label, not permission to contact; never derive or override these values solely in browser code.
Render eligibility independently and prominently: **Eligible**, **Ineligible**, or **Unknown/review required** must not be collapsed into a score band. Show suppression/do-not-contact as a hard, persistent state that wins over score, verification, pipeline, cached data, or refresh. Keep stale, expired, blocked, partial, and uncertain evidence visibly distinct from missing evidence and never present them as a positive or negative fact. There is no outreach control in this phase.
If the API exposes recalculation, the UI must show the requested rule-set/version, job/progress/partial state, actor/time, and before/after explanation or band changes; acceptance of a request is not completion. Recalculation history and audit details remain tenant-scoped server capabilities. Rule-set administration, activation, rollback, and eligibility policy are not client-side authorization controls.
Phase 10 remains a pilot display contract until the API supplies stable versioned rule metadata, reproducible input lineage, complete explanation payloads, explicit eligibility reasons, and audited recalculation results. Browser smoke coverage should include score/band disagreement with eligibility, suppression precedence, stale/uncertain rendering, version changes, partial recalculation, and cross-tenant non-disclosure.
## Phase 11 dashboard and review workflow UI contract
Phase 11 is the dashboard/review presentation contract for **saved filters**, a **review queue**, and bounded bulk actions. Saved filters must show a named, human-readable summary of the exact search/status/score/pipeline/eligibility predicate, sort, and page-size settings. Save/load/update/delete controls must reflect API authorization and tenant scope; the browser must not treat a filter ID, hidden field, or local-only copy as permission. If filters are shared, the UI must show that they are tenant-scoped and read-only or editable as returned by the API.
The review queue must make its scope visible: current filter name or predicate, matching-set versus current-page count, ordering, pagination/cursor state, and `has_more`. A row can be selected only from the current tenant-scoped result. Suppressed/do-not-contact state must remain prominent and disable contact-related actions; merged/non-active records are not merge-eligible. Merge suggestions remain **Human review required** and need an explicit confirmation dialog; no score or checkbox may auto-merge.
Bulk actions must show the bounded selection size and server maximum, provide a preview before confirmation, and report per-record success/skipped/failed outcomes. The UI must refresh or reconcile stale rows after execution, preserve suppression and eligibility reasons, and never describe a partial result as complete. A clickable count must navigate using the exact predicate that produced the count; distinguish full matching-set counts from page counts, and show loading/error/unavailable rather than zero. Count cards are navigation affordances, not authorization controls.
The UI must expose audit context for saved-filter changes, queue decisions, bulk preview/confirmation/execution, suppression/eligibility decisions, and merge/reversal: actor, time, bounded selection/filter snapshot, result totals, and safe reason/version metadata. It must not render secrets or unnecessary contact data. The API remains authoritative for tenant isolation, permissions, re-checks, idempotency, suppression precedence, and audit persistence.
The current Phase 11 client now renders saved-view controls, a review queue capped at 100 visible records, selectable rows, and explicit verify/reject bulk review actions. It also renders clickable dashboard metric cards. Current limitations are material: saved views can be created/loaded/deleted in the client but update is not exposed; queue selection is visible-row-only and the UI does not show a server maximum/preview/per-record outcomes; dashboard links use client filter shortcuts rather than a complete server predicate; and suppression/merge eligibility and audit results still depend on the API response. No bulk action sends outreach or auto-merges.
## Remaining limitations
The static client has no client-side crawler, scanner, contact extractor, enrichment scheduler, outreach integration, availability provider, or SSE delivery. It can display server-provided Phase 9 observations, but production still requires server-side official-site scoping, SSRF/DNS-rebinding/redirect controls, hard extraction/page/byte/time/candidate budgets, durable history/cache isolation and retention/deletion, abuse/rate controls, suppression regression tests, and authenticated provenance/audit coverage. For domain intelligence, display registrable-domain/PSL version and unresolved reasons, DNS status and freshness/TTL (not “available”), independent MX/NS/TXT uncertainty, and association confidence with explainable evidence. Never auto-attach candidate domains or treat `nxdomain`/`no_data` as availability. CSV preview is capped for display and is not an import workflow.