328 more routes used a JSON body as a dict straight from
request.get_json(), so a body of null, [] or a bare string crashed them
into a 500. Bare reads become json_object(); the
'get_json(silent=True) or {}' reads of action routes become
json_object(optional=True), which still treats a missing body as {}.
Routes that take arrays or pass the body through untouched are left
alone. The full backend suite shows no new failures.
Also carries three files' bare section citations (apps, backups,
notifications) and the extended citation guard, which needed them.
Eleven routes passed request.get_json() straight to data.get(...), so
a body of null, [] or a bare string crashed them into a 500. They now
read the body through json_object(), which answers a typed 400.
Toasts showed backend prose verbatim, with no code to translate
against and "application" where the UI says "service". Not-found,
missing-field, permission, conflict and agent-offline errors now come
from factories in app/exceptions.py that pair one fixed message with
a dotted code (not_found.<resource>, validation.required,
permission.denied, conflict.exists, agent.offline), so the client can
translate them.
The shared app-loading helpers and the apps, domains, servers,
databases and backups routes raise those errors instead of hand
shaping bodies, which drops the hand-shaped body count from 1132 to
850. Five routes picked an HTTP status by comparing error text; they
now key off a result code or a sentinel so rewording can't change a
status. Five broad excepts that echoed str(exc) now log the crash and
return the request id instead.
The user-delete guard 79a78c3c added queried Application straight from
the controller, tripping the API controller boundary ratchet. The count
now lives in application_restore.applications_owned_by — the module
that owns Application's recycle-bin semantics and documents why
tombstoned rows block too.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-up to 1704d60b, prompted by 'can this happen elsewhere?' — a
mapper-wide audit found 22 more one-to-many relationships whose child
FK is NOT NULL but which lacked a delete cascade, so hard-deleting the
parent (user/server/workspace/template/plugin delete, deployment
retention pruning, WP site teardown) raised the same IntegrityError as
the Recycle Bin purge.
20 get the cascade: plugin installs, deployment diffs, metric alerts,
template assignments (both sides), WP activities/sync/promotion jobs
(both sides), workspace members/keys, and the per-user detail rows
(passkeys, OAuth identities, notification prefs, sanitization
profiles, source connections, workflows, workspace memberships).
2 stay uncascaded on purpose, now enforced: User.applications (delete_
user refuses with 409 while the user owns apps — apps must go through
app delete + Recycle Bin purge so containers/volumes are cleaned) and
CloudProvider.servers (no delete path exists; dropping VM inventory
must stay a deliberate decision). The invariant sweep is now global
across every mapper with exactly that allowlist, which may only shrink.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
QueryParseError is now a ValidationError subclass — the global handler
answers it as a 400 and per-route translations become unnecessary (the
existing ones keep working). PageQuery parses, defaults, and bounds
page/per_page in one place; admin audit-logs, activity feed, error logs,
and telemetry events adopt it (telemetry keeps its 200 cap). git.py and
source_connections.py keep hand-parsed params deliberately — they proxy
pagination to Gitea/GitHub/GitLab/Bitbucket, not to DB queries.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The two worst endpoints in the backend, both measured in plan 69 phase 6.
**/status/apps** ran one `docker compose ps` per app plus a `docker inspect`
per container -- roughly 30 apps x 3 containers = ~120 process spawns per
request -- and it is refilled on a timer, so the cost never amortised. It is
now ONE host-wide `docker ps` whose output is indexed by the compose
project/service/working-dir labels Docker already stamps on every container,
shared between callers through a 3-second in-memory snapshot.
The snapshot is deliberately module memory rather than CacheService: it must be
droppable synchronously, because a status that survives the action that changed
it reads as a broken panel. So `invalidate(app_id)` is called from start, image
apply, deploy and rollback. 3s is short enough that a missed hook self-corrects
within one poll.
**/admin/activity/summary** ran two 90-day loops issuing one COUNT per day =
180 sequential queries. Now one `GROUP BY func.date(...)` per series with the
gaps zero-filled in Python, so the response shape is byte-identical. `func.date`
is portable across the SQLite and PostgreSQL this app supports. A third N+1 fell
out on the way: the top-users block did one SELECT per user, now a single IN.
New tests assert the zero-fill specifically -- a day with no rows must still
appear and the series length must match the window, which is exactly what a
GROUP BY drops if you forget.
1140 backend tests pass. The 3 failures in that run (test_fail2ban_jail_service,
test_url_swap, test_site_routing) all reproduce on a clean tree -- the last is
the known "serverkit-wordpress extension not installed" class.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AuditService.get_logs gains a target_id filter (paired with target_type) and
/admin/activity/feed passes both through — the per-resource activity
timeline can now ask 'what happened to THIS app'.
Turn the single-base "Managed Sites Domain & HTTPS" card into a registry
manager. Operators can now register several base domains, mark a default,
remove one, flip each between wildcard/per-site DNS, and set up per-base
wildcard HTTPS with a chosen DNS provider — all from the UI.
- Each registered base renders as a row with Default / HTTPS badges, a DNS-mode
select, a "Set up / Renew HTTPS" action (using the selected provider, base
passed through), plus Make default / Remove.
- Back-compat: an install with no registry rows shows its single legacy base as
an editable in-place row (saves sites_base_domain); adding a second domain
seeds that legacy value into a real row server-side, and the table takes over.
A fresh install with no base shows an empty state + the Add form.
- Server public IP and DNS provider are shared across bases.
- New admin PATCH /admin/sites-https/base-domains/<domain> for DNS-mode edits;
api service gains add/remove/setDefault/update helpers and setupSitesHttps now
takes a base.
Proven: test_site_base_domains.py adds a PATCH-dns-mode API test (13 pass);
frontend lints clean (0 errors) and builds.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire the base-domain registry into HTTP + the cert flow:
- SitesHttpsService.setup(provider_id, email, base) now issues the wildcard cert
for a specific base domain and persists HTTPS state to THAT base's registry row
(recording the provider used), so example.com on Cloudflare and toto.com on
Route53 each get their own cert; falls back to the legacy sites_* settings for
a single-base install. status() now also returns the full `bases` registry.
- Admin API: POST/DELETE /admin/sites-https/base-domains[/<domain>] and
.../<domain>/default to register, remove, and set the default base; setup
accepts an optional `base`.
- GET /api/v1/domains/base-domains lists the bases a new site can use (registry,
or the single legacy base) with the pre-selected default — the create-flow
picker's data source. give-subdomain + suggest-subdomain accept a `base`.
- WordPress create accepts baseDomain end to end (API -> service -> subdomain_for),
and picks the covering base's HTTPS state for the baked-in site URL.
Proven in test_site_base_domains.py: the list endpoint (registry + legacy),
admin add/set-default/remove, and per-base HTTPS setup persisting to one row
while the other base stays HTTP. WordPress suite (66) still green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
- Auto-generate SERVERKIT_ENCRYPTION_KEY in production when missing.
- Detect canonical domain from Host/X-Forwarded-Host; persist via SystemSettings.
- Expand CORS origins dynamically to include the canonical domain.
- Add SystemTab UI to view/save detected panel domain and HTTPS preference.
- Install script writes key, prompts for PANEL_DOMAIN, and sets nginx IP→domain redirect.
- Add backend tests for key bootstrap and domain detection helpers.
One-time operator setup that makes every managed subdomain HTTPS. New
SitesHttpsService.setup creates the *.<base> + <base> A records via a connected
DNS provider and issues a wildcard Let's Encrypt certificate over DNS-01,
reusing that provider's credentials — then flips sites_https_enabled on.
- AdvancedSSLService.issue_wildcard_cert now supports route53 (AWS env creds) as
well as cloudflare, takes an email, installs the matching certbot DNS plugin
best-effort, and returns the cert/key paths.
- SiteDomainService gains https_enabled / wildcard_cert_paths / covers. create_site
installs new subdomain sites at https when enabled, and _write_app_vhost serves
the wildcard cert on vhosts whose every domain it covers (custom domains keep
their own cert).
- NginxService.create_site takes optional ssl_cert/ssl_key and bakes a 443 block +
HTTP->HTTPS redirect (idempotent via full regen).
- sites_base_domain / server_public_ip / sites_https_enabled are now discoverable
settings. Admin endpoints GET/POST /admin/sites-https/{status,setup}, surfaced
in Settings → Site as a "Managed Sites Domain & HTTPS" panel.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Frontend and backend updates to improve activity visibility and multi-server support.
Backend: extend activity summary window to 90 days and add per-day series for the top user (top_user_daily) to the activity summary API.
Frontend:
- Activity tab refactor: rename feed→logs, add pagination, server-side audit log fetching, richer filters, icons, action formatting, details rendering, and improved loading/empty states.
- New ContributionGraph component + styles to visualize daily activity and per-user contributions; graphs injected into ActivityTab.
- Sidebar: separate manual vs auto-expanded state, auto-expand active parent, and persist preset switch locally while persisting to API in background.
- Dashboard: add server selector and remote-server metrics fetching, use remote metrics when a non-local server is selected, adjust polling and refresh semantics, and various UI/icon tweaks.
- Appearance tab: remove inline white-label UI; introduce a dedicated WhiteLabelTab component for custom branding.
Misc: add component and page SCSS files and small README updates describing new features and priority areas.
Introduce team invitation support and a revamped email subsystem plus admin features. Adds Invitation model and API endpoints (create, list, revoke, resend, validate) and integrates invitation validation into user registration. Replaces old email account model with richer email models (EmailDomain, EmailAccount, EmailAlias, EmailForwardingRule, DNSProviderConfig) and updates models __init__. Refactors email API to manage domains, accounts, aliases, forwarding, DNS providers, spamassassin, Roundcube/Postfix services and queue/log endpoints. Adds several backend services (invitation, DNS provider, DKIM, Dovecot, Postfix, Roundcube, SpamAssassin, permission service) and a DB migration for permissions/invitations. Enhances admin API with permission management and activity dashboard endpoints, extends audit log actions, and adds a permission_required RBAC decorator. Includes frontend additions for settings/invitations UI and miscellaneous config/service updates and tests.
Introduces backend endpoints and models for admin features including user management, system settings, and audit logging. Adds RBAC middleware, audit log and system settings models, and services for audit and settings. Updates authentication to support setup flow and registration control. On the frontend, adds settings tabs for users and audit logs, a setup page, and updates routing and context to support setup and registration state. Styles and API service are updated to support new features.