Comments cited plan sections as "(§G3)" or "(§8 unification)", which
point into the same git-ignored plans the plan-number citations did.
References next to a real document (docs/REDESIGN_MAP.md §3) stay. Code
and runtime strings are unchanged (AST and token comparison).
Comments, docstrings and a few strings cited plan numbers ("plan 29
#9", "plan 86 §B2") from a git-ignored folder, so they pointed at
nothing; one reached users in a bottleneck hint. Code and frontend
strings are unchanged apart from those citations (checked by AST and
token comparison). A vhost still carrying the old asset comment reads
as undrifted, and a guard test keeps new citations out.
Plan 87 phase B. An app that opts in keeps two slots, like the
panel updater's two installs. A deploy boots the new release in the
idle slot on its own loopback port while the live one serves, holds
it to the health gate, then switches nginx by moving app.port to the
new slot and rewriting the vhost, so every app.port reader follows
without edits. A watch window switches back on its own if the
release falls over; the old slot stays warm for an instant switch
back, then is stopped (not removed) by a standby sweep.
Opting in adopts the running container as slot a with no restart.
Eligibility is shown with reasons and re-checked on every deploy;
opting out folds the slots back on the next in-place deploy. Slot
images are never pruned and slot ports stay reserved.
Host disk alerts were off by default and Doctor's headroom failure
notified nobody, so a box reached 100% silently. A 15-minute builtin
now notifies admins once when the panel host disk crosses 85%, again
at 95%, and resets below 80% (storage.disk_alert_percent, 0 = off).
app/sockets_rooms.py owns every Socket.IO room-name builder (the grammar
lived in ~25 scattered f-strings); app/constants.py SOCKET_EVENTS is
mirrored verbatim by frontend/src/constants/events.js and a contract
test parses the JS so a renamed event fails in CI instead of silently
killing a stream. sockets.py / agent_gateway / notification bus and the
two frontend components that hand-derived rooms now use the builders;
ratchet forbids new room f-strings in the realtime layer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A VPS resized from 1 vCPU/1 GB to 4 vCPU/8 GB with a 60 GB volume attached,
and the panel never mentioned any of it. Not a stale cache: a resize forces a
power-off, so the process and its in-memory spec cache die and the next boot
reads the truth. The gap is that nothing ever persisted the PREVIOUS numbers,
so there was nothing to compare against — and the reboot that causes the
change is what destroys the only copy of the baseline.
Adds host_snapshots: capture specs + per-filesystem inventory at boot, diff
against the last row, and emit host.specs_changed / host.storage_advisory on a
real delta. Advisories fire on state transitions only, so an unfixed fstab
entry does not nag on every restart.
Repairs a divergence this exposed: resource_tier_service measured the first of
/var/lib/serverkit,/var/lib/docker,/ while capacity_service hardcoded /. Both
now use host_inventory_service.data_path(), and local headroom reports which
mountpoint it measured.
Notifications render in the existing bell UI, so no frontend change is needed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Restore the plan-33 bounce slice lost in the rebuild (migration 069 survived;
the model/route/service did not). EmailBounceState model reconstructed verbatim
from the clean pyc dump. New bounce_service.BounceService maps provider payloads
(generic/SendGrid/Postmark/SES/Mailgun), records bounces correlated to
NotificationDelivery by provider_message_id, auto-mutes a recipient past
MUTE_THRESHOLD (complaints mute immediately), and the email channel skips muted
targets. Inbound webhook POST /api/v1/notifications/inbound/email (HMAC-SHA256,
404 until notify.inbound_secret set, 401 on bad sig) + operator unmute + admin
bouncing list + email_status in GET /preferences.
Route/service names/paths faithful to the salvaged test string-index; logic
reconstructed from the plan-33 doc (the route+test pycs were corrupt). Model
registration in app/models/__init__.py lands with the Phase-2 commit (shared
file). Verified: test_email_bounce.py 16 passed; notification suite no regressions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Daily read-only drift sweep re-renders expected nginx vhosts and compose
overrides from panel state (via extracted pure render seams) and diffs
against disk; drift notifies admins. Doctor tab on Monitoring runs the
full diagnosis (drift, services, certs, disk, DB) with diff-confirmed
per-item and batch repair. Nothing auto-repairs. The nginx render seam
also carries the micro-cache blocks used by the next commit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New builtin.extension_updates ScheduledJob (daily) runs
check_for_updates() against the registry and notifies admins via the
Notifications Bus (new extensions.updates_available catalog event,
to=admins). Fingerprint marker in settings de-dups: one notification
per new release set, not one per day; the Marketplace badge stays the
live surface. Dogfoods both the Jobs and Notify extension-facing SDKs.
3 new tests (notify-once-per-release, quiet when none, schedule
seeded); jobs suite builtin count updated 9 -> 10; pipeline+jobs 51
green, notification bus 20 green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A managed-sites base domain that contains the panel's own hostname is a
wildcard footgun: *.<base> also matches the panel host, so it keeps resolving
only while the panel's explicit DNS record out-specifies the wildcard (or, when
base == panel domain, the wildcard setup's apex record repoints the panel
itself). Add a fourth publishing gap that detects this and nudges admins.
base_domain_overlaps_panel() flags panel host == base and panel host as a
direct single-label child of base; a deeper descendant (a.b.<base>) is safe
because a wildcard is single-label. It reads the panel's genuinely-configured
domain (canonical_domain / PUBLIC_URL), not panel_origin's base-domain
fallback — that fallback would equate panel with base and both mask real
overlaps and break the subdomain-vs-apex suggestion.
Proven in test_sites_publishing_gaps.py: equal-domain and direct-child overlap,
deep-descendant and scoped-label (apps.<domain>) non-overlap.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Creating a site while the managed-sites config is only partly done (base
domain set but not the DNS/HTTPS half, or nothing set at all) left the site
quietly on localhost with no signal to finish setup. Now each create checks
readiness and drops an in-app nudge to admins for any open gap:
- no base domain -> site is localhost-only; suggests one to set
- base domain, HTTPS off -> HTTP-only (gentle/optional, HTTPS stays best-effort)
- per-site DNS mode, no server IP -> A records can't auto-create
The suggested base domain is derived from the panel's OWN domain, so it fits
the install shape: an apex install (example.com) suggests apps.example.com,
while a subdomain install (panel.example.com) suggests a sibling
apps.example.com that won't collide with the panel's record. Nudges are
deduped against an already-open unread notice, so a gap is raised once, not on
every create. Wired into WordPress create and the generic give_subdomain
publish path; best-effort so a notification failure never affects the create.
Proven in test_sites_publishing_gaps.py (each gap, the apex-vs-subdomain
suggestion, and the dedup).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the email unification: the outbound relay is now one smtp
EmailProviderConnection flagged uses_relay, so a single table drives both the
Notification Bus and the Postfix relay. The legacy single-row email_relay_config
is folded in on first boot and kept only as a read fallback.
- EmailRelayService rewritten over the unified table: get/save/disable/test all
operate on the uses_relay smtp connection; password stays Fernet-encrypted in
credentials_json; masked round-trips preserve the stored secret. Postfix apply
unchanged (no-op when Postfix absent).
- migrate_legacy_config(): idempotent fold of email_relay_config → connection
(password copied verbatim — same Fernet key); wired into the boot-time legacy
migration block.
- API: PUT /notifications/admin/email-providers/:id toggles usage flags
(uses_notifications/uses_relay/relay_priority); uses_relay honored for SMTP only.
Verified: 6 new relay tests + usage-flag + notification-bus suites pass (30 passed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Foundation for folding the single-row email_relay_config into the multi-row
EmailProviderConnection so one table drives notifications and the Postfix relay.
- EmailProviderConnection gains uses_notifications (default true), uses_relay
(default false), relay_priority (default 0); surfaced in to_dict.
- Notification selection (default_provider) now restricted to uses_notifications
rows, so relay-only connections are never used to send notifications.
- New relay_provider() selects the active SMTP connection by relay_priority.
- add_provider() accepts the flags; uses_relay honored only for provider=smtp
(API providers can't be a Postfix smarthost).
- Migration 038 (idempotent additive columns).
Additive — existing providers default to uses_notifications=true / no relay.
Verified: 4 new tests + notification-bus suite pass (24 passed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#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) <noreply@anthropic.com>
Add SystemEvent model, TelemetryService, /api/v1 telemetry endpoints, and
a /telemetry UI page. Instrument audit, notification, webhook, backup,
deployment, and monitoring paths. Add correlation_id to Notification,
EventDelivery, and DeploymentJob so related events can be grouped.
- 18 backend tests added and passing
- Frontend production build clean
Reusable, non-blocking notification layer on top of the Queue Bus:
notify.send(event, to, data) records a Notification + per-channel
NotificationDelivery, queues them, and a daemon consumer delivers via
pluggable channel adapters (in-app, email, chat) inheriting retry,
backoff and dead-letter.
- Jinja2 email system: branded, dark-mode, Outlook-safe base layout +
per-event templates (security/backup/monitoring/welcome/invitation).
The legacy inline f-string email was removed and now renders here.
- In-app center API (inbox/unread-count/mark-read) + per-user Socket.IO push.
- Admin delivery log (stats/filter/retry).
- Email providers (SMTP/SendGrid/Postmark/SES/Mailgun) with encrypted
credentials + test; the email adapter prefers the default provider and
falls back to the legacy SMTP config; surfaced in ConnectionRegistry.
- NotificationService.send_all() is now a non-blocking shim that routes
system alerts through the bus. Migrations 023 + 024.
Tests: backend/tests/test_notification_bus.py (20). Full suite: 391 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>