936 Commits
Author SHA1 Message Date
Juan Denis 14fc04fbae Merge pull request #48 from jhd3197/dev
Cloudflare zone ops, DNS ownership overhaul, and backup protection
v1.6.32
2026-06-22 21:59:33 -04:00
github-actions[bot] 7cbc4700aa chore: bump version to 1.6.32 [skip ci] 2026-06-23 01:52:48 +00:00
Juan DenisandClaude Opus 4.8 84ee2ce789 fix(wordpress): canonical site URL must reflect DB URL, not panel origin
_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 652d903a) 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) <noreply@anthropic.com>
2026-06-22 21:52:27 -04:00
github-actions[bot] 83a88b1649 chore: bump version to 1.6.31 [skip ci] 2026-06-23 01:38:16 +00:00
Juan DenisandClaude Opus 4.8 5836294c4d feat(cloudflare): R2 / KV / D1 developer platform (Phase 6)
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) <noreply@anthropic.com>
2026-06-22 20:39:12 -04:00
Juan DenisandClaude Opus 4.8 2f1999018d feat(cloudflare): Cloudflare Tunnels (Phase 5)
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) <noreply@anthropic.com>
2026-06-22 20:34:32 -04:00
Juan DenisandClaude Opus 4.8 02774f741e feat(cloudflare): Workers edge hosting (Phase 4)
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) <noreply@anthropic.com>
2026-06-22 20:27:36 -04:00
Juan DenisandClaude Opus 4.8 5ec5be4b31 feat(cloudflare): WAF custom rules (Phase 3)
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) <noreply@anthropic.com>
2026-06-22 20:20:26 -04:00
Juan DenisandClaude Opus 4.8 6bc0ac36e5 feat(cloudflare): cache purge (Phase 2)
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) <noreply@anthropic.com>
2026-06-22 20:12:20 -04:00
Juan DenisandClaude Opus 4.8 125b1e4553 feat(cloudflare): zone settings page (Phase 1)
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) <noreply@anthropic.com>
2026-06-22 20:08:38 -04:00
Juan Denis 396876ccc1 Update .gitignore 2026-06-22 19:54:38 -04:00
Juan DenisandClaude Opus 4.8 6db6680433 docs(backups): backup protection guide
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) <noreply@anthropic.com>
2026-06-22 19:53:42 -04:00
Juan DenisandClaude Opus 4.8 7b9ae4f934 feat(backups): protection panel UI + global cost view
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) <noreply@anthropic.com>
2026-06-22 19:53:34 -04:00
Juan DenisandClaude Opus 4.8 1c641b2a9d feat(backups): backup protection backend — policy, scheduler, smart backups, 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) <noreply@anthropic.com>
2026-06-22 19:53:23 -04:00
Juan DenisandClaude Opus 4.8 9de7cc65db feat(notifications): route config notices into the notification center; 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) <noreply@anthropic.com>
2026-06-22 18:40:12 -04:00
Juan Denis 4a02b5f3f6 fix: Python 3.10 tomllib fallback, alembic logging, and frontend compose healthcheck 2026-06-22 18:12:50 -04:00
Juan DenisandClaude Opus 4.8 a03bd0c6dc feat(dns): wildcard vs per-site subdomain DNS toggle in Site Settings (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) <noreply@anthropic.com>
2026-06-22 18:00:50 -04:00
Juan DenisandClaude Opus 4.8 da404bd8cf fix(toast): accept a sonner options object, not just a numeric duration
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) <noreply@anthropic.com>
2026-06-22 18:00:50 -04:00
github-actions[bot] f0e92b3f8e chore: bump version to 1.6.30 [skip ci] v1.6.30 2026-06-22 20:53:20 +00:00
Juan Denis edf6260dee fix(build-release): make release builder work on Windows/Git Bash
- 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
2026-06-22 16:52:34 -04:00
Juan DenisandClaude Opus 4.8 4fba9c1f13 feat(dns): give-this-a-subdomain + all-managed-records view (Phase 4 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) <noreply@anthropic.com>
2026-06-22 16:45:10 -04:00
Juan DenisandClaude Opus 4.8 e0e6312c4f feat(dns): per-base-domain subdomain modes — wildcard vs per-site (Phase 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) <noreply@anthropic.com>
2026-06-22 16:40:35 -04:00
Juan DenisandClaude Opus 4.8 37707e7b5d feat(dns): "Changes to your Cloudflare" UI — activity feed + zone mirror (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) <noreply@anthropic.com>
2026-06-22 16:40:26 -04:00
Juan DenisandClaude Opus 4.8 38a258ee2d feat(dns): change log + admin notice on failure (Phase 2 backend)
#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>
2026-06-22 16:34:17 -04:00
Juan DenisandClaude Opus 4.8 b1b5ed1161 feat(dns): ownership ledger + never-touch-foreign guard + zone mirror (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) <noreply@anthropic.com>
2026-06-22 16:24:53 -04:00
Juan DenisandClaude Opus 4.8 1127243d1d feat(dns): unify Cloudflare credentials behind one connection + shared 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) <noreply@anthropic.com>
2026-06-22 16:12:31 -04:00
Juan Denis 306cea145d fix(build-release): make release builder work on Windows/Git Bash
- 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
2026-06-22 16:06:13 -04:00
Juan Denis 0f4e5821da feat: add dismissible system notices for common misconfigurations
- 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
2026-06-22 14:42:09 -04:00
Juan Denis 652d903ae7 Normalize emails, add panel_origin & domain-aware URLs
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.
2026-06-22 14:37:44 -04:00
Juan Denis fbccb7eab4 Merge pull request #47 from jhd3197/dev
Add release smoke test workflow and refine rsync excludes
v1.6.29
2026-06-22 08:12:34 -04:00
github-actions[bot] e192250ef1 chore: bump version to 1.6.29 [skip ci] 2026-06-22 12:11:23 +00:00
Juan Denis d3b41f4f2a Merge branch 'dev' of https://github.com/jhd3197/ServerKit into dev 2026-06-22 08:11:07 -04:00
Juan Denis f0ee3b2e3d Add release smoke test and refine rsync excludes
Introduce a GitHub Actions workflow (release-smoke.yml) that builds the release tarball on pushes/PRs to dev/main for relevant paths, catching release-script issues early. Adjust scripts/build-release.sh rsync excludes to explicitly target backups directories (root/backups and backend instance/dev-data backups) instead of a broader exclude, preventing accidental omission of needed files during release packaging.
2026-06-22 08:09:29 -04:00
Juan Denis f01b7cde7f Merge pull request #46 from jhd3197/dev
Unified jobs, notification bus, telemetry stream, and workspace/service UI refresh
v1.6.28
2026-06-22 01:51:02 -04:00
github-actions[bot] aafbc39332 chore: bump version to 1.6.28 [skip ci] 2026-06-22 05:45:25 +00:00
Juan Denis 0e29978e7b Truncate large telemetry payloads & UI robustness fixes
Telemetry: ensure sanitized payloads bigger than the 32KiB threshold are replaced with a {_truncated: True} marker before storage, and add a unit test covering that behavior.

Backend API: adjust imports in secrets_webhooks to use WorkspaceService and include User model.

Frontend: make Sidebar resilient to malformed active_workspace JSON in localStorage (try/catch), and ensure socketService.disconnect() is called when user becomes unauthenticated in NotificationsProvider to clear notifications and unread count.

Files: backend/services/telemetry_service.py, backend/tests/test_telemetry.py, backend/app/api/secrets_webhooks.py, frontend/src/components/Sidebar.jsx, frontend/src/contexts/NotificationsContext.jsx
2026-06-22 01:45:15 -04:00
github-actions[bot] f31edb8c2f chore: bump version to 1.6.27 [skip ci] 2026-06-22 05:38:42 +00:00
Juan Denis 10ca0b3970 Refactor sidebar, workspace list, and Git settings UI
Rename service settings 'repository' → 'git' (routes, labels, and links) and update App route comment. Reorder sidebar footer so user mini and notification bell share a single row and add accessible user-mini button. Adjust notification panel positioning, add show animation, and responsive layout tweaks. Replace Workspaces card grid with a dense table view: add search, status segment control, bulk select checkboxes, and selection state. Minor SCSS tweaks for sidebar button sizing and WordPress action width. These changes improve discoverability and bulk management UX.
2026-06-22 01:35:36 -04:00
Juan Denis bb0bd7ba1d Workspace and queue bus improvements
- Add workspace detail routes, tabs, and page title handling
- Add workspace switcher select component and updated workspace styles
- Add queue detail page and protect system queue groups from REST mutations
- Update sidebar layout styles and queue operations UI
2026-06-22 01:16:00 -04:00
Juan Denis 9b7f69e654 Normalize service detail page with WordPress layout and add Domain & SSL
- Rename service Environment tab to Env Vars and settings Environment to Environment Type
- Add Domain & SSL section to service settings with attach domain + Let's Encrypt flow
- Surface primary domain from domains array in service subtitle
- Add prominent repo connection pill to WordPress detail header
- Apply WordPress list layout to Services page (drop KPIs, tabs+search toolbar)
2026-06-22 01:15:53 -04:00
github-actions[bot] c9d50788e3 chore: bump version to 1.6.26 [skip ci] 2026-06-22 02:11:23 +00:00
Juan DenisandClaude Opus 4.8 6f99bef3a9 refactor(jobs): dispatch workflow events via a workflow.dispatch job (no thread)
WorkflowEventBus.emit no longer spawns a per-event daemon thread to match +
trigger workflows. It now enqueues a single 'workflow.dispatch' job; the new
WorkflowEventBus.dispatch_event handler does the matching (active event-trigger
workflows whose eventType matches, honoring the 60s cooldown) and enqueues a
workflow.execute job per match. emit is a non-blocking enqueue, best-effort
(never raises into the caller), and the whole event -> dispatch -> execute path
now flows through the bus (observable, persistent across restart). Removes the
last ad-hoc thread on the workflow path.

Registered alongside workflow.execute in WorkflowEngine.register_jobs.

tests: dispatch handler registered; emit enqueues a dispatch job (not a thread);
dispatch fans out only to the matching event workflow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 22:01:38 -04:00
Juan DenisandClaude Opus 4.8 fe7415842f feat(jobs): scheduled backups as backup.run jobs; fix dormant scheduler (Phase 7)
Wire scheduled backups onto the unified job system and retire the orphaned
backup scheduler thread.

- BackupService.start_scheduler/_scheduler_loop were defined but NEVER called,
  so scheduled backups never actually ran. Replace with check_backup_schedules()
  (enqueues a backup.run job per due schedule) driven by a new
  builtin.backup_scheduler tick, plus a run_backup_job handler that executes one
  schedule via the existing _run_scheduled_backup. max_attempts=1 (backups
  aren't idempotent); a failed run marks the job failed. This activates
  scheduled backups, but stays gated by the existing backup config.enabled
  (default False), so installs without configured backups are unaffected.
- Fix a latent bug in _send_backup_notification: it called
  NotificationService.send_all(title=, message=, severity=) but send_all takes
  a list of alert dicts — corrected (faithful: still routes to system channels
  via the queue-backed bus).
- Monitoring intentionally left as-is: a user-toggled continuous stream whose
  alert delivery already goes through the queued send_all. Phase 8 orchestration
  is already met — send_all is a non-blocking shim over broadcast_system.
- tests: backup.run register/handler success+failure; scheduler tick enqueues
  only due+enabled schedules and is inert when disabled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 21:50:27 -04:00
Juan DenisandClaude Opus 4.8 70f8697586 feat(jobs): admin Jobs UI page over the unified job API (Phase 9)
Add /jobs — the single pane to observe and manage all background work, built
on the /api/v1/jobs API. Mirrors the DeliveryLog/Telemetry admin pages.

- Jobs table polls every 5s: status Pills (succeeded/running/pending/failed/
  cancelled), kind, owner, attempts, age. Status/kind filters. Per-row Cancel
  (pending/running) and Retry (failed/cancelled) with toast feedback. Row
  expands to a detail view (payload/result/error/correlation/timing) via getJob.
- Scheduled-jobs table: name, kind, schedule (cron or "every Nh"), next/last run,
  with Run-now and Pause/Enable.
- Stat cards (total/running/pending/succeeded/failed) from /jobs/stats.
- Wiring: services/api/jobs.js module bound into ApiService; route + page title
  in App.jsx; "Jobs" item in the sidebar "system" group; _jobs.scss imported
  in main.scss. Admin-gated client-side via useAuth().isAdmin.

Lint clean (0 errors) and production build passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 21:28:41 -04:00
Juan DenisandClaude Opus 4.8 c0cb0f088e feat(jobs): run workflow executions as a workflow.execute unified job (Phase 6)
Migrate workflow executions onto the unified job system (single-job wrap).
WorkflowEngine is split into _create_execution (validate graph + persist the
running WorkflowExecution row) + run_execution (the synchronous DAG core) +
enqueue_execution (create row, enqueue a 'workflow.execute' job, return the
execution id immediately). execute_workflow stays as the synchronous primitive.

- All four triggers route through enqueue_execution: manual API, webhook,
  cron tick (builtin.workflow_schedules), and the WorkflowEventBus. The manual
  and webhook endpoints no longer block the HTTP request on the whole DAG; the
  cron tick enqueues one job per due workflow instead of running them serially
  inside the tick; the event bus thread now only matches + enqueues.
- max_attempts=1 (workflows deploy/script/notify — not idempotent). A failed
  run raises so the unified job is marked failed; the WorkflowExecution row
  keeps the detailed per-node status/logs. Graph validation still happens
  synchronously at enqueue, so an invalid workflow surfaces immediately.
- tests: register, enqueue-creates-row+job, empty workflow runs to success,
  failure path marks the job failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 21:13:55 -04:00
Juan Denis 73d611966f ci(release): build and attach linux amd64/arm64 tarballs
The release workflow was publishing Docker assets and source but never
the serverkit-vX.Y.Z-linux-{amd64,arm64}.tar.gz files that install.sh,
update.sh, and 'serverkit update' expect to download. This caused release
updates to fail with 'Failed to download the release tarball.'

- Make scripts/build-release.sh honor BUILD_ARCH for CI matrix builds.
- Add a build-release job that builds tarballs on native amd64 and arm64
  runners and uploads them as artifacts.
- Wire the release job to download those artifacts, include them in the
  GitHub Release, and checksum them.
- Add the one-line installer to the release notes body.
2026-06-21 21:13:10 -04:00
Juan Denis ed7296500c Ignore new roadmap docs in .gitignore
Add docs/NOTIFICATIONS_ROADMAP.md and docs/ORCHESTRATION_ROADMAP.md to .gitignore so newly added roadmap documents are not tracked by Git. This keeps in-progress planning files out of commits until ready for publishing.
2026-06-21 21:06:35 -04:00
Juan DenisandClaude Opus 4.8 85265df138 feat(jobs): run template installs as a deploy.install unified job (Phase 5)
Migrate deployment installs off their ad-hoc daemon thread onto the unified
job system. DeploymentJobService.install_template now enqueues a
'deploy.install' Job instead of spawning a thread via _start_background_job
(retired); the JobConsumer runs it by calling the existing run_job.

- max_attempts=1: installs create containers/files/an app row and are not
  idempotent, so no auto-retry (preserves prior single-shot behavior).
- The unified Job shares the DeploymentJob's correlation_id and links back via
  owner_type='deployment_job'/owner_id; the DeploymentJob row stays the rich
  record (plan/steps/logs/progress). A failed run raises so both rows show failed.
- Bonus: CLI `deploy-template` (no --wait) now persists to the queue and runs
  when a consumer is available, instead of dying with the CLI process.
- tests: handler success/failure mapping + install_template enqueues-not-threads.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 21:01:38 -04:00
Juan DenisandClaude Opus 4.8 8428326a26 feat(jobs): unified background-job orchestration on the Queue Bus
Generalize the Notification Bus pattern (thin {id} message -> fat DB row
-> single consumer) into a Job/WorkUnit layer on top of the Queue Bus, so
background work flows through one pipe instead of many ad-hoc threads.

- app/jobs: Job + ScheduledJob models, handler registry, JobService +
  ScheduledJobService, one generic JobConsumer, one JobScheduler ticker,
  and a plugin SDK surface (app.plugins_sdk.jobs).
- Migrate the 8 per-domain daemon scheduler threads (auto-sync,
  snapshot-retention, workflow, health-check, wp-update, api-background,
  pairing-prune, registrar-expiry) to ScheduledJob rows + built-in
  handlers that reuse the exact prior check functions: cadence and
  per-task de-dup unchanged, only the trigger moved.
- Retire the orphaned in-memory BackgroundJobService; repoint
  /api/v1/performance/jobs at JobService.
- Add /api/v1/jobs (list/get/cancel/retry/stats; scheduled list/run/
  toggle) as a single pane over all background work.
- tests/test_jobs.py: enqueue->run->succeed, dead-letter->failed,
  unknown-kind, cancel, retry, scheduler tick/advance, built-ins.

No Alembic migration (tables created via create_all), avoiding a
multi-head clash with the in-flight telemetry work. Plan lives in
docs/ORCHESTRATION_ROADMAP.md (local/git-ignored).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 20:44:12 -04:00
Juan Denis 3d3975fa3a feat(telemetry): unified system event stream with correlation
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
2026-06-21 20:19:57 -04:00