- /domains is one table: app-linked domains + live zones from connected DNS
providers (Cloudflare, ...) with provider badges. New GET /dns/portfolio
(aggregates provider zones, marks which are adopted) and idempotent
POST /dns/adopt (lazy-materialize a managed zone on demand).
- Every row opens one drawer = the unified DNS records surface
(components/domains/DomainDnsPanel): live Cloudflare records via
GET /dns/provider-records (no adopt needed), type badges, proxied=>HTTPS hint,
inline Add record, deep-links to Cloudflare ops / full DNS page. Fixed-layout
table (no horizontal scroll); actions moved into the drawer header.
- Registration expiry/registrar from Cloudflare Registrar + lazy RDAP fallback
(GET /dns/registration), persisted/cached in the new domain_registrations
table and merged into the portfolio so it survives a refresh. New
frontend/src/utils/expiry.js formatExpiry() date util.
- Connections moved to admin-only (Settings Admin group + isAdmin gate;
@admin_required on /api/v1/connections); CF connection card links to Domains.
- Cloudflare client: expose each zone's account_id; add list_registrar_domains.
- Tests: backend/tests/test_dns_portfolio.py.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>