Commit Graph
353 Commits
Author SHA1 Message Date
Juan DenisandClaude Opus 4.8 3f50db7a34 feat(git): make the provider chips real, selectable buttons
The repo-connect provider strip was a static "explainer" — clicking GitLab/
Bitbucket/Gitea did nothing, and the GitHub picker showed regardless, which
read as confusing. Now (when a host form passes onSelect) the chips are radio
buttons that drive the connect method: GitHub shows the one-click picker;
every other provider shows the paste-a-URL field with that host's placeholder
(gitlab.com / bitbucket.org / gitea / scp-style ssh). A recognized URL still
auto-selects its provider. GIT_PROVIDERS gained `oauth` + `placeholder`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 22:37:56 -04:00
Juan Denis 942342fac2 refactor(queue-bus): clarify Operations UI layout
- Overview: group cards are fully clickable and navigate to Messages
- Overview: add 'system' badge for system-owned groups
- Groups: add per-row 'Create Queue' action and clearer 'View Queues' label
- Messages: remove out-of-place 'Create Queue' button; keep Send Message only
- Create Queue modal: include group selector
2026-06-20 22:31:17 -04:00
Juan Denis 9b4618b24b feat(queue-bus): introduce ServerKit Queue Bus under Operations
- Add pluggable queue bus with SQLAlchemy backend (Redis-ready interface)
- Models: QueueGroup, Queue, QueueMessage with retries, visibility timeout,
  delayed delivery, priority ordering, and dead-letter handling
- REST API at /api/v1/queue for groups, queues, messages, and stats
- Plugin SDK: from app.plugins_sdk import queue
- Operations UI page with group/queue/message management and send-message form
- Migrate webhook deliveries from raw threading.Thread to the queue bus
- Add webhook consumer with retry/dead-letter support
- Migration 022_queue_bus and backend tests
2026-06-20 22:25:06 -04:00
Juan DenisandClaude Opus 4.8 fb96db72d4 feat(git): one-click "Connect with GitHub" repo picker
Add a shared GithubRepoPicker that lists the user's repositories from the
GitHub source-connection OAuth that already powers /services/new (no new
backend), and render it above the paste-a-URL fields in RepoConnectForm.
Picking a repo fills the clone URL + default branch, so linking a repo to a
WordPress site — and any future RepoConnectForm surface — becomes "authorize
once, pick a repo" instead of pasting a URL. Falls back gracefully to the URL
form when GitHub isn't configured/connected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 22:22:29 -04:00
Juan Denis f7e51cc570 Allow committed screenshots and fix ServiceDetail effect
Update .gitignore to allow committed screenshot assets under docs/screenshots/ (these are generated mock-data README images and should be tracked). Fix a bug in frontend/src/pages/ServiceDetail.jsx: avoid referencing isUpload from outer scope (temporal dead zone) by deriving it inside the useEffect, and replace the effect dependency with service?.source so the effect runs correctly when the service source changes. This prevents a render-time crash and ensures versions are loaded for uploaded services.
2026-06-20 22:01:27 -04:00
Juan DenisandClaude Opus 4.8 505ff0f2cd feat(git): shared RepoConnectForm + clickable commits; center icon tiles
- Extract the repo-connect form into components/git/RepoConnectForm.jsx
  (provider strip + URL/branch/auto-deploy + optional tracked paths +
  connected state) — the canonical reusable surface. components/wordpress/
  GitConnectForm.jsx is now a thin WordPress-flavored wrapper over it, so
  WordPress renders identically and other pages can drop the shared form in.
- Make the WordPress commit log clickable: each SHA links to the commit on
  the remote (GitHub/GitLab /commit, Bitbucket /commits; https + scp-ssh).
- Fix off-center icon tiles on /services/new (grid + place-items + block svg
  instead of inline-flex).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:58:28 -04:00
Juan DenisandClaude Opus 4.8 0ee1151cb2 feat(wordpress): rebuild site-detail page (sk-topbar, centered, grouped Settings)
Rework /wordpress/:id to match the new prototype and stay consistent with
the rest of the app:

- Top bar is the canonical PageTopbar (.sk-topbar) — same chrome as the
  WordPress list page; breadcrumb in the title slot, trimmed actions
  (Open Files, Open Database, View Logs, Dashboard, Auto Login).
- Identity row: no card background + empty-circle icon (per the prototype),
  in a CENTERED max-width body while the top bar stays full-bleed
  (.main-content:has(.wp-detail-page){padding:0}, normal scroll kept).
- Overview leads with a non-redundant KPI row (uptime / visitors / error
  rate / disk), consolidated Site information, a Traffic chart, and a
  Recent-activity feed; redundant panels dropped (killed the version-3x).
- Settings: grouped, URL-routed left nav (/wordpress/:id/settings/:section,
  shareable + refresh-safe) — General (Add Domain / Change URL / Clone),
  Configuration (PHP, Updates), Connections (Git), Security (Security,
  Vulnerabilities), Reports; those tabs left the top strip.
- Fix a wasted right column: .app-overview-grid collapses to one column
  when it has no .app-overview-right.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:34:53 -04:00
Juan DenisandClaude Opus 4.8 31ee5fee6a feat(nav): fold Remote Access into Servers tabs + redesign page
Remote Access was a standalone Infrastructure sidebar item, but it's
fundamentally agent-to-agent WireGuard pairing, so move it into the
Servers tab group (mirrors the Dynamic-DNS-into-Domains consolidation).
The /remote-access route now lives under the Servers TabGroupLayout and
the Servers sidebar item stays lit via matchPrefixes; the URL is
unchanged so existing links still work.

Rebuild the page body: the "Expose a Local Service" action rides the
shared PageTopbar via useTopbarActions, and the shadcn Card/Badge markup
is replaced with page-local .ra-* SCSS (styles/pages/_remote-access.scss)
plus the ds Pill and shared EmptyState. The old page rendered with no top
padding because the unlayered .ui-card-content rule beats the layered
Tailwind p-5 utility; a purpose-built SCSS element sidesteps that.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 19:25:42 -04:00
Juan DenisandClaude Opus 4.8 e24390cc03 feat(logs): Servers-style header for Terminal/Logs
Render the sections (Terminal / Log Files / System Journal / Processes / Services) as inline PageTopbar tabs routed via /terminal/<tab>, like the Servers group — dropping the separate shadcn TabsList and the 'shells / logs / processes / services' tagline. Title is now just 'Logs'. The active pane swaps on the URL and keeps filling the full-bleed region.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 13:07:49 -04:00
Juan DenisandClaude Opus 4.8 4cfc3cd0a6 fix(secrets): repair load + tab layout on the Secrets/Webhooks page
The api module called this.get/post/patch/delete, which the ApiService doesn't expose (only request(path, {method, body})) — so every load threw 'this.get is not a function'. Rewrote it to use request() like the other api modules. Also dropped an erroneous className='tabs' on the <Tabs> root: that class (display:flex, meant for the TabsList strip) made the root a row, so the panel rendered beside the tabs instead of below them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 12:55:15 -04:00
Juan DenisandClaude Opus 4.8 4aa4832527 feat(nav): fold Dynamic DNS into Domains; auto-hide GPU Monitor
Dynamic DNS becomes a tab in the Domains group (Domains / DNS Zones / SSL / Dynamic DNS) and is removed from the main nav, so DNS lives in one place. GPU Monitor is hidden from the sidebar when the host reports no GPU (getGpuInfo().available), reusing the wpInstalled-style condition gate for top-level items.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 12:55:04 -04:00
Juan DenisandClaude Opus 4.8 ad4d7eb8bf feat(logs): full-bleed Terminal/Logs with follow-tail + LIVE badge
Terminal/Logs now fills the viewport like the Servers ops view (full-bleed flex layout through the Radix tab panel). LogContent sticks to the newest line on new content/selection (respecting manual scroll-up) and shows a floating LIVE badge while a log auto-follows its tail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 12:47:47 -04:00
Juan Denis 19f702b8e8 Add TabGroupLayout and topbar actions
Introduce a TabGroupLayout and useTopbarActions hook to render shared PageTopbar + sub-nav for tabbed page groups (Servers, Domains, Services, Files, Monitoring, Marketplace, WordPress). Pages now publish top-bar actions to the enclosing layout instead of rendering their own PageTopbar, reducing remounts and keeping sidebar highlighting consistent. Sidebar items gain matchPrefixes to stay active across grouped routes. Updated many pages to use the hook and new layout, adjusted classes for full-bleed/tab-group shells, and added supporting SCSS. This makes tab groups behave like real tabs and centralizes header behavior.
2026-06-20 12:04:23 -04:00
Juan DenisandClaude Opus 4.8 70883d50a9 feat(ui): WAF panel + tab on the app detail page
- API client: WAF methods (getWafPolicy/updateWafPolicy/applyWaf/
  getWafEvents/getWafStatus/installWaf) in containerOps.js.
- WafPanel.jsx: install banner (+ install action), mode (off/detect/block)
  with descriptions, paranoia level + anomaly threshold, a disabled-CRS-rule
  chip editor, Save (saves + applies) + Re-apply (surfaces manual_include
  when no vhost is found), and a recent-events table.
- ApplicationDetail: a "WAF" tab (nginx-served apps), wired like Container Ops.

Build passes; new file lint-clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 14:41:26 -04:00
Juan DenisandClaude Opus 4.8 24d5362493 feat(ui): surfaces for DDNS, image-update, auto-sleep, auto-scale, GPU
- API client: DDNS methods (dns.js) + per-app container-ops/GPU methods
  (new containerOps.js), auto-bound via index.js.
- ApplicationDetail: a "Container Ops" tab (Docker apps) via the new
  ContainerOpsPanel — image-update check/apply, auto-sleep toggle +
  sleep/wake, and auto-scale policy + manual scale.
- GpuMonitor page (/gpu): per-GPU cards (utilization, VRAM, temp, power,
  fan, driver) + compute-process table; clean empty state when no GPU.
- DynamicDns page (/dynamic-dns): DDNS host CRUD with a one-time token +
  ready-to-use update URL, regenerate-token, and delete.
- Routes + sidebar nav (GPU Monitor, Dynamic DNS) + page SCSS.

Build passes; new files lint-clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:34:45 -04:00
Juan DenisandClaude Opus 4.8 e738f3d509 feat(ui): add topbar layout variant to the layout switcher
- Add 'topbar' to LayoutContext and a third switcher button (Sidebar |
  Compact | Top bar).
- Topbar: the sidebar becomes a horizontal bar pinned to the top; top-level
  items lay out in a horizontally-scrollable row with an underline active
  indicator. Category headers, expand chevrons, and sub-items are hidden
  (sub-nav is reached from each page's in-page navigation), which sidesteps
  the horizontal-overflow vs. dropdown clipping trap. Desktop-only; the
  mobile drawer is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 03:05:39 -04:00
Juan DenisandClaude Opus 4.8 cd16d9a479 feat(ui): add layout switcher with compact rail variant
- New LayoutContext (localStorage-persisted, sets data-layout on <html>),
  mirroring ThemeContext.
- "Layout" switcher in the sidebar user menu: Sidebar | Compact.
- Compact rail: a 64px icon-only sidebar that expands to the full sidebar
  on hover/keyboard focus; desktop-only, the mobile drawer is unchanged.
- Reuses existing nav markup — labels hide via font-size:0 (kept in the
  accessibility tree); no nav-render changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 03:02:23 -04:00
Juan Denis 2b7a98c348 feat: canonical domain + encryption key bootstrap
- 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.
2026-06-19 01:34:02 -04:00
Juan Denis bf9fee131e feat: add secrets manager and inbound webhook gateway
- Encrypted vault + secret models with Fernet encryption
- Webhook endpoint + delivery log models with HMAC verification
- SecretVaultService / WebhookGatewayService business logic
- /api/v1/vaults, /secrets, /webhooks REST endpoints (JWT + public receive)
- Migration 020_secrets_webhooks
- Frontend Secrets & Webhooks page, API methods, sidebar, styles
- Add SERVERKIT_ENCRYPTION_KEY to .env.example
2026-06-19 01:33:46 -04:00
Juan Denis 70a70642c1 feat(services): support manual/local apps and zip uploads
- Extend Application model with source, compose_file, systemd_unit,
  managed_by, version, and upload_path columns.
- Add migration 017_manual_apps for the new columns.
- Add POST /apps/manual to register existing server paths/systemd units.
- Add POST /apps/upload with versioning, rollback, and .env preservation.
- Update DockerService compose helpers to accept a custom compose_file.
- Update ProcessService to query systemd units and compose projects.
- Update apps API start/stop/restart/status/delete to honor compose_file
  and avoid deleting unmanaged manual paths.
- Add Manual/Local and Upload ZIP source modes to NewService.
- Show source badges in Services list and disable Git controls for
  manual/upload apps in ServiceDetail.
- Add version list, rollback, and new-version upload UI for upload apps.
2026-06-18 22:16:00 -04:00
Juan DenisandClaude Opus 4.8 ee7d492dd2 feat(remote-access): per-edge port allocation + blocked-UDP diagnostic (Phase 4)
Phase 4 of docs/REMOTE_ACCESS_ROADMAP.md (the buildable/verifiable parts):

- #20 multiple private peers per edge: allocate a per-edge WireGuard listen
  port (tunnel_netutil.pick_listen_port) instead of a hardcoded 51820, so one
  edge can front many private peers without UDP-port conflicts. (Multiple
  services per tunnel already worked since Phase 2.)
- #21 reachability diagnostic: tunnel_netutil.diagnose_reachability classifies
  an up-but-never-handshaking tunnel as likely-blocked outbound UDP; surfaced
  in the status-refresh response (diagnostic field) and as a hint on the Remote
  Access page. The relay data plane itself is deferred.

#22 (client-VPN) and #23 (identity-aware SSO) remain XL milestones, not built.

Verified: 14 backend tests (incl. port-allocator + diagnostic), eslint + build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 23:28:12 -04:00
Juan DenisandClaude Opus 4.8 7cf8d4e18a feat(remote-access): NAT-traversing WireGuard tunnels to expose private services
Self-hosted tunneled reverse proxy: a service on a NAT'd host (e.g. a home
Jellyfin box) gets a public HTTPS hostname with no port-forwarding, by pairing
two agents over WireGuard. Implements Phases 0-3 of docs/REMOTE_ACCESS_ROADMAP.md.

Agent (Go):
- new internal/wireguard package: Manager interface + kernel backend (wg/ip)
  and a userspace netstack backend (wireguard-go + gVisor) so Windows/macOS
  peers need no driver/admin; in-process keygen returns only the public key
  (private keys never leave the host)
- wireguard:keygen/interface/peer/status/forward/unforward + firewall
  :allow_port/:deny_port commands; wireguard + firewall capability probes
- TCP forwarder (tnet.ListenTCP / net.Listen -> dial localhost) carries
  traffic to the real service behind a netstack peer

Backend (Python):
- Tunnel + ExposedService models; tunnel_netutil (subnet allocator, health);
  tunnel_broker_service (keygen->up->peer->verify, firewall open, reconcile-on-
  reconnect); tunnel_publish_service (forward -> nginx -> DNS -> auth -> cert)
- nginx remote-upstream vhost (WebSocket + proxy_buffering off)
- /api/v1/tunnels CRUD + /<id>/services publish/unpublish
- panel-authoritative reconcile hooked into the agent gateway on reconnect

Frontend (React):
- Remote Access page (tunnels + exposed services, status, teardown) and an
  "Expose a Local Service" wizard; new services/api/tunnels.js

Verified: go build (win/linux) + vet + go tests (forwarder + no-leak parsers);
py_compile + pytest (netutil, reconcile, nginx template); create_app boot;
eslint + npm run build. Live multi-host run pending.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 23:20:00 -04:00
Juan DenisandClaude Opus 4.8 733f866f1c feat(wordpress): per-site PHP-fatals analytics from the WP_DEBUG log (#25)
Finish #25's deferred PHP-fatals signal, sourced from the WP_DEBUG log
(/tmp/wp-debug.log) that #30's Security toggle writes — not the access log
(which can't carry it):

- get_php_errors: one bounded docker exec (marker-guarded tail -c, 15s single-
  worker timeout) distinguishes logging-off / no-log from on-but-clean, then
  parses [time] PHP <Level>: <msg> into severity counts (fatal/warning/notice/
  deprecated; Parse + Recoverable-fatal fold into fatal) + the 25 most-recent
  entries (messages capped, stack-trace lines skipped).
- Merged into GET /sites/<id>/analytics as php_errors; a PHP errors panel added
  to the Analytics tab with an 'enable WP_DEBUG on the Security tab' empty state.
- Supplies the per-site error signal #27's error-spike rule wanted.

7 new tests (classification, parse/skip-traces, off/empty/unavailable, capping);
full backend suite 218 passed / 12 skipped; frontend lint-clean + production build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:06:58 -04:00
Juan DenisandClaude Opus 4.8 a6a214c245 feat(wordpress): wp-content/full-site zip import with zip-slip-safe extraction (#17)
Finish #17's deferred files half on top of the .sql MVP. After the DB import +
search-replace, an optional wp-content/full-site .zip is extracted and copied
into the container:

- _safe_extract_zip rejects any member escaping the temp dir BEFORE writing
  (zip-slip: ../ traversal, deep traversal, absolute paths) + bad-zip guard.
- _resolve_wp_content_dir handles the 3 layouts: zip OF wp-content, zip WITH
  wp-content/ at root, and full-site zip one wrapper folder deep.
- _import_wp_content_zip docker cp's the resolved tree into
  /var/www/html/wp-content and chowns it to www-data. Non-fatal: DB import still
  succeeds; response carries a warning + wp_content_imported flag.
- Route accepts an optional wp_content multipart file (.zip, own temp, cleaned
  up); client importSite({wpContentFile}) + a file input in the import modal.

11 new tests (zip-slip rejection, layouts, docker cp/chown flow mocked); full
backend suite 211 passed / 12 skipped; frontend lint-clean + production build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 21:59:59 -04:00
Juan DenisandClaude Opus 4.8 aaf99212cb feat(wordpress): durable per-site PHP limit writes + wire the PHP tab (#24)
Finish #24's deferred limit-WRITE side, Docker-correct for wordpress:*-apache
(Apache + mod_php):

- set_php_limits: whitelist + re.fullmatch-validate each directive (fullmatch so
  a newline-smuggled extra ini line can't pass the $ anchor), write a host-side
  php-conf/zz-serverkit.ini and bind-mount that single file into conf.d so it
  survives container recreate. File written before the mount is added; new mount
  applies via compose up, content-only change via restart so php.ini reloads.
  Partial updates merge; mount injected once.
- Route POST /sites/<id>/php/limits (admin), client setPhpLimits, and an Edit
  PHP Limits panel in the PHP tab (inputs seeded from live values, partial save).
- Fix latent bug: PhpTab (the #24 read panel) was built but never wired into
  WordPressDetail (no tab button / render branch) — the whole PHP panel was
  invisible. Added 'php' to VALID_TABS + a tab + render branch.

7 new tests; full backend suite 200 passed / 12 skipped; frontend lint-clean +
production build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 21:43:12 -04:00
Juan DenisandClaude Opus 4.8 fd2313077f Polish connections: domain-expiry alerts, S3 image preview, encryption-health banner
- Domain-expiry notifications: a daily scheduler (RegistrarService.notify_expiring)
  fires through the notification channels when a registrar domain crosses 30/14/7/1
  days or expires, de-duped via a small state file so each crossing alerts once.
- S3 image preview: ImageThumb is now S3-aware (loads objects via short-lived
  presigned URLs); threaded through FileCard / PreviewDrawer / FileManager.
- Encryption-health banner: the Connections hub consumes GET /api/v1/connections
  and warns if any credential is not encrypted at rest (hidden in the happy path)
  — the registry's first real consumer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:43:00 -04:00
Juan DenisandClaude Opus 4.8 bbe6bee267 Unify credential encryption + add a read-only connection registry (#18)
- Encrypt the last plaintext credential store: CloudProvider.api_key_encrypted is
  now Fernet-encrypted on write and decrypted in _auth_headers, with an idempotent
  encrypt_legacy_secrets() migration run at startup. All five stores (source, DNS,
  cloud, registrar, storage) now encrypt identically via app.utils.crypto.
- Add ConnectionRegistry + GET /api/v1/connections: one normalized, secret-free
  list of every connected external account (the "what's connected" source of
  truth). Frontend api.getAllConnections().
- The full write-side "one model replaces five" migration is deliberately not done
  — it would risk five working subsystems for no user-facing benefit. The uniform
  encryption + read registry deliver #18's practical value.

Verified by booting create_app('testing') and running the registry live (returns
a list; /api/v1/connections present in the URL map).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 10:47:59 -04:00
Juan DenisandClaude Opus 4.8 d157fc514d Encrypt provider secrets at rest + add Namecheap registrar (#16, #17)
Encryption-at-rest (#16/#17) for the two provider stores still in plaintext:
- crypto.py: add decrypt_secret_safe (decrypt with plaintext fallback) + is_encrypted.
- DNS providers: encrypt api_key/api_secret on write; decrypt at point-of-use via
  _api_key/_api_secret in the Cloudflare/Route53/DigitalOcean/GoDaddy builders.
- Storage: encrypt secrets in save_config; decrypt in get_config.
- Idempotent encrypt_legacy_secrets() migrations run on startup, encrypting any
  pre-existing plaintext in place. Legacy values keep working via the decrypt
  fallback, so there is no flag day.

Namecheap registrar:
- RegistrarConnection.config_json column for non-secret extras (username, client_ip).
- RegistrarService: Namecheap XML domains.getList (portfolio + expiry), test, and
  MM/DD/YYYY -> ISO date parsing.
- Frontend: namecheap tile is live; the registrar form is provider-aware (GoDaddy
  key+secret vs Namecheap key + username + whitelisted IP).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 10:33:09 -04:00
Juan DenisandClaude Opus 4.8 845df9959e Add S3 bucket browsing to the Files app (#14, #15)
- backend: StorageProviderService.s3_* (browse/read/write/upload/delete/
  presigned-get) + /api/v1/files/s3/* routes, reusing the configured backup
  bucket — no new credentials.
- frontend: an "S3 bucket" target in the File Manager. UI paths stay slash-rooted
  and map to S3 keys server-side, so breadcrumbs/history work unchanged; virtual
  folders from prefixes; presigned-URL download. Folders/rename/permissions are
  disabled and the local-only sidebar is hidden on S3. TargetPicker gained a
  backward-compatible extraOptions prop.
- docs: mark #14/#15 done (Phase 4).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 10:21:39 -04:00
Juan DenisandClaude Opus 4.8 7b6086934d Finalize Connections hub: complete #8 SMTP relay docs + lint cleanup
Follow-up to the connections work already committed in 2e1bdbc + c1d4653
(swept in by a concurrent commit). Commits the two remaining edits:

- docs/CONNECTIONS_ROADMAP.md: mark #8 (SMTP relay) done — a real Postfix
  smarthost (relayhost + SASL) with an OS-agnostic smtplib connection test.
- ConnectProviderModal: drop the unused `provider` arg from EmailBody (lint).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 09:23:06 -04:00
Juan Denis c1d4653f6c Add registrars, GitLab, DO/Godaddy DNS and SMTP relay
Introduce a broad Connections expansion: add RegistrarConnection model, RegistrarService and /api/v1/registrars endpoints to surface domain portfolios and expiry. Add EmailRelayService and API routes plus Postfix relay wiring to configure/test/disable outbound SMTP. Extend DNSProviderService with DigitalOcean and GoDaddy implementations and helpers. Add GitLab support to source connection service and routes. Frontend: expand Connections hub, provider catalog, modal bodies, provider icons, RegistrarPortfolio component, API client methods and styles. Add docs/CONNECTIONS_ROADMAP.md describing roadmap and intent.
2026-06-15 09:19:56 -04:00
Juan Denis 2e1bdbcb3d Add Connections hub and provider UI
Introduce a new Connections hub for Settings: adds presentational components and catalog for external providers (ProviderBrands, ProviderCard, ConnectionsHub, ConnectProviderModal, providerCatalog) plus SCSS (_connections.scss) and brand icons. Replaces the old GitHub-only SourceConnectionsTab (removed) and wires ConnectionsHub into Settings. Also imports the new styles in main.scss and tweaks settings styles. Provides UI to list/manage GitHub/DNS providers, show scopes, and handle connect/manage flows; keeps several providers marked comingSoon for future work.
2026-06-15 02:22:29 -04:00
Juan Denis 5212704942 Add Git provider UI, refactor headers, and WP fixes
Introduce GitProviders (provider detection, badges, and RepoProviderStrip) and a shared _git-connect.scss; update Git connect modal and WordPress Git form to use the new UI and provider detection. Replace many page header blocks with PageTopbar and wrap loading states in page-container for consistent layout. Backend: add _is_wp_app helper and use it in wordpress API endpoints; ensure wp-cli login plugin is installed and activated. Minor style/token cleanups and .gitignore addition for docs/UI_CONSISTENCY_AUDIT.md.
2026-06-15 01:53:30 -04:00
Juan DenisandClaude Opus 4.8 0a74e5670d feat(sites): wildcard HTTPS for managed subdomains
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>
2026-06-15 00:52:30 -04:00
Juan DenisandClaude Opus 4.8 ad5c9b6bf0 feat(wordpress): attach a custom domain with auto-DNS
Turns "point myblog.com at this site" into one action that consumes the URL-swap
tool. New DNSProviderService.find_zone_for_domain (longest zone-suffix match
across connected providers) + ensure_a_record upsert the A record via Cloudflare/
Route53, or return the exact record to add when nothing manages the zone.

WordPressService.attach_custom_domain orchestrates it: auto-create DNS (server IP
from SiteDomainService), optionally obtain a Let's Encrypt cert (the host vhost is
written first so certbot --nginx can validate), then migrate the site URL to the
domain via change_site_url. External steps degrade to warnings; only a requested
migration that errors aborts.

POST /wordpress/sites/<id>/domain (admin). Frontend: "Add Domain" button +
AttachDomainModal — enter a domain, optional HTTPS, and the result shows the
auto-created record or the exact A record to add manually.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 00:30:50 -04:00
Juan DenisandClaude Opus 4.8 57a7b7b3e6 feat(wordpress): URL-swap tool — safe, previewable site URL changes
Changing a WordPress site's URL is not a single UPDATE: the URL is embedded
across wp_options, post content, and PHP-serialized blobs whose byte-length
prefixes break under a naive REPLACE. This adds a first-class swap tool built on
WP-CLI search-replace (serialization-safe), with the rails to make it routine:

- preview_url_change: dry-run that reports per-pair replacement counts, no writes
- change_site_url: backs the database up first, rewrites with --skip-columns=guid
  (full-URL + host-only pairs so scheme/protocol-relative refs are covered),
  updates home/siteurl, flushes the object cache + permalinks, then re-points the
  primary Domain row and nginx vhost. Rolls the DB back from the backup on
  failure. keep_old_redirect keeps the old host resolving so WordPress 301s it.
- POST /sites/<id>/url/preview and /sites/<id>/url (admin-gated)
- "Change URL" modal on the site detail page: live dry-run preview, backup
  notice, keep-old-address toggle, then apply.

_write_app_vhost is factored out of _provision_routing and shared by the
re-point path. The pure-SQL search-replace in db_sync_service remains the
offline-dump fallback only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 00:19:16 -04:00
Juan DenisandClaude Opus 4.8 a6f66b2976 fix(databases): distinguish a connection failure from an empty database
docker_mysql_get_tables returned [] on both a genuinely empty database and a
container/auth failure, so the explorer rendered "Empty" either way and silently
hid broken connections. It now returns {connected, tables, error}; the API
passes that through, and the source tree shows "No tables yet" for a connected
but empty database vs the real reason when the container exec/auth fails.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 00:01:41 -04:00
Juan DenisandClaude Opus 4.8 0abed0efa1 redesign(dashboard): admin-gated recent-activity feed
Wire the previously-unused Feed/FeedItem ds primitives to the existing /admin/activity/feed endpoint as a Recent Activity panel beside Applications (span 8 + span 4). Admin-gated via AuthContext isAdmin so non-admins neither see it nor trigger the 403; actions map to tone+icon, with relative timestamps and a link to the audit log.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 03:18:30 -04:00
Juan DenisandClaude Opus 4.8 e1a2a68359 redesign(security): fail2ban per-jail KPI cards
Fetch each active jail's status (getFail2banJailStatus) and render a per-jail card grid (Banned / Failed / Total banned) above the banned-IPs table, matching the prototype. Per-jail counts already existed behind the /fail2ban/jails/<jail> endpoint — the tab just never called it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 03:12:10 -04:00
Juan DenisandClaude Opus 4.8 a3f4676ad9 redesign(wordpress): functional plugin/theme card grids
Replace the dense plugin/theme tables with the prototype's card grids. Plugins: ServiceTile avatar + name/slug + version + update-flag, and a working activate/deactivate Switch. Themes: preview placeholder + Active pill + Activate. Wires the already-existing backend WP-CLI endpoints via new activatePlugin/deactivatePlugin/activateTheme api methods (these were never backend-blocked — only the frontend api methods were missing).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 03:12:10 -04:00
Juan DenisandClaude Opus 4.8 80bdd21375 redesign(files): owner column + flush inline preview pane
Add an Owner column to the list view (header + FileRow cell + grid). Convert the file preview from a fixed overlay-drawer into a flush inline 3rd pane within the file-manager body via an opt-in 'inline' prop on PreviewDrawer (the shared .preview-drawer overlay used by Terminal/Docker is untouched). All preview features, tree, uploads, bulk and context-menu preserved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 02:57:47 -04:00
Juan DenisandClaude Opus 4.8 b08c7970e3 redesign(security): overview posture gauge + dense integrity table
OverviewTab gains a ScoreGauge posture ring + hardening checklist computed client-side from real signals (ClamAV state, file-integrity baseline, recent integrity changes, notifications) — no invented backend number. IntegrityTab's plain path lists become a dense sk-dtable with change-kind state chips (modified/deleted/new/permission). Fail2ban per-jail cards left out (payload has no per-jail counts).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 02:57:47 -04:00
Juan DenisandClaude Opus 4.8 fb0fd1cf44 redesign(domains): inline DNS records table in the drawer
The domain drawer punted DNS to a /dns link; now it loads the domain's zone records and renders them inline as a dense sk-dtable (colored .dns-rtype chip + mono name/value + TTL), reusing the DNSZones records API. Handles loading/empty/no-zone/error; the Manage DNS records link is kept for full editing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 02:57:47 -04:00
Juan DenisandClaude Opus 4.8 1feb6ef336 redesign(dashboard): server-switcher dropdown on title
Replace the static hostname h1 + separate select with a .srv-switch button that opens a popover server list (status dot + name + provider/IP meta + active check), matching the prototype env-menu. Reuses the existing server-change handler verbatim; shown even for a single server. (Recent-activity feed stays backend-blocked.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 02:57:47 -04:00
Juan DenisandClaude Opus 4.8 3eeb0611b9 redesign(wordpress): list subtitle, filter counts, search, gradient tiles
Add a count subtitle, per-filter counts on the status SegControl, a client-side site search, and per-site ServiceTile gradient avatars (replacing the flat static Globe). Bulk-select, tag filter, status pills and links are preserved; enriched columns (plugins/visits/uptime) stay backend-blocked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 02:49:36 -04:00
Juan DenisandClaude Opus 4.8 fd341b4d63 redesign(services): restyle new-service method picker as a grid
Convert the vertical 3-card mode strip into the prototype's .method-grid (icon tile + title + sub, accent border/wash on the selected card, hover lift). All three create flows and form logic are unchanged; Docker-image/from-scratch stay backend-blocked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 02:49:36 -04:00
Juan DenisandClaude Opus 4.8 83cab5af28 redesign(git): add repository-connect hint banner
Add the prototype's .git-hint accent-gradient explainer banner atop the Git page (icon tile + headline + sub). The providers OAuth grid remains backend-blocked and is intentionally not added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 02:49:36 -04:00
Juan DenisandClaude Opus 4.8 e3a9208172 redesign(marketplace): add horizontal category chip row
Add a quick-scan .cat-chips row above the browse grid (All + per-category), wired to the existing category filter. The right sidebar list and select are preserved (additive).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 02:49:36 -04:00
Juan DenisandClaude Opus 4.8 69cb44dbd5 redesign(email): show status dots on DNS presence pills
DKIM/SPF/DMARC presence pills passed dot={false}, rendering as flat chips; the prototype dtable uses dot+label status pills. Drop dot={false} so the colored status dot shows. A 3rd pending/verifying state remains backend-blocked (API reports boolean presence only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 02:43:43 -04:00
Juan DenisandClaude Opus 4.8 67ae2ae8ab redesign: settings nav accent-wash + rail width parity
Settings nav active item used a heavy solid accent fill; switch to the subtle accent-bg wash + accent-bright text/icon used by .sk-topbar__tab (theme-safe in light and dark). Tighten the nav column 240->206px and widen the workflow palette rail 212->248px to match the prototype.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 02:43:43 -04:00