Conversation
…_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.
There was a problem hiding this comment.
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 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`; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
/domainsis 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 hardierserverkit updatethat tracksmainand 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
/domains, and/domains?open=<domain>deep-links straight into a domain's drawer.serverkit updatenow tracksmain, 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.Technical changes
Backend — DNS portfolio & registration
GET /api/v1/dns/portfolio(dns_zones.py→DNSZoneService.list_portfolio) aggregates every connectedDNSProviderConfig's live zones, flags which are already adopted (matched to localDNSZonerows by bare domain), and reports any connection that can't enumerate zones undererrorsrather than silently showing nothing — so a single-zone scoped token surfaces a "needs Zone:Read" prompt instead of an empty list.POST /api/v1/dns/adopt(DNSZoneService.adopt_zone) materializes a localDNSZonerow on demand and backfills the connection link on a pre-existing manual row; the route is admin-gated (is_admincheck, 403 otherwise).GET /api/v1/dns/provider-records(list_provider_records_by_ref) returns a Cloudflare zone's live records addressed byconfig_id+ provider zone id with no adoption required, each taggedserverkitvsexternalviaDnsOwnershipService.owned_keys.GET /api/v1/dns/registration(lookup_domain_registration) performs an RDAP lookup (rdap.org bootstrap — nowhoisbinary), parsed by helpers_parse_rdap_date(Zulu→naive-UTC) and_rdap_entity_name(jCard), persisted/cached in the newdomain_registrationstable; 30-day TTL for hits, 3-day for misses, and returns stale cached data on transient failure.DomainRegistrationmodel (domain_registrations, unique-indexed by bare domain), registered inmodels/__init__.py.CloudflareClient.list_zonesnow returns each zone'saccount_idand pages 100; newlist_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_mapbuilds the per-account domain→registration map.list_portfoliobackfills 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) usingget_current_user().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
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.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 onemergedRowstable (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 viaformatExpiry, 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_TABStrimmed to Domains + SSL (DNS Zones / Dynamic DNS tabs removed).App.jsx: droppedDNSZones/DynamicDnsimports and routes;/dnsand/dynamic-dnsnow<Navigate replace>to/domains; the three route-guard loading fallbacks renderAppLoader.pages/DNSZones.jsx,pages/DynamicDns.jsx,styles/pages/_dns-zones.scss,styles/pages/_dynamic-dns.scss(and theirmain.scssimports); relocated shared.dns-rtype+.ddns-token-calloutstyles into_domains.scss.services/api/dns.jsmethods:getDnsPortfolio,adoptDnsZone,getProviderRecords,getDomainRegistration.utils/expiry.jsformatExpiry(): 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: movedconnectionsintoADMIN_TABS, relocated its nav button into the Administration group, and gated the panel render onisAdmin.components/settings/connections/ConnectionsHub.jsx: defense-in-depth non-admin guard render, plus a "Manage → Domains" link on the Cloudflare connection card.components/AppLoader.jsx+_main-content.scssstyles: full-screen branded first-paint loader (logo + pulsing ring,aria-busy).pages/WordPressDetail.jsx: rebuiltDetailPageSkeletonto mirror real chrome (topbar, identity header, repo pill, tab strip, overview KPI/quick-actions/traffic/activity grid); added reusablePanelSkeletonandOverviewGridSkeletonand swapped seven tabs' "Loading…" text for skeletons;_wordpress.scssstyles.components/backups/ScheduleCard.jsx: guard against an undefinedpolicyto 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.scssbroke when Domains migrated toMetricCard, leaving the cards as an unstyled vertical stack.Scripts & install lifecycle
scripts/update.sh: defaults to pulling latestmainand rebuilding (converts a release/tarball install to a shallowmaingit checkout, preserving.env+ database);EXITtrap auto-rolls-back toINSTALL_DIR.oldand restarts backend/nginx on any failure; detectsserverkit/vsopt/serverkit/tarball layouts; createsbackend/instancebefore copying the live DB; restores executable bits; runsnpm ci && npm run build; clears a staleINSTALL_DIR.old; anddocker compose up -d --force-recreate frontendso the bind mount follows the rebuiltdist/inode.install.sh: normalizeserverkit/vsopt/serverkit/tarball layouts and restore executable bits onserverkit+scripts/*.sh.serverkit+uninstall.sh: truecolor ServerKit masthead/tagline that degrades to plain text (respectsNO_COLOR/dumb terminals) and a version-aware header.