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