Commit Graph
784 Commits
Author SHA1 Message Date
github-actions[bot] a053df2b34 chore: bump version to 1.6.15 [skip ci] 2026-06-16 03:57:08 +00:00
Juan DenisandClaude Opus 4.8 2483541356 fix(storage): create the config file's own dir in save_config
save_config() ran os.makedirs(SERVERKIT_CONFIG_DIR) unconditionally rather
than the directory of the file it actually writes (cls.CONFIG_FILE). When a
caller overrides CONFIG_FILE (test_storage_secret_encrypted_at_rest points it
at a pytest tmp dir), creating the real config dir failed on the sandboxed CI
runner and — because the surrounding except swallows it and returns
{success: False} — the file was never written, surfacing as a confusing
FileNotFoundError. Now makedirs(dirname(CONFIG_FILE)); production behavior is
unchanged (there dirname(CONFIG_FILE) == SERVERKIT_CONFIG_DIR). 8/8 encryption
tests pass; full backend suite was 241 passed / 1 failed -> now green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 23:55:03 -04:00
github-actions[bot] ad7e38c203 chore: bump version to 1.6.14 [skip ci] 2026-06-16 03:49:33 +00:00
Juan Denis bedbd06bff Merge branch 'dev' of https://github.com/jhd3197/ServerKit into dev 2026-06-15 23:49:15 -04:00
Juan DenisandClaude Opus 4.8 3a8fafcba4 chore(deps): bump PyJWT 2.13.0 + cryptography 48.0.1 (Safety advisories)
The Safety dependency scan flagged PyJWT==2.12.1 (5 vulns) and
cryptography==46.0.7 (1 vuln, bundled-OpenSSL). Bump to the Safety-suggested
fixes, which resolve all 6 currently-active (non-policy-ignored) findings.
Verified: installs with no resolver conflicts, the Fernet/encryption tests
pass, the app boots (JWT manager), and the full suite is 232 passed / 12
skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 23:47:15 -04:00
Juan DenisandClaude Opus 4.8 66ec2a79b3 ci(backend): install pytest so the test job can run
The Backend CI installed only requirements.txt (which doesn't carry the
test-only pytest dependency) and then ran `python -m pytest`, failing with
"No module named pytest". Add `pip install pytest` to the install step.
pytest is the only test-only import across tests/ (everything else they
import — cryptography, flask_jwt_extended, prompture, yaml — is already a
runtime dependency). Full suite: 232 passed, 12 skipped locally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 23:47:15 -04:00
github-actions[bot] 3cf19059f2 chore: bump version to 1.6.13 [skip ci] 2026-06-16 03:39:35 +00:00
Juan Denis a9c22c27c1 Merge branch 'dev' of https://github.com/jhd3197/ServerKit into dev 2026-06-15 23:39:23 -04:00
Juan DenisandClaude Opus 4.8 b596925313 ci(agent): build the embedded UI before go (fix embed all:dist)
The agent embeds its desktop console via //go:embed all:dist
(internal/agentui/embed.go), which needs agent/internal/agentui/dist to
exist at compile time. The CI and release workflows ran go build/vet/test
without first building the React app (agent/ui -> vite build outputs to
that dist), so a fresh checkout failed with
"pattern all:dist: no matching files found".

Add a Node setup + `npm ci && npm run build` (in agent/ui) before the go
steps in both jobs of agent-ci.yml and in the release build job. Verified
the build command produces internal/agentui/dist locally.

Not a Go-version issue: wireguard-go@2026, gvisor@2025 and the agent all
declare go 1.23.1, and gvisor's gohacks build constraints are go1.20+
(no upper bound), so CI's Go 1.23 compiles them fine.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 23:39:21 -04:00
github-actions[bot] 267066dce0 chore: bump version to 1.6.12 [skip ci] 2026-06-16 03:31:05 +00:00
Juan Denis 49360806d2 Merge branch 'dev' of https://github.com/jhd3197/ServerKit into dev 2026-06-15 23:30:49 -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
github-actions[bot] 5885b425b5 chore: bump version to 1.6.11 [skip ci] 2026-06-16 02:36:46 +00: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 4a637c7181 docs(wordpress): split #34 into an executable per-site edge + scaling spec
#34 is infra-blocked and unverifiable on the Windows dev host, so spec it
rather than build it blind. A capability sweep found the load-balancing,
rate-limit, proxy-cache, per-vhost-log, wildcard-TLS, and S3-upload primitives
already exist (nginx_advanced_service / nginx_service / advanced_ssl_service /
storage_provider_service) — so #34 is wiring + a public base-domain + stateless
WP, not greenfield nginx.

Adds docs/HORIZONTAL_SCALING_SPEC.md: a shared keystone (K0-K4) that also
unblocks #21/#15-TLS/#8/#30-jail/#25, then #34 proper (H0-H4), each phase a
separately verifiable PR with a Windows-vs-real-host split. Links it from the
roadmap #34 entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 21:09:42 -04:00
Juan DenisandClaude Opus 4.8 fa73b217ef feat(workspace): scope domains/databases + reconcile workspace roles (#33)
Finish #33's two named workspace-scoping slices, backend-only (no schema
migration, no frontend changes):

- Domains: get_domains scopes through the parent Application via scope_query
  (own + granted, workspace-narrowed); also surfaces granted apps' domains,
  closing a gap vs the per-domain routes. Admin-no-context path unchanged.
- Databases: the app-linked /docker/databases enumeration scopes via
  scope_query; host-level MySQL/PG/SQLite stay global system resources
  (documented inline).
- Role reconciliation: WorkspaceService.effective_role + can_write_in_workspace
  intersect global User.role with per-workspace WorkspaceMember.role,
  narrow-only (a workspace role can only reduce capability; system admin never
  capped). Enforced at apps/servers create + reassign seams (viewer -> 403,
  only when a workspace context is active). Reconciled role surfaced on
  GET /workspaces (my_role/my_effective_role).

6 new tests; full backend suite 193 passed / 12 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 21:00:57 -04:00
Juan DenisandClaude Opus 4.8 60a676025a Fix wildcard HTTPS reading encrypted DNS creds raw + add encryption tests
Auditing the encryption-at-rest work surfaced one missed read site: sites_https_service
passed the DNS provider api_key/api_secret straight to the cert issuer, but those are
now Fernet-encrypted at rest — so DNS-01 wildcard issuance would have received ciphertext
and failed.

- Add DNSProviderService.decrypted_credentials() as the public, canonical way to read a
  DNS provider's usable creds (nothing reads .api_key raw anymore); use it in
  sites_https_service.
- Add tests/test_provider_secret_encryption.py — 8 proving tests: ciphertext-at-rest +
  plaintext-on-read for DNS / cloud / storage / registrar, the idempotent legacy-plaintext
  migration, and the wildcard path now receiving decrypted creds (the regression above).

pytest: 17/17 pass (8 new + 9 existing test_sites_https.py). Full suite: 187 passed; the
lone failure (test_agent_poll_e2e command roundtrip) is a pre-existing flake — the agent
registry in-memory singleton leaks across tests in that file (passes solo), unrelated here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:55:57 -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 1a0214f11d feat(wordpress): publish new sites at a real subdomain, not localhost:port
create_site installed WordPress at http://localhost:<port>, which WordPress
bakes in as its canonical home/siteurl — reachable on the box but useless as a
public site URL. New SiteDomainService resolves <slug>.<base_domain> (config
SITES_BASE_DOMAIN: lvh.me in dev, opt-in in prod, empty falls back to the old
localhost URL). create_site now installs at that host and provisions a primary
Domain row + nginx reverse-proxy vhost (_provision_routing, best-effort so a box
without nginx warns instead of failing). Teardown removes the vhost; clone_site
URL rewriting is generalized via _canonical_site_url so cloning stays correct
with real hostnames.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 00:01:51 -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
Juan DenisandClaude Opus 4.8 a8f77bc97b redesign(cron): migrate page header to PageTopbar
Cron Jobs still used the legacy .page-header (plain h1 + subtitle). Swap to the shared PageTopbar (icon + title + actions) used by the other migrated pages, matching the prototype's topbar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 02:43:43 -04:00
Juan DenisandClaude Opus 4.8 dfc628e13b redesign(fleet): render fleet tables with sk-dtable
Agent Fleet and Fleet Monitor used the generic .table style while every other migrated list page uses the dense .sk-dtable look (mono uppercase headers, token row borders). Swap the className so they match their siblings under the Servers tab group.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 02:43:43 -04:00
Juan DenisandClaude Fable 5 b2fad9528a docs: clarify agent.key + session-token notes; record audit remediation (L2/L4-L7)
- SECURITY.md: spell out that agent.key encryption is off-host/tamper
  protection, not confidentiality against local root; 0600 perms are the real
  control (L2).
- agent_registry.py: reword the per-message session-token "TODO" to an accepted
  limitation (defence-in-depth, not a bug) (L7).
- AGENT_AUDIT.md: record H1/H2/H3 and L1-L7 status, with L4 (cap OR-merge),
  L5 (single-worker), L6 (windows .bat) noted as accepted by-design/documented.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 18:35:52 -04:00
Juan DenisandClaude Fable 5 df67ce782b fix(ui): remove dead agent-version management buttons (audit L3)
The Agent Versions tab rendered an "Add Version" button and per-row Edit/Delete
buttons with no handlers — there are no edit/delete endpoints. Remove the dead
controls (and now-unused Plus/Trash2 imports); the version list is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 18:35:43 -04:00