Conversation
Make email handling case-insensitive (queries + model normalization) to prevent duplicate accounts. Add SiteDomainService.panel_origin() helper and use it across services to build public URLs (Gitea, WordPress, Roundcube) and Django ALLOWED_HOSTS. Add optional custom-domain support when creating WordPress sites and propagate domain to Roundcube install. Improve environment domain fallback to prefer panel/base domains over .localhost. Small Dockerfile healthcheck tweak (use 127.0.0.1). Update install/update/build scripts to always (re)create virtualenv locally for release installs. Remove unused frontend CreateSiteModal and expose domain field in WordPress page.
- Add GET /api/v1/system/notices endpoint (admin-only) - Detect missing canonical domain, sites base domain, server public IP, and wildcard HTTPS - Add SystemNotices React component with dismiss persistence in localStorage - Render notices inside DashboardLayout so they appear on every page - Add scoped SCSS for warning/info/error notice banners
- Fall back to cp + manual cleanup when rsync is unavailable - Exclude .venv-wsl, test output, scratch images, and logs from the tarball - Use tar --transform to avoid moving directories across filesystems - Stop including root-level PNG/JPEG/JPG/log/tmp artifacts in releases
…d client Phase 0 of the DNS roadmap. #2 Shared Cloudflare client (app/services/dns/): DnsCredential + DnsRecordSpec + CloudflareClient. Both DNSProviderService and DNSZoneService delegate to it, so auth (scoped token / global key), the CAA structured-data wire format, MX/SRV priority parsing, and idempotent upsert live in one place. Fixes dup-on-recreate, silent update/delete no-op, and missing proxied/priority on the provider path. #1 Single credential source: new FK dns_zones.dns_provider_config_id (migration 025) + DNSZoneService._resolve_credential (linked -> auto-discover-by-domain -> legacy inline-token). create_zone accepts a connection id; link_legacy_zones() converges inline tokens onto encrypted connections at boot. The /dns create-zone modal now picks a connection instead of pasting a token. DDNS rides it for free. Tests: test_dns_cloudflare_client.py, test_dns_credential_resolution.py. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
… (Phase 1) #3 managed_dns_records ledger (migration 026) — the single source of truth for records ServerKit created in a connected provider zone, written by every write path (zones page, DDNS, WordPress auto-DNS, email). #4 DnsOwnershipService.guarded_upsert/guarded_delete: automatic paths refuse to overwrite or delete a record ServerKit didn't create (the user's own records), while the explicit Zones page adopts a matching record and takes ownership. Wired into both CF paths (DNSZoneService._cloudflare_sync and DNSProviderService._cloudflare_set_record/_delete_record). ensure_a_record now surfaces a 'foreign_record' reason instead of clobbering. #5 Live zone mirror: CloudflareClient.list_records + DNSZoneService.list_provider_records classify every live record as serverkit vs external; GET /api/v1/dns/<id>/mirror. Tests: test_dns_ownership.py. Existing set_record mocks updated for the new source kwarg. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
#6 dns_changes audit log (migration 027) — every record write ServerKit sends to a connected provider is recorded at the write choke point (guarded_upsert / guarded_delete): create/update/delete, source, result (ok|error|conflict|skipped), and the value set. Powers the "Changes to your Cloudflare" activity feed. GET /api/v1/dns/changes lists it (filter by config_id / zone / result). #8 A real write failure now surfaces an admin notice via the Notification Bus (dns.sync_failed) instead of being swallowed to the log. Best-effort — surfacing never breaks the write it describes. Tests: test_dns_change_log.py. The frontend activity/mirror UI lands separately. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…ror (Phase 2 frontend) #7 Surface the Phase 2 backend: - DnsActivity: a per-connection "Recent changes" feed (action, record, result badge, error) from GET /dns/changes, expandable on each DNS connection in Settings -> Connections. - DNS Zones page: a "ServerKit records / In your provider" toggle; the provider view (GET /dns/<id>/mirror) lists every live record badged ServerKit vs External, external records read-only/muted ("ServerKit never modifies external records"). - api: getDnsChanges() + getZoneMirror(). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…ase 3) #9 sites_dns_mode setting (wildcard | per-site) via SiteDomainService.dns_mode(); surfaced in SitesHttpsService.status(). #10 One-click wildcard already exists (SitesHttpsService.setup creates *.<base> + issues the wildcard cert); it now also pins the mode to wildcard, and the wildcard A record flows through the Phase 1 guarded/logged path. #11 SiteDomainService.ensure_site_dns(): in per-site mode a new managed site gets its own A record auto-created via a connected provider (ownership-guarded + logged), wired into WordPressService._provision_routing. Wildcard mode stays a no-op. Tests: test_dns_subdomain_modes.py. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…backend) #12 SiteDomainService.give_subdomain(app, label): one-click publish an app at <slug>.<base> — primary Domain row + nginx vhost + (per-site mode) an auto-created, guarded/logged A record. Endpoints: POST /domains/give-subdomain and GET /domains/suggest-subdomain. #14 GET /api/v1/dns/managed: every record ServerKit owns across all provider zones, enriched with the app that triggered each (from the ownership ledger). #13 inline propagation reuses the existing /dns/propagation/<domain> check. Tests: test_dns_give_subdomain.py. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
- Replace rsync with tar-based copy to avoid missing rsync on Windows/Git Bash - Exclude backend/venv, backend/.venv, backend/.venv-wsl, and broken WSL symlinks - Exclude ./backups, backend/dev-data/backups, backend/instance/backups, and scripts/test/output - Exclude scratch images (PNG/JPEG/JPG), logs, and tmp files from the tarball - Pack with tar --transform so /opt/serverkit prefix is created without cross-device moves - Reduce release tarball from 13 GB back to ~6 MB
ToastContext wrapped the 2nd arg as { duration }, so the ~30 call sites that pass a
full sonner options object (toast.info(msg, { duration: 4000 })) got it
double-wrapped ({ duration: { duration } }) and their custom durations were silently
dropped. Normalize at the wrapper: a number becomes { duration }, an object passes
through. Backward-compatible with the few numeric callers.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
… (Phase 3 #9 UI) Surfaces sites_dns_mode in the Managed Sites Domain & HTTPS card: a select to choose Wildcard (point *.base once, every site instant) or Per-site (each new site gets its own auto-created, ownership-tracked A record). Reads dns_mode from the sites-https status and saves via updateSystemSetting. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…r; top banner = urgent only The "common misconfiguration" notices stacked every hint as a banner at the top of the dashboard while the notification bell sat empty. Now: - SystemNotices (top banner) shows only urgent notices (error/critical, or an explicit urgent flag), at most one — the dashboard stays clear. - The live notices fold into the notification center (bell + /notifications) via the shared NotificationsContext: they show with their Fix action, bump the unread badge, and can be dismissed. Dismissal syncs across both surfaces (shared localStorage). New util utils/dismissedNotices.js (shared dismiss state + notice->item mapper). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…ackups, API Add a real Protection layer for WordPress sites and applications: one BackupPolicy per target (cron mirrored into a ScheduledJob on the unified job bus), BackupRun history, cost estimation, and full/incremental smart backups with chain-aware retention and remote verify. - models BackupPolicy + BackupRun (migration 028) - backup_policy_service: CRUD, schedule wiring, backup.policy.run/restore.run handlers (distinct from legacy backup.run), chain-aware retention, restore chain - backup_cost_service: storage-cost rates ($/GB/mo); local free by default - backup_service: tar --listed-incremental + zstd/gzip compression tiers - API: policy/runs/restore/verify/delete on /wordpress/sites and /apps; cost-rates + cost-summary on /backups - restore.completed/failed notification events - tests: cost, schedule sync, kind decision, chain-aware retention, restore wiring Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Shared ProtectionPanel (status, schedule, history) rendered for WordPress (Backups tab + Settings) and Services (Settings → Backups), plus a calendar view, detail/restore drawers, and storage-cost columns/settings/KPIs on the global Backups page. - components/backups: ProtectionPanel, ProtectionStatusCard, ScheduleCard, BackupHistoryList, BackupCalendar, BackupDetailDrawer, RestoreDrawer, format helpers - api/backupProtection: generic policy/runs methods over targetType - WordPressDetail + service-detail SettingsTab render the shared panel - Backups page: cost rates settings, Site/Cost columns, cost KPI cards Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Operator-facing guide to the Protection panel: scheduling, smart/incremental backups, retention, storage cost (free local disk vs real S3/B2), and one-click restore. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Add a Cloudflare Zone Settings surface on top of the existing Cloudflare DNS
connection — SSL/TLS, Speed, Caching and Security toggles plus a one-click
"recommended hardening" preset. Reuses the connected DNSProviderConfig
credential and shared CloudflareClient, so auth/encryption-at-rest are not
re-implemented.
Backend:
- CloudflareClient: generic v4 request() helper + zone-settings get/update
(envelope normalized to {success, error?, result?}).
- CloudflareService: resolve zone+credential by ServerKit DNS zone id, curated
setting groups (UI metadata), get/update setting, apply recommended preset
with per-setting reporting.
- /api/v1/cloudflare blueprint: settings GET / GET-one / PATCH + apply-preset
(reads any user, writes admin; mutations captured by the audit fallback).
Frontend:
- CloudflareZoneSettings page (tabbed SSL/TLS · Speed · Caching · Security ·
Actions) with declarative toggle/select/HSTS controls and optimistic save.
- Route /cloudflare/zones/:zoneId, "Cloudflare" link from Cloudflare-managed
DNS zones, api module, SCSS.
Tests: client envelope/zone-settings + service resolution guards, indexing,
and preset reporting (11 tests).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Add Cloudflare cache management to the Zone Settings page. Purge everything (with a confirm) or purge specific URLs (one per line, capped at 30 to match Free/Pro limits); hosts/prefixes/tags are accepted by the API for Enterprise plans and their plan errors are surfaced verbatim. - CloudflareClient.purge_cache(zone_id, payload). - CloudflareService.purge_cache: validates intent (raises on empty), caps files at 30, normalizes the payload. - POST /api/v1/cloudflare/zones/:id/purge-cache (admin). - Actions tab gains a "Purge cache" card (purge-everything button + URL list); api method; SCSS. Tests: client payload, purge-everything, 30-file cap, empty-raises, provider error surfaced (5 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Add a WAF tab to the Cloudflare Zone Settings page for managing custom firewall rules (the http_request_firewall_custom phase entry-point ruleset). - CloudflareClient: ruleset list/get + phase-entrypoint create (PUT) + per-rule POST/PATCH/DELETE. - CloudflareService: list rules (empty when no custom ruleset yet, distinguished from an auth error by listing rulesets first), add rule (creates the ruleset on first use), update/delete, and three one-click presets — lock WordPress admin to an IP, block common exploit paths, challenge suspicious bots. The IP in the lock preset is validated with ipaddress so it can't inject expression syntax; rule actions are constrained to a safe terminal subset. - /api/v1/cloudflare/zones/:id/waf/* routes (list any user; add/preset/update/ delete admin). - WafPanel component (preset picker + custom-rule form + active-rules list with enable toggle and delete); api methods; SCSS. Tests: ruleset discovery, empty vs auth-error, create-on-first-rule vs append, action validation, preset IP validation + safe expression, update/delete (10). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Add a Workers tab to the Cloudflare Zone Settings page for deploying edge
scripts and managing Worker routes. Workers are account-scoped, so the owning
account is resolved from the zone — the whole feature reuses the existing
Cloudflare DNS connection (no new credential).
- CloudflareWorker model: records the source ServerKit uploaded per (account,
name) so a worker can be tracked and re-deployed (created via create_all).
- CloudflareClient: zone→account lookup, list/delete scripts, and a stable
multipart PUT /scripts/{name} module upload (the still-beta resource-oriented
Workers API is deliberately avoided per the roadmap's fallback note), plus
zone Worker routes list/add/delete.
- CloudflareService: list (live scripts flagged when ServerKit-managed + routes),
deploy (validated lowercase name, records source, optional route attach),
delete, route add/delete.
- /api/v1/cloudflare/zones/:id/workers* routes (read any user; writes admin).
- WorkersPanel component (deploy form with a starter script, worker list, routes
list); api methods; SCSS.
Tests: multipart upload shape, name validation, deploy records source, list
managed-flagging, delete prunes the local row (5).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Add a Tunnels tab for creating and managing Cloudflare Tunnels (cloudflared) — exposing a local/private service through Cloudflare's edge with no public IP. Clearly distinct from ServerKit's WireGuard remote-access tunnels (always labelled "Cloudflare Tunnel" in the UI). Account-scoped; account resolved from the zone. - CloudflareTunnel model: records the connector token (encrypted at rest) and owning connection per (account, tunnel_id). - CloudflareClient: cfd_tunnel create (config_src=cloudflare) / list (is_deleted=false) / delete / token / connections / configurations get+put. - CloudflareService: create (returns + stores the one-time token, yields the cloudflared install command), list (managed-flagged), delete, fetch install, and public-hostname routing — sets the ingress config (always re-appends the required http_status:404 catch-all) and best-effort upserts the proxied CNAME hostname→<id>.cfargotunnel.com via the shared ownership-guarded DNS path. - /api/v1/cloudflare/zones/:id/tunnels* routes (read any user; writes admin). - TunnelsPanel (create form + one-time install box w/ copy, tunnel list with per-tunnel install reveal and public-hostname add/remove); api methods; SCSS. Tests: list is_deleted param, create config_src, name required, token stored encrypted + install string, managed-flagging, delete prunes local row, ingress built with catch-all last (7). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Add a Storage tab listing and managing Cloudflare's account-scoped storage primitives — R2 buckets, KV namespaces, and D1 databases. Management only (list/create/delete); the object/key/row data planes are out of scope. - CloudflareClient: r2/buckets, storage/kv/namespaces, d1/database list+create +delete. - CloudflareService: combined list_storage (each product fetched independently so a missing token scope degrades to a per-product error, not a dead tab), validated create/delete for each. R2 bucket names validated S3-style. - /api/v1/cloudflare/zones/:id/storage* routes (read any user; writes admin). - StoragePanel (three generic create/list/delete sections; R2 notes its S3-compatibility for future backup targeting); api methods; SCSS. No new tables — wiring an R2 bucket as an S3 backup backend (minting scoped R2 keys) is a deliberate follow-up, noted in the UI. Tests: aggregate listing + per-product error degradation, R2 name validation, create/delete, KV/D1 required-field guards (6). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
_canonical_site_url feeds clone/URL-swap search-replace, so it must return the URL actually baked into the WordPress DB. A domain-less site is installed at localhost:<port>, not the panel apex; the panel_origin branch (added in 652d903) made it resolve to the base domain, so clones/swaps would search-replace nothing and silently leave stale URLs. Drop that branch, keep the ssl_enabled-aware scheme. Fixes the test_canonical_site_url_prefers_primary_domain regression. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
There was a problem hiding this comment.
Pull request overview
This PR significantly expands ServerKit’s Cloudflare integration beyond DNS, hardens how DNS writes are performed via a unified credential + ownership/logging layer, and introduces “Backup Protection” (scheduled + incremental backups with restore/verification and cost estimation). It also includes a set of operational/install improvements (release tarball venv rebuild), notification UX changes (dismissible system notices), and account/domain normalization fixes.
Changes:
- Add Cloudflare “zone operations” API + UI (settings, cache purge, WAF rules, Workers, Tunnels, R2/KV/D1).
- Overhaul DNS integration: unify Cloudflare credential resolution, add “never-touch-foreign” ownership ledger, and introduce a provider change log + UI surfaces.
- Implement Backup Protection policies/runs with restore flows, verification, and storage cost estimation + UI.
Reviewed changes
Copilot reviewed 112 out of 113 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| VERSION | Bump release version |
| scripts/update.sh | Rebuild venv on release installs |
| scripts/build-release.sh | Build tarball without baked venv |
| install.sh | Always build venv locally |
| frontend/src/utils/dismissedNotices.js | Persist/shape dismissed system notices |
| frontend/src/styles/pages/_service-detail.scss | Styles for “give subdomain” modal |
| frontend/src/styles/pages/_notification-center.scss | Notice action/dismiss styles (page) |
| frontend/src/styles/pages/_dns-zones.scss | DNS mirror/managed records styles |
| frontend/src/styles/pages/_connections.scss | Connection list + DNS activity styles |
| frontend/src/styles/main.scss | Import system notices + CF page |
| frontend/src/styles/components/_system-notices.scss | System notice banner styles |
| frontend/src/styles/components/_notification-center.scss | Notice row styles (bell) |
| frontend/src/services/api/system.js | Add system notices + backup cost APIs |
| frontend/src/services/api/index.js | Register Cloudflare + backupProtection modules |
| frontend/src/services/api/files.js | Add suggest/give subdomain API calls |
| frontend/src/services/api/dns.js | Add DNS changes/mirror/managed APIs |
| frontend/src/services/api/cloudflare.js | Cloudflare operations API client |
| frontend/src/services/api/backupProtection.js | Backup Protection API client |
| frontend/src/pages/WordPressDetail.jsx | Replace backups tab with ProtectionPanel |
| frontend/src/pages/WordPress.jsx | Add optional custom domain on create |
| frontend/src/pages/Notifications.jsx | Render/dismiss notice items |
| frontend/src/pages/Backups.jsx | Add cost summary + rates settings UI |
| frontend/src/layouts/DashboardLayout.jsx | Mount SystemNotices banner |
| frontend/src/contexts/ToastContext.jsx | Fix sonner options normalization |
| frontend/src/contexts/NotificationsContext.jsx | Merge system notices into inbox list |
| frontend/src/components/wordpress/index.js | Remove CreateSiteModal export |
| frontend/src/components/wordpress/CreateSiteModal.jsx | Remove unused component |
| frontend/src/components/SystemNotices.jsx | Admin system notice banner component |
| frontend/src/components/settings/SiteSettingsTab.jsx | Add managed-sites DNS mode setting |
| frontend/src/components/settings/connections/DnsActivity.jsx | Connection DNS change feed UI |
| frontend/src/components/settings/connections/ConnectProviderModal.jsx | Expandable DNS activity per connection |
| frontend/src/components/service-detail/SettingsTab.jsx | Add app ProtectionPanel + subdomain publish UI |
| frontend/src/components/NotificationBell.jsx | Render/dismiss notice items in bell |
| frontend/src/components/cloudflare/WorkersPanel.jsx | Cloudflare Workers UI |
| frontend/src/components/cloudflare/StoragePanel.jsx | Cloudflare R2/KV/D1 UI |
| frontend/src/components/backups/RestoreDrawer.jsx | Restore flow drawer + typed confirm |
| frontend/src/components/backups/ProtectionStatusCard.jsx | Protection status + KPIs/actions |
| frontend/src/components/backups/format.js | Backup formatting helpers |
| frontend/src/components/backups/BackupHistoryList.jsx | Backup run history table |
| frontend/src/components/backups/BackupDetailDrawer.jsx | Backup run details drawer |
| frontend/src/components/backups/BackupCalendar.jsx | Backup calendar UI |
| frontend/src/App.jsx | Route for Cloudflare zone settings page |
| frontend/Dockerfile | Healthcheck use 127.0.0.1 |
| docs/BACKUP_PROTECTION.md | Backup Protection documentation |
| docker-compose.yml | Mount dist + IPv4 healthcheck |
| backend/tests/test_dns_subdomain_modes.py | Tests for wildcard vs per-site DNS |
| backend/tests/test_dns_give_subdomain.py | Tests for “give subdomain” + managed records |
| backend/tests/test_dns_credential_resolution.py | Tests for unified DNS credentials |
| backend/tests/test_dns_change_log.py | Tests for DNS change logging + notify |
| backend/tests/test_dns_caa.py | Test update for set_record kwargs |
| backend/tests/test_attach_domain.py | Test update for set_record kwargs |
| backend/requirements.txt | Add tomli fallback for <3.11 |
| backend/migrations/versions/028_backup_policies.py | Backup policy/run tables migration |
| backend/migrations/versions/027_dns_change_log.py | DNS changes table migration |
| backend/migrations/versions/026_managed_dns_records.py | Managed DNS ownership table migration |
| backend/migrations/versions/025_dns_provider_link.py | Link zones to provider configs migration |
| backend/migrations/alembic.ini | Add logging config |
| backend/app/services/wordpress_service.py | Domain-aware URLs + custom domain create + per-site DNS |
| backend/app/services/sso_service.py | Case-insensitive email lookup |
| backend/app/services/sites_https_service.py | Expose dns_mode + force wildcard on setup |
| backend/app/services/site_domain_service.py | panel_origin + dns_mode + ensure_site_dns + give_subdomain |
| backend/app/services/roundcube_service.py | Domain-aware public URL + proxy setup |
| backend/app/services/repository_manifest_service.py | tomllib/tomli compatibility |
| backend/app/services/python_service.py | Domain-aware Django ALLOWED_HOSTS |
| backend/app/services/git_service.py | Domain-aware Gitea public URL |
| backend/app/services/environment_domain_service.py | Prefer real domain over .localhost |
| backend/app/services/dns/base.py | Provider-agnostic DNS dataclasses |
| backend/app/services/dns/init.py | Shared DNS client factory |
| backend/app/services/dns_ownership_service.py | Ownership ledger + guarded writes |
| backend/app/services/dns_change_service.py | DNS change log + failure notices |
| backend/app/services/backup_service.py | Smart backup + incremental restore helpers |
| backend/app/services/backup_cost_service.py | Storage cost rates + projections |
| backend/app/notifications/catalog.py | Add restore + dns sync events |
| backend/app/models/user.py | Lowercase email normalization |
| backend/app/models/managed_dns_record.py | Managed DNS ownership model |
| backend/app/models/dns_zone.py | Link zones to provider config |
| backend/app/models/dns_change.py | DNS change log model |
| backend/app/models/cloudflare_worker.py | Worker source tracking model |
| backend/app/models/cloudflare_tunnel.py | Tunnel token tracking model |
| backend/app/models/backup_run.py | Backup run model |
| backend/app/models/backup_policy.py | Backup policy model |
| backend/app/models/init.py | Export new models |
| backend/app/api/wordpress.py | Accept custom domain on create |
| backend/app/api/wordpress_sites.py | Backup Protection endpoints (WP) |
| backend/app/api/system.py | /system/notices endpoint |
| backend/app/api/email.py | Pass Roundcube domain param |
| backend/app/api/domains.py | Suggest/give subdomain endpoints |
| backend/app/api/dns_zones.py | Mirror/changes/managed endpoints |
| backend/app/api/backups.py | Backup cost rates + cost summary endpoints |
| backend/app/api/auth.py | Case-insensitive email register/login/update |
| backend/app/api/apps.py | Backup Protection endpoints (apps) |
| backend/app/api/admin.py | Case-insensitive email create/update |
| backend/app/init.py | Register Cloudflare bp + backup policy jobs + migrate zones |
| .gitignore | Ignore new roadmap/plan docs |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+8
to
+15
| export function getDismissedNotices() { | ||
| try { | ||
| const raw = localStorage.getItem(STORAGE_KEY); | ||
| return raw ? JSON.parse(raw) : []; | ||
| } catch { | ||
| return []; | ||
| } | ||
| } |
Comment on lines
+229
to
+233
| notices = [] | ||
| canonical_domain = (SiteDomainService.panel_origin() or '').replace('https://', '').replace('http://', '') | ||
| base_domain = SiteDomainService.base_domain() | ||
| server_ip = SiteDomainService.server_ip() | ||
| https_enabled = SiteDomainService.https_enabled() |
Comment on lines
+5
to
+6
| import { Button } from '@/components/ui/button'; | ||
| import { getDismissedNotices, addDismissedNotice, isUrgentNotice } from '../utils/dismissedNotices'; |
Comment on lines
+18
to
+23
| export default function SystemNotices() { | ||
| const navigate = useNavigate(); | ||
| const [notices, setNotices] = useState([]); | ||
| const [loading, setLoading] = useState(true); | ||
| const [dismissed, setDismissedState] = useState(getDismissedNotices); | ||
|
|
Comment on lines
+40
to
+42
| const handleDismiss = (id) => { | ||
| setDismissedState(addDismissedNotice(id).slice()); | ||
| }; |
Comment on lines
+151
to
+168
| from app.services.dns_change_service import DnsChangeService | ||
| if provider_record_id: | ||
| owned = DnsOwnershipService.owns(provider_zone_id, provider_record_id=provider_record_id) | ||
| else: | ||
| owned = DnsOwnershipService.owns(provider_zone_id, record_type=record_type, name=name) | ||
| if not owned: | ||
| return {'success': True, 'skipped': True, | ||
| 'message': 'No ServerKit-owned record to delete.'} | ||
| res = client.delete(provider_zone_id, record_id=provider_record_id, | ||
| record_type=record_type, name=name) | ||
| DnsOwnershipService.record_delete(provider_zone_id, record_type=record_type, | ||
| name=name, provider_record_id=provider_record_id) | ||
| DnsChangeService.record( | ||
| provider=provider, provider_zone_id=provider_zone_id, action='delete', | ||
| record_type=record_type, name=name, provider_record_id=provider_record_id, | ||
| source=source, result='ok' if res.get('success') else 'error', | ||
| error=None if res.get('success') else res.get('error'), config_id=config_id) | ||
| return res |
Comment on lines
+873
to
+884
| if not archives: | ||
| raise RuntimeError('No archives to restore') | ||
| os.makedirs(restore_path, exist_ok=True) | ||
| parent = os.path.dirname(restore_path.rstrip('/')) or '/' | ||
| for archive in archives: | ||
| if not os.path.exists(archive): | ||
| raise RuntimeError(f'Backup archive missing: {archive}') | ||
| cmd = ['tar', '--listed-incremental=/dev/null', '-xf', archive, '-C', parent] | ||
| result = subprocess.run(cmd, capture_output=True, text=True, timeout=1800) | ||
| if result.returncode != 0: | ||
| raise RuntimeError(f'tar restore failed: {(result.stderr or "").strip()[:300]}') | ||
| return {'success': True, 'restore_path': restore_path} |
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 has been on speaking terms with Cloudflare for a while, but the conversation never got past DNS records. This release opens up the rest of the dashboard — and tightens how the panel touches your zone in the first place. On the feature side, a new Cloudflare operations surface sits on top of the existing DNS connection (no second credential): zone settings with one-click hardening, cache purging, WAF custom rules, Workers, Tunnels, and the R2/KV/D1 storage products. Underneath it is a DNS overhaul that makes the panel safe to point at a real zone — one unified Cloudflare connection behind a shared client, an ownership ledger so ServerKit never overwrites a record you created yourself, and a "Changes to your Cloudflare" feed that shows every write it made. The third pillar is Backup Protection: scheduled, smart (full + incremental), restorable backups for both WordPress sites and generic apps, with retention, optional remote copies, verification, and a storage-cost view that's honest about local disk being free and only S3/B2 costing real money. Rounding it out, emails are matched case-insensitively so case variants can't spawn duplicate accounts, public links prefer your real domain over
localhost:<port>, and common setup gaps now surface as dismissible notices instead of silent dead-ends.Highlights
/xmlrpc.php,/.env,/.git), and challenge suspicious bots.cloudflaredtunnels with a copy-paste install command, and provision storage — all on the Cloudflare account you already connected.localhost:<port>, logins are case-insensitive on email, and setup gaps show as dismissible notices in the notification center.Technical changes
Cloudflare operations (
/api/v1/cloudflare)CloudflareService(backend/app/services/cloudflare_service.py) +cloudflare_bpblueprint, registered at/api/v1/cloudflare. A zone is addressed by its ServerKitDNSZoneid; the Cloudflare credential, zone id, and account are resolved server-side via the canonicalDNSZoneService._resolve_credential, so the whole surface reuses the existing DNS connection's auth + encryption-at-rest._require_admin). A reached-but-failed provider call maps to502, a caller/resolution problem (CloudflareError) to400.SETTING_GROUPScatalog (SSL/TLS, Speed, Caching, Security);RECOMMENDED_PRESETapplies one-click hardening and returns a per-setting report so the UI can show what the plan gated.purge_cachesupports everything / files / hosts / prefixes / tags, cappingfilesat 30 (Free/Pro limit); empty requests raise rather than no-op.http_request_firewall_customruleset, creating the zone's custom ruleset on first use. Actions are restricted to a terminal-action allowlist (noskip, which can disable WAF), and thelock_wp_adminpreset validates the operator-supplied IP withipaddressso a preset can't inject Cloudflare expression syntax.cloudflared) are account-scoped, the account read from the zone.CloudflareWorkerrecords the uploaded source;CloudflareTunnelstores the connector token encrypted at rest and never serialized (revealed once on create, re-fetched on demand via the install endpoint). R2 buckets / KV namespaces / D1 databases get create/list/delete.CloudflareClient(backend/app/services/dns/cloudflare.py) gained the zone-settings, ruleset, worker, tunnel, account-id, and storage calls so all Cloudflare HTTP lives in one module.DNS overhaul (unify, own, log)
backend/app/services/dns/):DnsCredential+DnsRecordSpecdataclasses (base.py), the sharedCloudflareClient, and aget_clientfactory. BothDNSProviderServiceandDNSZoneServicenow drive the one client — the duplicated Cloudflare request/CAA code that had drifted between the two layers is deleted, so wire-format fixes (e.g. CAA's structureddataobject) live in one place.DNSZone.dns_provider_config_idFK links a zone to a Settings → Connections credential.DNSZoneService._resolve_credentialresolves linked-config → auto-discovery (and backfills the link +provider_zone_idso the next call is API-free) → legacy inline token.link_legacy_zones()runs on startup to migrate inline Cloudflare tokens onto the connection store (encrypted), logged alongside the other legacy-secret migrations inapp/__init__.py.ManagedDnsRecordmodel +DnsOwnershipService.guarded_upsert/guarded_deleteare the single write choke point — they refuse to overwrite a record ServerKit didn't create (allow_foreign=Falsefor automatic paths like WordPress auto-DNS, email, wildcard;allow_foreign=Truefor the explicit/dnsZones page, which adopts the record) and record ownership on success.DnsChangemodel +DnsChangeService.recordlogs every provider write (ok/error/conflict/skipped) at that same choke point and fires adns.sync_failedadmin notification on real failures.DNSZoneService.list_provider_recordsreturns a live zone mirror tagging each recordserverkitvsexternal.SiteDomainService.dns_mode()(wildcarddefault vsper-site),ensure_site_dns()(per-site auto-creates the A record through a connected provider, guarded + logged; wildcard is a no-op), andgive_subdomain()(Domain row + nginx vhost + DNS in one call). WordPress custom-domain attach andSitesHttpsServiceset the mode accordingly.GET /dns/<id>/mirror,GET /dns/changes,GET /dns/managed,GET /domains/suggest-subdomain,POST /domains/give-subdomain.DNSProviderService.set_recordgainsproxied/priority/source, andensure_a_recorddistinguishes aforeign_recordconflict from anapi_error.025_dns_provider_link,026_managed_dns_records,027_dns_change_log.Backup Protection
BackupPolicyandBackupRunmodels (backup_policies/backup_runs). One policy per target,target_type∈ {wordpress_site,application} (unique constraint); the cron schedule is mirrored into aScheduledJobon the unified job bus, andlast_*columns cache the latest run so the panel renders status without scanning runs.BackupRunrecordsfull/incrementalkind, sizes,Decimalcosts, paths, verification, and the originatingjob_id.BackupPolicyService(get_or_create_policy,update_policy,run_policy_now,list_runs,request_restore,verify_run,delete_run) registered viaregister_jobs()inapp/__init__.py; smart backups alternate full vs incremental byfull_every_n_dayswith chain-aware retention.BackupCostService—$/GB/monthrates persisted inbackups.json(local$0 default;s30.023 /b20.006 list-price defaults), all money inDecimal.cost-ratesGET/PUT plus acost-summaryaggregate (sum of successful-run sizes × current rates) with a forward monthly projection from each policy's recent average size.apps.pyandwordpress_sites.py:…/backup-policyGET/PUT,…/backupsGET/POST,…/backups/<run>/restoreand/verifyPOST,…/backups/<run>DELETE — access-gated per target (_can_edit_app/ site ownership). Newrestore.completed/restore.failedcatalog events. Migration028_backup_policies.components/backups/ProtectionPanel.jsx(used byBackups.jsx, service detailSettingsTab, andWordPressDetail) withScheduleCard,ProtectionStatusCard,BackupCalendar,BackupHistoryList,BackupDetailDrawer,RestoreDrawer, and aformat.jshelper;Backups.jsxadds the global cost view. Newservices/api/backupProtection.js+system.jscost helpers and_backups.scss.System notices & notification center
GET /system/notices(admin) computes dismissible misconfiguration hints fromSiteDomainService— missing canonical domain, missing sites base domain, missing server public IP, and managed-sites wildcard HTTPS not set up — each with a level, message, and a deep-link to the fix.SystemNotices.jsxtop banner now shows at most one urgent notice; the rest fold into the notification center viaNotificationsContext(admins only).utils/dismissedNotices.jspersists dismissals inlocalStorageand maps notices onto the bell/page item shape, so banner, bell, and Notifications page stay in sync;unreadCountbecomes bus-unread + notice-count, andmarkAllReadleaves notices alone (they clear on fix/dismiss, not on read).NotificationBellandNotifications.jsxrender notice items distinctly (action label + dismissX); notice clicks route to the fix instead of marking read.Email normalization & domain-aware URLs
User.emailis lowercased via a@validateshook, and all lookups (register, login, admin create/update, self-update, SSOfind_or_create_user) usefunc.lower(...)so case variants can't create duplicate accounts or block a login.SiteDomainService.panel_origin()(canonical-domain setting →PUBLIC_URL/SERVERKIT_PUBLIC_URL→ sites base domain) now backs Gitea, WordPress, and Roundcube public URLs, DjangoALLOWED_HOSTS(panel host + its subdomains), and the environment-domain fallback (prefers the base/panel domain over a useless.localhost).create_siteaccepts an optional customdomain(attached + URL-migrated on create); Roundcubeinstallaccepts adomainand otherwise auto-publishes atwebmail.<panel_host>with an nginx proxy._canonical_site_urlnow honorshttpsand the panel origin. Removed the unusedCreateSiteModal.jsx;WordPress.jsxexposes the domain field.patch_localhost.pyis a one-off patcher that applies the samepanel_originURL fixes to an already-installed/opt/serverkittree.Misc fixes, build & release
ToastContexttoastOptions()accepts either a numeric duration or a full sonner options object; previously an options object was double-wrapped and custom durations were silently dropped app-wide.203/EXEC):install.shandscripts/update.shalways (re)build the venv locally (update.shaddslocate_pythonfor 3.11/3.12 +rebuild_virtualenv), andscripts/build-release.shswitches fromrsync+ baked venv to atar-based copy that survives Windows/Git Bash and broken symlinks.tomlifallback for Python < 3.11 (repository-manifest TOML parsing), alembic logging config, frontend Dockerfile + compose healthchecks use127.0.0.1(the container only listens on IPv4), dev compose mountsfrontend/dist, and.gitignoreignores the new local plan/roadmap docs.