- 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).
6.8 KiB
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:
- Serve
portal.debtrecoveryexperts.comfrom this directory withindex.htmlas the site index. - Serve
my.debtrecoveryexperts.comfrom this directory withlogin.htmlas the site index,verify.htmlat/verify, anddashboard.htmlat/dashboard. - Reverse-proxy
/api/*on both subdomains to the FastAPI backend (same-origin so the JSfetch()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 inlocalStorageunder the keydre_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 includesstatus_label,tier_step(used to render the 4-step progress bar),debtor:{name, business_type},documents:[...],notes:[...]. Note: this endpoint does not return a claimcreated_at; the UI showsdate_assigned(or "Not yet assigned") instead of a submission date.POST /api/claims/{claim_number}/documents- multipartFormDatawith field namefile(matchesUploadFile = File(...)param name inclaims.py). Client-side pre-checks: 20 MB max, extensions.pdf .jpg .jpeg .png .doc .docx(mirrorsALLOWED_EXTinclaims.py).POST /api/claims/{claim_number}/messages- body{subject, content}.subjectmust be one of the fixedMESSAGE_SUBJECTSenum frommodels.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, neverinnerHTML, 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 viaDRE.formatCentsUSD().
What was NOT done (out of scope for this seat)
- No deployment: nothing was copied to
/opt/dre-portalor 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.