Files
root 7a62b0b340 Backend Aug 22-25: AI analysis, welcome-packet templating, LetterStream, DocuSeal, staff RBAC + tier gate
- analysis.py: deterministic claim scorer + /analyze /approve /letter /advance-tier endpoints (auto-runs on intake)
- packet.py + packet_fields.json: welcome-packet templating engine (6 onboarding docs, field catalog)
- letterstream.py + letters.py: certified-mail send pipeline + letter lifecycle (webhook verified)
- docuseal.py: DocuSeal signing integration
- staff.py/models.py/schema.sql/auth.py: approval actor from staff key, tier gate (APPROVED+ACTIVE+onboarding docs), onboarding_docs table
- frontend/: dependency-free static portal (intake, magic-link login/verify, dashboard)
- landing-mockups/: 4 design-stance mockups + favicons
- legal/: aup/privacy/sms-terms/terms HTML
- docs/: letter-queue scope, letterstream API contract, 6 welcome-packet templates
- review-dre-landing-2026-08-21.md: 3-variant landing feedback sprint
- compliance/DRE_Compliance_Manual.md: updated

Source synced from deployed /opt/dre-portal/app/ (was 4 days ahead of git).
2026-08-26 02:26:33 -04:00
..

DRE Customer Portal - Static Frontend

Dependency-free HTML/CSS/JS frontend for the Debt Recovery Experts (DRE) customer portal. No build step, no frameworks, no npm. Every page loads css/dre.css and (where interactive) js/dre-api.js via plain <script> tags, and calls the backend using relative /api/* paths only. This is intended to be dropped behind Caddy, which proxies /api/* to the FastAPI backend and serves everything else as static files on the two subdomains below.

This directory is a DESIGN-ONLY deliverable: no deploy, no Caddy/systemd config, no backend changes were made or are included here.

Files created

/root/projects/dre/frontend/
  index.html          Public intake form
  login.html          Client login (request magic link)
  verify.html         Magic-link verification landing page
  dashboard.html       Client dashboard (claims list + detail + upload + messages)
  css/dre.css         Shared stylesheet for all four pages
  js/dre-api.js        Shared JS helper: fetch wrapper, session storage, formatting
  README.md           This file

Route / subdomain mapping

File Serves at Auth required
index.html portal.debtrecoveryexperts.com/ (site root) No (public)
login.html my.debtrecoveryexperts.com/ (site root) No (public)
verify.html my.debtrecoveryexperts.com/verify No (public, consumes a one-time token)
dashboard.html my.debtrecoveryexperts.com/dashboard Yes (redirects to login.html if no session)
css/dre.css /css/dre.css on both subdomains -
js/dre-api.js /js/dre-api.js on both subdomains -

Caddy is expected to:

  1. Serve portal.debtrecoveryexperts.com from this directory with index.html as the site index.
  2. Serve my.debtrecoveryexperts.com from this directory with login.html as the site index, verify.html at /verify, and dashboard.html at /dashboard.
  3. Reverse-proxy /api/* on both subdomains to the FastAPI backend (same-origin so the JS fetch() calls need no CORS config and no hardcoded backend host).

No Caddyfile is included per the design-only constraint; this table is the spec for whoever wires up routing.

API contract implemented (verified against backend/main.py, backend/models.py, backend/claims.py, backend/intake.py)

  • POST /api/intake - body {client:{company_name, contact_name, email, phone?}, debtor:{name, business_type, contact_email?, contact_phone?, physical_address?}, claim:{amount_cents, description?, client_reference?, invoice_date?}, tos_accepted:true}. Response {claim_number, client_number, status, message}. Amount is collected as dollars in the UI and converted to integer cents client-side before posting.
  • POST /api/auth/request - body {email}. Always returns the anti-enumeration message {message: "If an account exists..."}; UI always shows the "check your email" state on a 2xx response regardless of whether the account exists.
  • POST /api/auth/verify - body {token}. Response {session_token, expires_at, client:{client_number, company_name, contact_name}}. Token is read from ?token= in the URL, stripped from the address bar immediately (history.replaceState) before the API call, and the session token is stored in localStorage under the key dre_session_token.
  • GET /api/auth/me - Authorization: Bearer <token>. Response {client:{...}, claim_count}.
  • POST /api/auth/logout - Authorization: Bearer <token>. Clears local storage and redirects to login regardless of response.
  • GET /api/claims - Response {claims:[{claim_number, status, status_label, tier, amount_cents, amount_display, debtor_name, created_at, date_resolved}]}.
  • GET /api/claims/{claim_number} - Response includes status_label, tier_step (used to render the 4-step progress bar), debtor:{name, business_type}, documents:[...], notes:[...]. Note: this endpoint does not return a claim created_at; the UI shows date_assigned (or "Not yet assigned") instead of a submission date.
  • POST /api/claims/{claim_number}/documents - multipart FormData with field name file (matches UploadFile = File(...) param name in claims.py). Client-side pre-checks: 20 MB max, extensions .pdf .jpg .jpeg .png .doc .docx (mirrors ALLOWED_EXT in claims.py).
  • POST /api/claims/{claim_number}/messages - body {subject, content}. subject must be one of the fixed MESSAGE_SUBJECTS enum from models.py; rendered as a <select> with those exact values. (The backend's client-facing endpoint for adding case correspondence is /messages, not /notes - the dashboard's "Message the Team" panel targets this and refreshes the notes list on success, since messages are stored as shared case notes.)

Error envelope handled uniformly everywhere: {"error": {"code", "message"}}. Specific codes handled: validation_error (422, including per-field mapping on the intake form for client.* / debtor.* / claim.* / tos_accepted locations), unauthorized (401, clears session + redirects to login), not_found (404), rate_limited (429), payload_too_large / unsupported_media_type / conflict (document upload).

Compliance / safety notes

  • No field anywhere collects SSN, full bank account, or card numbers. The intake form has an explicit on-page warning, and the backend's PII regex rejection (validation_error) is surfaced inline on the matching form field when triggered.
  • All user-authored or backend-sourced free text (case notes, messages) is rendered via textContent, never innerHTML, so it cannot execute as markup even though the backend also HTML-escapes it server-side.
  • Wording throughout intake/login/dashboard is neutral and professional (e.g. "recovery review", "claim", "case notes") - no aggressive or threatening collector language, consistent with FDCPA/TDCPA constraints.
  • Dollar amounts are always rendered from integer cents (amount_cents / 100), formatted as USD via DRE.formatCentsUSD().

What was NOT done (out of scope for this seat)

  • No deployment: nothing was copied to /opt/dre-portal or any web root.
  • No Caddy or systemd configuration was created or modified.
  • No backend files were modified (only read for contract verification).
  • No database was created, seeded, or touched.

Manual smoke test performed

Served this directory locally with python3 -m http.server (throwaway, not part of the deliverable) and confirmed all six files return HTTP 200, all inline <script> blocks parse without syntax errors (node -c equivalent check), all HTML tags balance, and every getElementById() reference in the JS resolves to an element that actually exists in the corresponding HTML file.