Phase B of honest-cam needs a front-end for the actual humans this is built for: unit owners at a property managed on Honest CAM. This PR is Phase B Step 6 — the minimum viable owner portal. FastAPI backend, HTMX front-end, magic-link auth, Stripe Checkout for payments, and a document library for shared records.
What it does
- Dashboard — current balance, 30/60/90 aging breakdown, monthly assessment amount, and the most recent transactions. Refreshes itself every 60 seconds via an HTMX swap so the number stays live without a full page reload.
- Payments — one button kicks off a Stripe Checkout session. Stripe handles the card capture, PCI scope, and the redirect flow; the portal just receives the success/cancel and (in a future PR) reconciles the event against the owner's ledger.
- Document library — shared docs (budgets, board minutes, insurance certificates) with per-unit access control. Path traversal protection is enforced at the route layer; every download path is resolved against a whitelist before the file is served.
- HTMX, not SPA. Zero custom JavaScript in this PR. HTMX swaps HTML fragments over the wire. The codebase is Jinja2 templates and Python all the way down.
Magic-link auth, no passwords
Passwords are a liability I don't want. The flow:
- Owner enters their email on
/login. - Backend looks the email up in the property's owner registry, generates a signed token with
itsdangerous(15-minute expiry), and logs the magic link to stdout. Production email sending is deferred to Step 7 — SendGrid plumbing is a separate concern and I don't want to block this PR on it. - Owner clicks the link, server verifies the signature and expiry, issues a session cookie (also signed with
itsdangerous), and redirects to the dashboard. - Session cookies are short-lived and carry the owner's unit number and email. No server-side session store needed.
The signing key is per-deployment, loaded from env. Signed tokens are stateless, which keeps the portal trivially horizontally scalable once that ever matters.
New files
| File | Purpose |
|---|---|
portal/app.py | FastAPI app factory |
portal/auth.py | Magic link + session token signing / verification |
portal/deps.py | FastAPI dependency injection (settings, auth, templates) |
portal/routes.py | All routes: login, dashboard, pay, documents |
portal/stripe_pay.py | Stripe Checkout session creation + webhook verification |
portal/models.py | OwnerSession, PaymentIntent |
portal/templates/ | 6 Jinja2 templates (base, login, dashboard, documents, payment) |
docs/feature-owner-portal.md | Architecture decisions + design doc |
CLI
honestcam portal bamboo-house # localhost:8000
honestcam portal bamboo-house --reload # dev mode with auto-reloadDeliberately not in this PR
- Email sending. Magic links get logged to console; SendGrid wiring is Step 7.
- Real Xero ledger data. The dashboard reads from a fixture right now and will show $0 for everyone until the Xero client is plugged in — that's the next integration.
- Stripe webhook handler. The checkout session creation works and a success page renders; the webhook that actually records the payment back into the ledger is also Step 7.
Shipping the portal on mock data first lets me put the UX in front of actual owners and collect feedback on the shape of the dashboard before I start moving money.
Verification
- 52 new tests, 324 total:
- 15 auth tests (magic-link generation and verification, session token lifecycle, email lookup).
- 6 Stripe tests (checkout creation, webhook signature verification, error paths).
- 31 route tests (login flow, dashboard, payments, documents, path traversal rejection).
- Full suite green, ruff clean.