Skip to content

Unify domains & DNS into one surface#49

Merged
jhd3197 merged 6 commits into
mainfrom
dev
Jun 23, 2026
Merged

Unify domains & DNS into one surface#49
jhd3197 merged 6 commits into
mainfrom
dev

Conversation

@jhd3197

@jhd3197 jhd3197 commented Jun 23, 2026

Copy link
Copy Markdown
Owner

ServerKit's DNS story had quietly sprawled into three pages — Domains, DNS Zones, and Dynamic DNS — that overlapped, disagreed, and greeted Cloudflare users with a confidently wrong "0 ServerKit records." This PR collapses all of it into one surface: /domains is now a single table of your app-linked domains plus every zone from a connected DNS provider, and each row opens one drawer that is the DNS records manager — live Cloudflare records, inline editing, per-record Dynamic DNS, export, and propagation checks, with nothing to learn twice. Zones are adopted lazily (the first edit materializes a managed zone, idempotently), so read-only viewing stays open to everyone while writes stay admin-gated. Registration expiry and registrar ride along too, sourced from Cloudflare Registrar with a cached RDAP fallback so the list shows real dates without hammering WHOIS on every load. Connections also moved under Administration, since a Cloudflare token is a server-wide credential, not a personal preference. Bundled in: a hardier serverkit update that tracks main and self-heals, a branded first-paint loader, fuller WordPress detail skeletons, and a couple of fixes for an SSL layout regression and a dashboard crash.

Highlights

  • One Domains page for everything. Your app domains and every zone from a connected DNS provider (Cloudflare today) now live in one table, with provider brand badges, a "Managed" tag for adopted zones, and a registration-expiry column.
  • The domain drawer is the only DNS surface now. Open any domain to see its live Cloudflare records (clearly split into your own vs ServerKit-managed), add records inline, export the zone, and check propagation — all without leaving the page. The underlying zone is adopted automatically the first time you edit it.
  • Dynamic DNS, one record at a time. Flip any A/AAAA record into a token-updatable host straight from the drawer; the one-time token and a ready-to-paste update URL are surfaced immediately, and you can regenerate or stop it in place.
  • Domain expiry at a glance. Registrar and expiry date come from Cloudflare Registrar with a WHOIS/RDAP fallback for domains registered elsewhere, cached so they stick across refreshes instead of re-querying every load.
  • Old DNS pages retired cleanly. DNS Zones and Dynamic DNS are gone as separate pages; their URLs redirect to /domains, and /domains?open=<domain> deep-links straight into a domain's drawer.
  • Connections is admin-only. It now lives under Administration in Settings and is gated end to end — these are shared infrastructure credentials, not per-user settings.
  • Updates that don't strand you. serverkit update now tracks main, auto-rolls-back a failed release swap (restoring the old install and restarting services), rebuilds the frontend, and force-recreates its container so the new build actually ships.
  • Loading feels intentional. A full-screen branded loader on first paint and WordPress detail skeletons that mirror the real chrome — plus the SSL status cards render as a proper grid again, and the dashboard no longer crashes when a backup schedule hasn't loaded yet.
Technical changes

Backend — DNS portfolio & registration

  • New GET /api/v1/dns/portfolio (dns_zones.pyDNSZoneService.list_portfolio) aggregates every connected DNSProviderConfig's live zones, flags which are already adopted (matched to local DNSZone rows by bare domain), and reports any connection that can't enumerate zones under errors rather than silently showing nothing — so a single-zone scoped token surfaces a "needs Zone:Read" prompt instead of an empty list.
  • New idempotent POST /api/v1/dns/adopt (DNSZoneService.adopt_zone) materializes a local DNSZone row on demand and backfills the connection link on a pre-existing manual row; the route is admin-gated (is_admin check, 403 otherwise).
  • New GET /api/v1/dns/provider-records (list_provider_records_by_ref) returns a Cloudflare zone's live records addressed by config_id + provider zone id with no adoption required, each tagged serverkit vs external via DnsOwnershipService.owned_keys.
  • New GET /api/v1/dns/registration (lookup_domain_registration) performs an RDAP lookup (rdap.org bootstrap — no whois binary), parsed by helpers _parse_rdap_date (Zulu→naive-UTC) and _rdap_entity_name (jCard), persisted/cached in the new domain_registrations table; 30-day TTL for hits, 3-day for misses, and returns stale cached data on transient failure.
  • New DomainRegistration model (domain_registrations, unique-indexed by bare domain), registered in models/__init__.py.
  • CloudflareClient.list_zones now returns each zone's account_id and pages 100; new list_registrar_domains(account_id) reads Cloudflare Registrar expiry/auto-renew/registrar (best-effort — a DNS-scoped token without Registrar:Read simply yields no expiry). DNSZoneService._cf_registrar_map builds the per-account domain→registration map.
  • list_portfolio backfills expiry/registrar from the cached RDAP table for domains the provider doesn't supply, so a lookup done in the drawer shows up in the list afterward.
  • api/connections.py: list endpoint moved from @jwt_required() to @admin_required (RBAC middleware) using get_current_user().
  • Tests: new backend/tests/test_dns_portfolio.py (11 tests) covering portfolio listing, adopted-zone flagging, scoped-token error surfacing, idempotent adopt (case-insensitive), Cloudflare registrar enrichment, provider-records tagging + Cloudflare-only guard, the HTTP endpoint, RDAP parse/not-found, cache persistence (one network call), and cached-registration merge.

Frontend — Domains surface

  • New components/domains/DomainDnsPanel.jsx: the single DNS records surface inside the drawer. Reconciles live Cloudflare records (FQDN) against managed records (relative names), supports inline add-record with adopt-on-write (ensureZone), per-record make-dynamic / regenerate / stop, export, propagation check, a proxied⇒HTTPS hint, and a deep link into the Cloudflare ops surface.
  • New components/domains/DdnsTokenCallout.jsx: one-time DDNS token + update-URL callout, shared by the make-dynamic flow.
  • pages/Domains.jsx: merges app domains with the provider portfolio into one mergedRows table (provider-only zones become their own rows; a domain that is both keeps its app row and gains provider data), adds provider brand icons, a new "Expires" column via formatExpiry, a banner when a connection can't list zones, ?open=<domain> deep-linking, a widened (1100px) drawer with header actions, and lazy registration resolution for provider rows. Removes the old in-drawer DNS-records block and "Manage DNS records" navigation.
  • components/domains/domainTabs.jsx: DOMAIN_TABS trimmed to Domains + SSL (DNS Zones / Dynamic DNS tabs removed).
  • App.jsx: dropped DNSZones/DynamicDns imports and routes; /dns and /dynamic-dns now <Navigate replace> to /domains; the three route-guard loading fallbacks render AppLoader.
  • Deleted pages/DNSZones.jsx, pages/DynamicDns.jsx, styles/pages/_dns-zones.scss, styles/pages/_dynamic-dns.scss (and their main.scss imports); relocated shared .dns-rtype + .ddns-token-callout styles into _domains.scss.
  • New services/api/dns.js methods: getDnsPortfolio, adoptDnsZone, getProviderRecords, getDomainRegistration.
  • New utils/expiry.js formatExpiry(): returns exact date + relative phrase + urgency tone (red/amber/green), reused by both the list column and the drawer.

Frontend — Connections, loaders, fixes

  • pages/Settings.jsx: moved connections into ADMIN_TABS, relocated its nav button into the Administration group, and gated the panel render on isAdmin.
  • components/settings/connections/ConnectionsHub.jsx: defense-in-depth non-admin guard render, plus a "Manage → Domains" link on the Cloudflare connection card.
  • New components/AppLoader.jsx + _main-content.scss styles: full-screen branded first-paint loader (logo + pulsing ring, aria-busy).
  • pages/WordPressDetail.jsx: rebuilt DetailPageSkeleton to mirror real chrome (topbar, identity header, repo pill, tab strip, overview KPI/quick-actions/traffic/activity grid); added reusable PanelSkeleton and OverviewGridSkeleton and swapped seven tabs' "Loading…" text for skeletons; _wordpress.scss styles.
  • components/backups/ScheduleCard.jsx: guard against an undefined policy to prevent a dashboard crash before the backup policy resolves.
  • styles/pages/_ssl.scss: define .ssl-status-* locally as a 3-up stat-card grid — the styles it had borrowed from _domains.scss broke when Domains migrated to MetricCard, leaving the cards as an unstyled vertical stack.

Scripts & install lifecycle

  • scripts/update.sh: defaults to pulling latest main and rebuilding (converts a release/tarball install to a shallow main git checkout, preserving .env + database); EXIT trap auto-rolls-back to INSTALL_DIR.old and restarts backend/nginx on any failure; detects serverkit/ vs opt/serverkit/ tarball layouts; creates backend/instance before copying the live DB; restores executable bits; runs npm ci && npm run build; clears a stale INSTALL_DIR.old; and docker compose up -d --force-recreate frontend so the bind mount follows the rebuilt dist/ inode.
  • install.sh: normalize serverkit/ vs opt/serverkit/ tarball layouts and restore executable bits on serverkit + scripts/*.sh.
  • serverkit + uninstall.sh: truecolor ServerKit masthead/tagline that degrades to plain text (respects NO_COLOR/dumb terminals) and a version-aware header.

jhd3197 and others added 6 commits June 22, 2026 23:24
…_card guard

- update.sh: default to pulling latest main source; clone release installs to git
- update.sh: auto-rollback on failed release swap, tarball layout detection,
  backend/instance mkdir, executable-bit restore, frontend dist build,
  force-recreate frontend container after dist rebuild
- install.sh: normalize serverkit/ vs opt/serverkit/ tarball layouts
- serverkit/uninstall.sh: install-style masthead + tagline
- ScheduleCard.jsx: guard against undefined policy to prevent dashboard crash
…ections

- /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) <[email protected]>
/dns and /dynamic-dns duplicated the Domains portfolio + drawer and led
with a misleading "0 ServerKit records" view for Cloudflare zones.
Collapse DNS to one surface: both pages now redirect to /domains and
DomainDnsPanel becomes the full records manager.

- Per-record "Make dynamic" on A/AAAA rows (adopt-on-demand ->
  createDdnsHost; one-time token via shared DdnsTokenCallout; Dynamic
  badge + regenerate/stop)
- Export + Check propagation moved into the drawer; dead "Full DNS page"
  button removed
- DOMAIN_TABS trimmed to Domains + SSL; /domains?open=<domain> deep-link
- Relocate shared .dns-rtype + .ddns-token-callout into _domains.scss
- Delete DNSZones.jsx, DynamicDns.jsx, _dns-zones.scss, _dynamic-dns.scss

Frontend-only; DDNS/portfolio backend untouched (20 tests green).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The .ssl-status-* classes were borrowed "from _domains.scss", but that
borrow broke when Domains migrated to MetricCard - leaving the Certbot /
Certificates / Expiring Soon cards as an unstyled vertical stack at the
top-left. Define them locally in _ssl.scss as a 3-up grid of stat cards.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Introduce a full-screen AppLoader component and corresponding styles, and replace simple loading placeholders across App.jsx with the new loader. Revamp WordPressDetail: add comprehensive skeleton UI (topbar, repo pill, tab strip, KPI/panel/list/activity skeletons), generic PanelSkeleton and OverviewGridSkeleton utilities, and swap multiple tab-loading messages to use the new skeletons. Update layout and wordpress SCSS files to style the app-level loader and the WP detail skeletons. Improves perceived load UX and unifies loading states across the app.
Copilot AI review requested due to automatic review settings June 23, 2026 13:12
@jhd3197 jhd3197 merged commit 1f8091c into main Jun 23, 2026
1 check passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR consolidates ServerKit’s DNS/domain management into a single /domains surface by merging app-linked domains with connected DNS-provider zones (Cloudflare), moving DNS record management into the domain drawer, and adding registration expiry lookups with caching. It also relocates “Connections” under admin-only Settings, improves first-paint loading UI, hardens the updater/install scripts, and includes several UI fixes/skeleton improvements.

Changes:

  • Added backend “DNS portfolio” + “adopt zone” + “provider live records” + RDAP-cached registration lookup APIs, plus tests.
  • Reworked the frontend Domains page to merge provider zones into the table and moved all DNS record management (including per-record Dynamic DNS) into the drawer via DomainDnsPanel.
  • Admin-gated Connections UI + new app-level loader, WordPress detail skeleton revamp, SSL status layout fix, and updater/install script improvements.

Reviewed changes

Copilot reviewed 33 out of 33 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
VERSION Bumps ServerKit version to 1.6.33.
uninstall.sh Adds truecolor branded header with terminal capability fallback.
serverkit Adds truecolor branded CLI header with local version display.
scripts/update.sh Adds rollback trap and changes update flow to prefer tracking main + rebuild, plus frontend force-recreate.
install.sh Normalizes release tarball layouts and restores executable bits.
frontend/src/utils/expiry.js Adds shared expiry-date formatting helper for list + drawer.
frontend/src/styles/pages/_wordpress.scss Adds new WordPress detail-page skeleton styles.
frontend/src/styles/pages/_ssl.scss Restores SSL status-card grid styles locally after Domains refactor.
frontend/src/styles/pages/_dynamic-dns.scss Removes retired Dynamic DNS page styles.
frontend/src/styles/pages/_domains.scss Adds unified Domains/DNS drawer styles, including DNS panel + migrated shared chips/callouts.
frontend/src/styles/pages/_dns-zones.scss Removes retired DNS Zones page styles.
frontend/src/styles/main.scss Drops imports for retired DNS Zones/Dynamic DNS styles.
frontend/src/styles/layout/_main-content.scss Adds full-screen initial app loader styling.
frontend/src/services/api/dns.js Adds API methods for portfolio/adopt/provider-records/registration lookup.
frontend/src/pages/WordPressDetail.jsx Replaces “Loading…” placeholders with richer skeletons across tabs.
frontend/src/pages/Settings.jsx Moves Connections under Administration group and attempts to admin-gate the panel.
frontend/src/pages/DynamicDns.jsx Removes retired Dynamic DNS page.
frontend/src/pages/Domains.jsx Merges app domains + provider portfolio; adds drawer DNS panel; supports ?open=<domain> deep-linking.
frontend/src/pages/DNSZones.jsx Removes retired DNS Zones page.
frontend/src/components/settings/connections/ConnectionsHub.jsx Adds defense-in-depth non-admin guard and links DNS connections to Domains.
frontend/src/components/domains/domainTabs.jsx Removes DNS Zones / Dynamic DNS tabs, leaving Domains + SSL.
frontend/src/components/domains/DomainDnsPanel.jsx New unified in-drawer DNS records manager (live Cloudflare, adopt-on-write, DDNS per record, export, propagation checks).
frontend/src/components/domains/DdnsTokenCallout.jsx New shared one-time DDNS token/update-URL callout.
frontend/src/components/backups/ScheduleCard.jsx Adds null-guard to prevent crash while backup policy loads.
frontend/src/components/AppLoader.jsx Adds branded app-level loader component used by route guards.
frontend/src/App.jsx Redirects old /dns and /dynamic-dns to /domains; uses AppLoader for auth/setup loading.
backend/tests/test_dns_portfolio.py Adds test coverage for portfolio/adopt/provider-records/registration cache behavior.
backend/app/services/dns/cloudflare.py Enriches zone listing with account_id; adds registrar-domain listing for expiry/auto-renew.
backend/app/services/dns_zone_service.py Implements portfolio aggregation, adopt-zone, provider-records-by-ref, and RDAP registration cache.
backend/app/models/domain_registration.py Adds persisted domain registration cache model.
backend/app/models/init.py Registers the new DomainRegistration model.
backend/app/api/dns_zones.py Adds /dns/portfolio, /dns/adopt, /dns/provider-records, /dns/registration endpoints.
backend/app/api/connections.py Makes connections listing admin-only via RBAC middleware.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread scripts/update.sh
Comment on lines +97 to +111
if [ "$INSTALL_FROM_RELEASE" != "1" ] && [ ! -d "$INSTALL_DIR/.git" ]; then
step "No source tree found — converting install to a git checkout from main..."
tmp_env=$(mktemp)
tmp_db=$(mktemp)
cp "$INSTALL_DIR/.env" "$tmp_env" 2>/dev/null || true
cp "$INSTALL_DIR/backend/instance/serverkit.db" "$tmp_db" 2>/dev/null || true
rm -rf "$INSTALL_DIR"
git clone --depth 1 --branch main "https://github.com/${GITHUB_REPO}.git" "$INSTALL_DIR" \
|| halt "Failed to clone ${GITHUB_REPO}.git"
cp "$tmp_env" "$INSTALL_DIR/.env" 2>/dev/null || true
mkdir -p "$INSTALL_DIR/backend/instance"
cp "$tmp_db" "$INSTALL_DIR/backend/instance/serverkit.db" 2>/dev/null || true
rm -f "$tmp_env" "$tmp_db"
step "Source tree cloned from main."
fi
Comment on lines +199 to +207
if existing:
if config_id and not existing.dns_provider_config_id:
from app.models.email import DNSProviderConfig
config = DNSProviderConfig.query.get(int(config_id))
if config:
existing.provider = config.provider
existing.dns_provider_config_id = config.id
db.session.commit()
return existing
Comment on lines 299 to 303
<div className="settings-content">
{activeTab === 'profile' && <ProfileTab />}
{activeTab === 'security' && <SecuritySettingsTab />}
{activeTab === 'connections' && <ConnectionsHub />}
{activeTab === 'connections' && isAdmin && <ConnectionsHub />}
{activeTab === 'appearance' && <AppearanceTab />}
Comment on lines +13 to +22
let relative;
if (days < 0) relative = `Expired ${Math.abs(days)}d ago`;
else if (days === 0) relative = 'Expires today';
else if (days < 60) relative = `${days} days left`;
else if (days < 365) relative = `${Math.round(days / 30)} months left`;
else {
const years = Math.floor(days / 365);
const months = Math.round((days % 365) / 30);
relative = months ? `${years}y ${months}mo left` : `${years} year${years > 1 ? 's' : ''} left`;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants