324 Commits
Author SHA1 Message Date
Juan DenisandClaude Fable 5 751dde7a7d fix(deploy): threaded gunicorn worker — gevent-websocket class broke WebSocket
The app runs flask-socketio with async_mode='threading' (the backend
is real-thread based: jobs scheduler, queue bus, agent registry), where
engineio serves WebSocket via simple-websocket on the raw socket. The
systemd unit template and Dockerfile still launched gunicorn with the
GeventWebSocketWorker, which ALSO answers the upgrade — the client got
HTTP/1.1 101 twice and read the second one as a frame ('Invalid frame
header'), so every panel silently degraded to polling. Proven by
hexdump (double 101, first 'frame' byte 0x48 'H') and fixed live:
single 101 + 0x81 text frame, socket.io connects through Cloudflare.

- unit template + Dockerfile: gunicorn -w 1 --threads 100 (single
  PROCESS stays mandatory — in-memory agent gateway)
- requirements: pin simple-websocket explicitly (was transitive-only)
- CLAUDE.md / ARCHITECTURE.md / SECURITY.md single-worker notes updated

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 00:14:49 -04:00
Juan DenisandClaude Fable 5 0ec55accbe feat(extensions): settings.section slot + manifest shape lint (#52)
Extensions can now contribute Settings sections: a settings.section
PluginSlot renders below the active Settings tab with the tab id as
context. _validate_manifest grew shape checks for everything the
platform consumes declaratively — module:attr refs (entry_point,
socket_entry, models, lifecycle hooks, job handlers), jobs/schedules
shapes, and per-kind required contribution keys (nav/routes/tabs/
palette/widgets/layouts) — so a malformed block fails the install with
a message naming each problem instead of being silently dropped at
runtime. Unknown contribution kinds only warn (forward compat). The
scaffolder gained --validate <path> mirroring the same rules for
authors, wired into the conversion recipe in docs/EXTENSIONS.md.

3 new lint tests incl. one asserting every shipped builtin manifest
passes; full backend suite 1227 green; lint 0 errors; build clean;
--validate smoke-tested on a good and a bad manifest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 02:36:46 -04:00
Juan DenisandClaude Fable 5 4ca07fb71c feat(extensions): per-plugin config store + Configure form (#49)
Completes the deferred half of #30. InstalledPlugin gains config_json
(migration 045); admin-gated GET/PUT /api/v1/plugins/<id>/config
persists values; plugins read them via the new plugins_sdk.config(slug)
accessor. The Marketplace Installed tab shows a Configure button for
plugins declaring a config_schema and renders a real form
(string/number/integer/boolean/enum/secret fields, defaults, hints).
Values may hold secrets so they are excluded from to_dict() and only
served by the admin endpoint. Documented in docs/EXTENSIONS.md.

2 new tests (config round-trip + SDK accessor + secret non-leak;
non-object rejection); pipeline suite 18 green; lint 0 errors; build
clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 02:09:39 -04:00
Juan DenisandClaude Fable 5 50166b8f78 feat(extensions): tab-group contributions (#43) + FTP as serverkit-ftp (#44)
Extensions can now contribute a real tab to a core-owned tab group:
a new `tabs` contribution kind ({group, to, label, icon?, end?, order?},
group id == sidebar item id) is merged into the strip by TabGroupLayout
(new groupId prop; core wins `to` collisions), and route contributions
gained a `group` field so ExtensionRoutes collects them per group and
App.jsx mounts them INSIDE that group''s TabGroupLayout — the shared
PageTopbar chrome stays and the Sidebar keeps the group item lit via
contributed matchPrefixes. Wired for files/servers/monitoring.

First consumer: the FTP Server page moves to the serverkit-ftp builtin
extension (tab in the Files group; backend /api/v1/ftp stays core per
D2). Added to CONVERTED_BUILTIN_SLUGS so upgraded panels auto-install
it once; fresh installs find it in the Marketplace.

Proven by the extended contributions-envelope test (26 plugin/platform
tests green; full backend suite 1215 passed), lint 0 errors, build clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 01:39:37 -04:00
Juan DenisandClaude Opus 4.8 39d21f7a46 feat(extensions): Phase 5 — WordPress UI contributed by serverkit-wordpress
Completes #38: the WordPress frontend now belongs to the extension too, not just
the backend. The tab-group constraint (contributed routes are flat — there's no
way to contribute a shared TabGroupLayout parent inside the dashboard) is solved
by contributing a SINGLE splat route `wordpress/*` whose component
(WordPressExtension) self-renders the whole WordPress sub-router — reproducing the
exact structure core App.jsx used to own:
  - TabGroupLayout + WORDPRESS_TABS over the 3 list pages (sites / plugin library /
    pipelines),
  - full-bleed site + pipeline detail routes as siblings,
  - the legacy /wordpress/projects → /wordpress/pipelines deprecation redirects.

The 5 page components stay in core pages/ and are re-used (Pattern A, same as
serverkit-git/email/workflows); only the routing + nav/palette/title ownership
moves into the manifest.

- builtin-extensions/serverkit-wordpress/frontend/index.jsx (source) → synced to
  frontend/src/plugins/serverkit-wordpress/ (pre-bundled, D5) via
  scripts/sync-builtin-frontends.mjs (drift check green).
- plugin.json contributions: nav (WordPress), routes ([{path:"wordpress/*",
  component:"WordPressExtension"}]), page_titles, command_palette.
- Core App.jsx: removed the WordPress imports, the TabGroupLayout WORDPRESS_TABS
  route block, the detail routes, LegacyWpPipelineRedirect, and the now-unused
  ModuleRoute + WORDPRESS_TABS imports (kept the dynamic /wordpress/ title
  fallbacks). Removed the sidebar nav item and the two command-palette entries.
  WordPress now disappears cleanly (nav+routes+API) when the extension is
  uninstalled.

Verified: npm run lint (0 errors) + npm run build clean; the screenshots skill
renders /wordpress/42 correctly (visually confirmed — full detail page, Overview
tab, sidebar item active); backend flagship still seeds/loads (has_frontend now
true) with tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 00:50:10 -04:00
Juan DenisandClaude Opus 4.8 a55e50bc16 feat(extensions): Phase 5 — extract WordPress into serverkit-wordpress (backend)
The largest single move in the plan (#38). The entire WordPress backend leaves
core for the bundled, default-installed serverkit-wordpress extension (D4).

Moved (12 files → builtin-extensions/serverkit-wordpress/backend/, intra-extension
imports made relative, core imports stay absolute):
  api/wordpress.py, api/wordpress_sites.py, api/environment_pipeline.py,
  wordpress_service.py (2676 lines), git_wordpress_service, wordpress_env_service,
  wordpress_plugin_library_service, wp_analytics/reports/security/update/vulnerability.

Wiring:
- Blueprints keep /api/v1/wordpress, /wordpress/projects and the /wordpress/pipelines
  alias (D9) via new manifest `extra_blueprints` support (3 blueprints / 4 mounts).
- Flagship (D4): seeded active on every panel (fresh + upgrade), uninstallable,
  never deletes tracked builtin/pre-bundled files; the `wordpress` module toggle is
  retired (MODULES now empty) — the plugin status guard is the gate.
- Loadability: the dashed package app.plugins.serverkit-wordpress is registered
  in-place via spec_from_file_location (no copy) — uniform across prod/dev/test.
- The 11 WP test files re-pointed through app.services.wordpress_bridge; the 6 lazy
  core callers were re-pointed in the prior commit.
- _regenerate_frontend_manifest() is a no-op under testing so the flagship boot
  seed can't clobber the checked-in plugins-manifest.json.

Deviations (documented, matching the Email precedent):
- WordPress *models* stay core — status_page/environment_activity/promotion_job hold
  FKs into wordpress_sites, so relocation would be a breaking data migration.
- Event catalog + Fail2ban WP filter stay core as two-speed items (extension calls
  them via core APIs); inverting adds risk without blocking the move.
- Frontend UI stays core this session (it lives in a TabGroupLayout — the tab-grouped
  conversion Phase 1 deliberately deferred); the extension is backend-only, so there
  is no route duplication. Frontend contribution is the scoped follow-up.

Verified: clean collection across all 1223 tests; 199 passed / 2 skipped / 0 failed
across the WP + plugin/extension + module + backup + core-caller parity set, incl.
the new tests/test_wordpress_extraction.py proving the move.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 00:15:10 -04:00
Juan DenisandClaude Opus 4.8 b59a57430e feat(extensions): Phase 4 — extract Email into serverkit-email extension
Moves the mail-server stack out of core into builtin-extensions/serverkit-email/:
- api/email.py + email_service/dovecot/dkim/spamassassin/roundcube → extension
  backend (relative intra-extension imports; core imports absolute); blueprint
  keeps /api/v1/email (D9). Core __init__ no longer registers email.
- Stays core (deviations from the audit, documented): postfix_service (the
  outbound relay actively uses it) + all email models (no external importers,
  so no DNSProviderConfig relocation / data migration needed).
- Frontend: serverkit-email manifest contributes nav/route/title/palette; core
  App.jsx/sidebar/palette entries removed; Email page re-exported (D2 pattern).
- #34 auto-install gated on real mail usage (EmailDomain/EmailAccount rows);
  the core Email module toggle is retired in favor of the extension.
- Tests: test_email_extraction.py proves core has no /api/v1/email, install
  registers+responds (exercises relative-import rewiring), uninstall clean.
  Full backend suite: 1200 passed. Frontend lint 0 errors + build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 22:39:18 -04:00
Juan DenisandClaude Opus 4.8 81d070b1d8 docs(extensions): Phases 4-5 boundary audits (Email + WordPress)
Grounded coupling maps + cut lists ("stays core" vs "moves") that gate the
actual extractions (#32/#38):
- EXTRACTION_EMAIL_AUDIT.md: mail-server stack cleanly extractable; only pre-req
  is relocating DNSProviderConfig + lazy postfix import; notifications SMTP is
  independent.
- EXTRACTION_WORDPRESS_AUDIT.md: extractable but 7 core hooks to invert first
  (eager import, backup-engine branch, Fail2ban WP filter, DatabaseSnapshot/SyncJob
  relocation, event catalog, lazy cross-imports). Ships bundled default-installed
  per D4; run after Email proves the machinery.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 22:13:51 -04:00
Juan DenisandClaude Opus 4.8 cdcfba5980 feat(extensions): Phase 3 — platform primitives (models, jobs, sockets, perms)
Backend:
- Plugin data models (manifest `models` → ext_<slug>_ tables; upgrade hook;
  purge on uninstall) via extension_lifecycle.py
- Declarative jobs/schedules wired into the Jobs SDK; pause on disable,
  resume on enable, removed on uninstall
- Socket.IO extension point (plugins_sdk.sockets → status-guarded /ext/<slug>)
- Permission capability gate (plugins_sdk.permissions.require_permission)
- Version compat gates enforced at every install source + panel_version() SDK;
  app/utils/version.py
- Keep-data vs purge on uninstall (lifecycle.uninstall(plugin, purge=))
- Plugin-asset serving route (delivery foundation, ADR 0001); manifest-spec
  documents all new fields

Frontend:
- Generic PluginSlot; mounted dashboard.top / service.detail.tab /
  domain.drawer.panel
- Uninstall keep/purge dialog; permission consent list + config_schema preview
  on the extension detail modal

Docs: ADR 0001 (frontend delivery), ADR 0002 (sandboxing note), EXTENSIONS.md
updates; scripts/new-extension.mjs scaffolder (#40).
Tests: test_extension_platform.py (11). Full backend suite: 1197 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 22:11:09 -04:00
Juan DenisandClaude Opus 4.8 6f5f2b1047 feat(extensions): Phase 2 backend — remote registry, checksum install, updates
- RegistryService: configurable SERVERKIT_REGISTRY_URL, TTL cache, offline
  fallback (last-good → bundled app/data/registry_index.json); read-only, never
  auto-installs. GET /api/v1/marketplace/registry + registry install endpoint.
- Checksum-verified install: install_from_url(expected_sha256) hard-fails on
  mismatch; force= path enables in-place updates.
- Update flow v1: check_for_updates() + GET /plugins/updates + POST
  /plugins/<id>/update; respects min/max_panel_version via new
  app/utils/version.py (get_panel_version + semver compare).
- serverkit-gui listed as the first registry entry (bundled index).
- Docs: EXTENSIONS_REGISTRY.md (index format + publisher/review guide).
- Tests: test_registry.py (fetch/cache, checksum, compat gate, update flow).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 21:41:50 -04:00
Juan DenisandClaude Opus 4.8 74c5e0a885 feat(extensions): Phase 0 hygiene — author guide, sync gate, truth pass, tests
- docs/EXTENSIONS.md: honest author guide (manifest, contributions, SDK,
  lifecycle, install sources, production frontend constraint, conversion recipe)
- Retire the serverkit-git compat shim in contributions.js (manifest is canonical)
- scripts/sync-builtin-frontends.mjs + extensions-ci.yml: builtin-frontend
  drift gate (single source of truth per D5)
- Marketplace UI truth pass: relabel bundled entries "Built-in"
- backend/tests/test_plugins_pipeline.py: first coverage for the install
  pipeline (builtin install, contributions envelope, disable 503, reinstall
  refresh, zip-slip)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 21:13:04 -04:00
Juan DenisandClaude Opus 4.8 45ba40a7ad feat(wordpress): surface the live site URL on create (closes roadmap #21)
After creating a WordPress site the post-create banner showed only the admin
credentials, so users had no signal that the site was already reachable at
<slug>.<base> — the exact "preview URL before DNS" gap roadmap #21 tracked,
now unblocked by the base-domain + Phase-5 wildcard-cert stack.

The banner now surfaces the live link on every successful create: a clickable
<slug>.<base> (noting HTTPS when the wildcard cert is set up), or, when the
site only came up on localhost, a nudge to set a managed-sites base domain in
Settings to publish it publicly. Shown even when the automated install did not
finalize (no captured credentials).

Mark #21 done in the roadmap: the wildcard base subdomain is the working,
TLS-valid preview a user relies on while pointing a custom domain, so the
separate *.preview.<panel-domain> scheme is unnecessary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 23:35:25 -04:00
Juan Denis 96d3094c52 Add 'Faro' related-projects note to README
Add a 'Related Projects' section linking Faro (desktop SFTP/SSH/S3 client) to the main README and translated docs (Spanish, Portuguese, Chinese). Explains Faro's features and relationship to ServerKit, and adds a link to the latest releases. This provides users a recommended desktop companion for file transfers and agent bridge functionality.
2026-06-30 23:03:47 -04:00
Juan DenisandClaude Opus 4.8 ca66629780 feat(deploy): serve panel frontend from host nginx, retire the container
The panel is now fully off Docker: the React SPA is served as static
files directly by host nginx from frontend/dist, eliminating the
serverkit-frontend container and the double nginx hop (host nginx ->
container nginx -> dist). Docker is now used ONLY for managed app
workloads.

- nginx sites (serverkit.conf + serverkit-insecure.conf): `location /`
  now roots at /opt/serverkit/frontend/dist with a try_files SPA
  fallback and a long-cache /assets/ block, instead of proxying
  127.0.0.1:3847. Root tracks the /opt/serverkit symlink so blue/green
  updates swap assets atomically.
- install.sh: launch_services no longer builds/starts a frontend
  container; adds apply_frontend_root (rewrites the dist root for a
  custom SERVERKIT_DIR) and selinux_label_frontend_dist (httpd_sys_-
  content_t so SELinux hosts can read the bundle from /opt).
- update.sh: refresh_config carries the same root rewrite across
  upgrades; selinux_label_dist relabels the switched-in slot; dropped
  the `docker compose up -d --force-recreate frontend` from both the
  forward and rollback paths.
- docker-compose.yml: frontend service moved behind a `legacy-frontend`
  profile (opt-in escape hatch); default `up -d` starts nothing.
- docs/DEPLOYMENT.md: documents the off-Docker panel model.

Tests: T14 (refresh_config rewrites dist root for custom install dir),
T15 (shipped nginx configs serve static, never proxy :3847). 16 passed.

Note: live nginx -t / SELinux file-serving not validated on a RHEL box
from this dev host; logic is best-effort guarded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 02:24:22 -04:00
Juan DenisandClaude Opus 4.8 31330a09be fix(deploy): make docker-compose.yml frontend-only (backend runs on host)
ServerKit's backend manages the host it runs on -- it creates app dirs under
/var/serverkit/apps, drives the host Docker daemon (one container per app),
and reloads nginx. The default compose containerized the backend, so it ran
as a non-root user into a root-owned /var/serverkit/apps with no host
docker.sock: WordPress/app creation failed with
`Permission denied: /var/serverkit/apps/...`. It also contradicted install.sh,
which runs the backend on the host via systemd and expects compose to bring
up only the frontend.

- docker-compose.yml: frontend-only; the frontend reaches the host backend via
  extra_hosts ["backend:host-gateway"].
- backend/Dockerfile: chown /var/serverkit to appuser (latent EACCES on the
  apps dir for dev/demo container use) + header noting it cannot manage host.
- frontend/nginx.conf: document the backend:host-gateway upstream.
- docs/DEPLOYMENT.md: add a Deployment model note; fix build/start + data paths.
- scripts/test/test_install.sh: T12 guards against re-adding a backend service.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 16:20:32 -04:00
Juan DenisandClaude Opus 4.8 c4f3b4fdac feat: Phase 4 polish & fleet — onboarding completion, fleet proxy, shared-resource depth, docs
- Server onboarding completion (C8): a pure pre-flight compatibility check
  (arch/distro/kernel/cpu/memory) gating validate; real-but-guarded
  distro-aware install_prerequisites/install_docker/pair_agent steps that
  no-op safely on dev/Windows and never raise; audit + best-effort
  notifications on each transition; and a live auto-scrolling log trail +
  elapsed timer in the onboarding wizard.

- Fleet-wide proxy dashboard (C6): ProxyStackService.fleet_overview aggregates
  every server's proxy posture (configured stack vs host Nginx default) behind
  GET /api/v1/servers/proxy/overview, surfaced as a new Fleet Proxy page in the
  Servers tab group with summary tiles + click-through to each server's proxy.

- Polymorphic shared resources depth (C9): hierarchical variable resolution
  (workspace < project < environment < direct attachment < resource) with
  source provenance and {{group.KEY}} interpolation, an audit trail on every
  tag/group/variable/attachment mutation, GET /api/v1/shared/resolved/
  hierarchical, and the reusable Tags/Environment-Variables panels embedded on
  the application and server detail pages.

- Docs & coverage: docs/ENHANCEMENTS.md (brand-neutral guide to all ten
  capabilities + verified endpoints), README/CHANGELOG updates, and a
  cross-cutting integration test asserting the new endpoints are wired.

Full backend suite: 924 passed, 14 skipped; frontend builds clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:04:04 -04:00
Juan DenisandClaude Opus 4.8 f47d0e1428 feat: Phase 1 foundation — status aggregator, API scopes, onboarding, template magic vars
Four foundational enhancements toward the deploy-code developer experience:

- Container status aggregator (C7): new ContainerStatusService applies a
  deterministic priority hierarchy (degraded > restarting > running:unhealthy >
  starting > running:healthy > exited > unknown) over an app's containers,
  cached with a short TTL. Exposed at /api/v1/status and pushed over a new
  container_status Socket.IO channel; Applications page now renders an
  aggregated ContainerStatusPill with sub-status tooltip.

- API token scopes + sensitive data masking (C10): require_scope() decorator
  enforces fine-grained scopes for API-key requests (pass-through for JWT),
  a canonical SCOPES catalog at /api/v1/api-keys/scopes, and a reusable
  sensitive_data_filter (mask_sensitive) honoring secrets:read. Frontend gains
  a grouped ApiKeyScopesModal picker and per-key scope pills.

- Server onboarding state machine (C8): new onboarding lifecycle
  (pending -> validating -> installing_prerequisites -> installing_docker ->
  pairing_agent -> ready/failed) with ServerOnboardingService, a
  server_onboarding_logs table, servers.onboarding_* columns, REST endpoints,
  and an OnboardingWizard surfaced on the server detail overview.

- Declarative template catalog (C5): documented catalog schema and
  magic-variable resolution (SERVICE_PASSWORD_/USER_/FQDN_/URL_/BASE64_*)
  layered onto the existing YAML template loader without changing behavior for
  templates that don't use them; GET /api/v1/templates/catalog/schema.

Migration 029 adds the onboarding table + columns. 60 new tests pass; frontend
builds clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 18:23:47 -04:00
Juan Denis 76ded002da Add per-site Fail2ban jail and UI for WP brute-force protection
Introduce a ServerKit-managed per-site Fail2ban layer to protect WordPress login endpoints. Adds Fail2banJailService (write-side) to generate filter and per-site jail configs, enable/disable jails, reload fail2ban, and unban IPs (Linux-only, best-effort, ownership-guarded). Wire it into WordPress service lifecycle (enable on site create, remove on delete) and WpSecurityService wrapper methods. Expose API endpoints for status, toggle and unban in backend API. Add unit tests for Fail2banJailService (config rendering, naming/safety, lifecycle, graceful degradation). Frontend: Security tab loads brute-force status, can toggle protection, shows ban counts and unban action; API client and styles updated. NginxService gains canonical per-site log/error path helpers. Docs updated to reflect shipped brute-force jail feature.
2026-06-23 17:48:59 -04:00
Juan Denis 3f37994f18 feat(installer): atomic blue/green updates, pre-flight checks, offline support, pre-built releases
- Rewrite scripts/update.sh with pre-flight validator (disk, memory, Python,
  deps, network, health) and automatic rollback on failure.
- Implement atomic blue/green install layout (/opt/serverkit-a|b + symlink).
- Add explicit database migration step (flask db upgrade) before switching slots.
- Add --dry-run, --force, --branch, --release flags to update.sh.
- Add SERVERKIT_OFFLINE_TARBALL and SERVERKIT_MIRROR_URL support.
- Verify release tarball SHA-256 checksums on download.
- Ship pre-built frontend/dist and relocatable venv in release tarballs.
- Make serverkit update delegate fully to scripts/update.sh; remove inline
  _update_source fallback.
- Harden install.sh for blue/green, checksums, offline, and pre-built venv.
- Update uninstall to remove both blue/green slots and backups.
- Update docs/INSTALLATION.md and README.md with new options.
2026-06-23 11:02:26 -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 Denis f9d0610452 chore: remove agent code now living in serverkit-agent repo
The Go agent has moved to its own repository:
https://github.com/jhd3197/serverkit-agent

Changes:
- Delete agent/ directory and all source, build, and packaging files
- Delete .github/workflows/agent-ci.yml and agent-release.yml
- Delete scripts/test/agent-test.ps1 (agent now builds in separate repo)
- Remove committed dist/serverkit-agent-windows-amd64.exe build artifact
- Update docs/INSTALLATION.md and docs/README.md links to point to the new repo
- Add migration notes to docs/AGENT_AUDIT.md, docs/REMOTE_ACCESS_ROADMAP.md,
  SECURITY_AUDIT.md, and docs/plans/AGENT_FLEET_PHASES_3-4.md

Panel agent API endpoints under /api/v1/servers/agent/ and /api/v1/agent/
are retained — they are the control-plane contract with the standalone agent.

Verified:
- backend: pytest (371 passed, 12 skipped)
- frontend: npm run lint (0 errors)
2026-06-21 03:30:57 -04:00
Juan DenisandClaude Opus 4.8 467ebc51b0 chore(docs): track regenerated UI screenshots
Drop the stray /docs/screenshots ignore that overrode the existing negation (.gitignore:117-118), so the 23 mock-data README screenshots are committed assets again, and add the freshly regenerated set (ServiceDetail header selector updated in the local capture skill).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 02:21:47 -04:00
Juan DenisandClaude Opus 4.8 6d8d8dfa31 docs(i18n): sync es/zh-CN/pt READMEs with the new meaningful features
The localized READMEs are older, slimmer variants missing several sections the
English README has, so each picked up the feature entries that map to existing
sections (Manual & Zip deploys, Container Lifecycle, SSL/TLS hardening, S3/B2
browsing, Passkeys/WebAuthn, WAF, GPU monitoring) plus the full set of new
completed roadmap items, the Tech Stack security row, and a NEW_FEATURES link.

Features whose parent section doesn't exist in a given translation (Remote
Service Tunnels, Connections, WordPress Publishing, Dynamic DNS, Secrets &
Webhook Gateway, CVE/SBOM, Encrypted Secrets at Rest) are represented in that
file's roadmap rather than fabricating new sections. No Layout Switcher
(UI-only) line added, consistent with the English README.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 17:23:13 -04:00
Juan DenisandClaude Opus 4.8 ae177d789c docs: catch README/CHANGELOG/feature reference up to the unreleased dev work
The dev branch is 23 commits ahead of origin with many user-facing features that
were never reflected in the docs. Bring them current.

- README: add WAF, container CVE/SBOM (grype/syft), passkeys/WebAuthn, dynamic
  DNS, GPU monitoring, container lifecycle (image updates / auto-sleep /
  auto-scale), secrets + inbound webhook gateway, backup encryption, optional
  hardened TLS, encrypted secrets at rest, and the layout switcher to the
  Features list, Roadmap, and Tech Stack; link NEW_FEATURES.md.
- CHANGELOG: add the above to [Unreleased] and a new Security subsection
  (CVE/SBOM, optional/hardened TLS + auto-CAA + HSTS gating, encryption at rest).
- NEW_FEATURES.md: add §6 documenting the SSL/TLS hardening follow-up
  (02afdec..a09bf0f) and renumber Known follow-ups to §7.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 17:08:24 -04:00
Juan DenisandClaude Opus 4.8 1f700cea06 docs: endpoint & page reference for this session's features
Reviewer-facing summary of the new endpoints, pages, models, config flags,
and operational notes for the DDNS / image-update / auto-sleep / auto-scale /
GPU / backup-encryption / WAF features and the layout switcher.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 15:34:55 -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 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 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 089b75526e fix(agent): accept registration token from env to keep it out of argv (M5)
The installers ran `serverkit-agent register --token <token>`, putting the
registration token in the process argument list — world-readable via `ps` /
/proc/<pid>/cmdline while register runs. The documented curl|bash one-liner also
landed the token in shell history.

- register now falls back to SERVERKIT_REGISTRATION_TOKEN /
  SERVERKIT_CONNECTION_STRING / SERVERKIT_SERVER_URL when the flags are absent
  (a process's environment is readable only by its owner and root).
- install.sh passes the token via env (drops the old `eval`) and accepts it from
  the environment so it need not appear on the curl line.
- install.ps1 passes the token to the child via env, and makes -Token optional
  with the same env fallback.

Verified: agent reads the env token at runtime (register proceeds past flag
validation to the network call); install.sh `bash -n` and install.ps1 parser
both clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 17:52:59 -04:00
Juan DenisandClaude Fable 5 0bc30b62ef ci: run agent Go checks and backend pytest on push/PR (addresses audit M4)
Nothing ran the agent's tests or the backend suite automatically, so breakage
was only found by hand. Add two workflows:

- agent-ci.yml: go vet + go test on linux, plus a cross-compile build matrix
  (linux amd64/arm64, windows amd64) to catch platform-specific breakage at
  PR time. darwin is excluded — fyne.io/systray needs the macOS Cocoa toolchain
  and can't cross-compile from Linux (it's built on a mac at release time).
- backend-ci.yml: install requirements and run the full pytest suite, including
  the new end-to-end agent command-loop test.

Verified locally: go vet clean, all three cross-builds succeed, agent unit
tests and the full backend suite (136 passed) are green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 17:31:26 -04:00
Juan DenisandClaude Fable 5 b57e436432 docs: add agent subsystem security & completeness audit
Severity-ranked, file:line-referenced audit of the agent fleet (Go agent,
gateway/registry/long-poll, pairing, panel→agent APIs, install/packaging, UI,
docs/CI) plus a remediation-status section recording the M1/M2/M3 fixes and the
M2 correction (compose allowlisting was already enforced at the handler layer).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 01:34:02 -04:00
Juan DenisandClaude Fable 5 5ceb2d83b7 fix(ui): serialize terminal input + demo chrome for the Terminal tab
Verified the S8.3 terminal live end-to-end (WSL agent -> gateway ->
browser xterm) and fixed what the test surfaced:

- RemoteTerminal sent every keystroke as its own HTTP POST; parallel
  POSTs reach the agent out of order, so fast typing arrived scrambled
  ("echo" -> "ehe..."). Input is now queued and flushed sequentially,
  coalescing keys that arrive while a chunk is in flight.
- Terminal page header -> PageTopbar (icon + mono meta), matching every
  other migrated page.
- term-shell rail rows: server icon + right-aligned glowing status dot
  (demo con-target); shell pane now sits flush against the rail with
  RemoteTerminal's drawer chrome flattened (demo con-main).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 00:36:45 -04:00
Juan DenisandClaude Fable 5 b2e4ac7c60 feat: interactive Terminal tab on /terminal over agent PTY (S8.3)
The "Terminal & Logs" page had no terminal. Now a Terminal tab hosts the
xterm console (RemoteTerminal) against paired agent servers, behind a
demo-style target rail (.term-shell: agent list with glowing status
dots, offline rows disabled, first online server auto-selected).

RemoteTerminal was built but never mountable - it imported a
nonexistent SocketContext and listened for a terminal:<sid> event that
nothing emits. Fixed the full output path:

- backend sockets.py: new subscribe_terminal/unsubscribe_terminal
  handlers join the browser into the agent gateway stream room
  (server_<id>_terminal:<session>) after validating the session via
  TerminalService.get_session
- RemoteTerminal: rewired to services/socket.js; consumes server_stream
  events filtered by the session channel (agent PTY -> gateway ->
  xterm), re-subscribes on socket reconnect, unsubscribes on unmount

VALID_TABS gains terminal (appended - logs stays the default landing
since the shell needs a connected agent; panel-host PTY remains a S5
gap, TerminalService rejects local).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 23:55:39 -04:00
Juan DenisandClaude Fable 5 1cbaa6fa27 feat(ui): Databases SQL editor gutter + highlighting, click opens console (S8.1)
- new databases/SqlEditor.jsx: the demo .sql-editor as a working editor -
  transparent <textarea> overlaid on a scroll-synced highlighted <pre>
  (identical font metrics), line-number gutter, SQL keyword/string/
  number/comment tints; wrap=off + horizontal scroll sync
- ConsoleTab swaps its bare textarea for SqlEditor (focus/shortcuts/
  read-only badge unchanged); dead .dbx-editor styles replaced by
  .dbx-sqled
- UX: single-clicking a database node in the tree now opens its SQL
  console directly (previously only via right-click context menu) and
  expands its tables; collapsing stays on the chevron so re-clicks do
  not fold the tree

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 23:34:51 -04:00
Juan DenisandClaude Fable 5 7647fde208 feat(ui): Docker per-container accent dots (S8.6)
The demo gives each container a hue-hashed identity dot in the name cell
(.dot-ico); the live table only had a green/gray status dot duplicating
the Status pill. The dot is now deterministic per container name, glows
when running, dims when stopped.

Also closes the S8.6 audit items: KPI icon tone-wash was a false alarm
(rules + tone props verified correct); Projects KPI skipped (labels only
exist on per-container inspect, count would cost an extra fetch per
target); logs modal->drawer stays blocked on the S6.3 decision.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 23:09:20 -04:00
Juan DenisandClaude Fable 5 ad2d3015cb feat(ui): Files parity - syntax-tinted preview + quick access (S8.5)
- new file-manager/highlight.js: the demo hlCode line tinting (escape
  first, then kw/str/num/cmt/var token spans; PHP + JS keyword sets)
- PreviewDrawer read view -> .pv-code with line-number gutter on
  --bg-code; Edit mode keeps the textarea; >4000-line files fall back to
  the plain read-only textarea
- Quick access rail group above Folders (Sites /var/www, Stack
  /opt/serverkit, Web config /etc/nginx, Logs /var/log)
- PageTopbar meta shows the browse target (agent name / "panel host")
- breadcrumb chevron icons -> demo mono "/" separators
- ext-badge on image thumbs always visible (was hover-only)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 23:06:53 -04:00
Juan DenisandClaude Fable 5 8ac61a7f1e feat(ui): WP list bulk-select + detail posture gauge & PHP version (S8.4)
List (demo bulkbar, real APIs, no bulk route needed):
- header + per-row checkboxes with accent is-selected rows
- bulkbar fans out per site: Start (unarchive), Stop (archive), Update
  core, Backup (snapshot), Clear cache - each scoped by an applies()
  filter so e.g. Stop only hits running sites; failure counts toasted

Detail:
- Security tab gains the demo posture panel: ScoreGauge ring + checklist
  computed from 3 real signals (integrity checksums clean, WP_DEBUG off,
  no critical/high vulnerabilities); checks that have not run stay out of
  the score instead of faking it
- header subtitle now shows PHP version (site.application.php_version)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 18:46:45 -04:00
Juan DenisandClaude Fable 5 6e3d0f5155 feat(ui): Services list -> dense sk-dtable + SegControl + src badges (S8.2)
The list was a flex-row layout with three native selects - the main reason
it did not read as the demo. Now:

- rows -> .sk-dtable in a surface card: checkbox gutter, Service (type
  tile + label sub), Source badge (GitHub/git icon + owner/name from
  deploy_repo_url), Domain (mono), Status Pill (amber deploying/building,
  red failed), Last Deploy, right-aligned lucide action icons
- status select -> SegControl (All/Running/Stopped); Type + Sort selects
  kept (capability superset over the demo)
- bulk-select + row-select preserved (sk-dtable is-selected)
- narrow screens drop Source/Domain/Last Deploy columns
- dead flex-row/list-header/status-dot/repo-pill SCSS removed

CPU/Memory/Server columns omitted (no list payload - S5 gap).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 11:15:04 -04:00
Juan DenisandClaude Fable 5 8b49fd2255 docs: mark S8.7 Workspaces done in the parity map
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 10:11:51 -04:00
Juan DenisandClaude Fable 5 c31ad144d3 docs: re-audit vs new design extract - add S8 parity-gap map
Pass-1 restyle stops short of real parity on several pages; S8 ranks the
gaps per page (Agent Fleet/Fleet Monitor never migrated, Databases still
cardified, Services list structure diverged, Terminal IA, WP list, etc.)
and sets the execution order.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 02:37:17 -04:00