- 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>
- 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>
- 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>
- 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.
- 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.
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>
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>
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>
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>
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>
- 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>
- 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>
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>
- 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>
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>
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.
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.
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.
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
The demo's missing /workspaces/:id detail view, built on real data only:
- new pages/WorkspaceDetail.jsx: back-link + site-header hero (brand tile,
active-workspace / status Pill, mono slug-members-since meta), Workspace
info card (sk-info-row) + live Resources count strip, accent-underline
tabs (Servers / Services / Sites / Members) over sk-dtable rows with
Manage links, move-in/out pickers, member add/remove, per-app Share ACL
modal, Set active (localStorage scope + reload, mirrors
WorkspaceSwitcher), Settings edit modal (updateWorkspace), Archive/
Restore + admin Delete
- list page slimmed to the demo card: clickable card -> detail, "Open >"
footer, .ws-card.active accent wash for the scoped workspace; the
members/resources/sharing modals moved into the detail page
- route /workspaces/:id added
Deferred (S5 backend): plan/billing, aggregated CPU/MEM usage bars,
member last_active.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The demo is a flush full-height 3-pane console; the live page was padded
floating cards, which is why it did not read as the new design.
- de-cardify: .db-explorer padding/gaps -> 0; toolbar/tree-panel/
workspace/statusbar lose radius+outer borders, join on 1px edges
- ResultsGrid: demo cellEl semantic tints (NULL italic-ghost, emails/URLs
+ positive statuses green, negative amber)
- tab strip: demo + new-tab pill (opens SQL console on selected source)
- result count number bold green
Follow-up (S8.1): SQL editor line-number gutter + keyword highlighting.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
These two pages were never migrated: hard-coded Tailwind light-palette
utilities (bg-gray-200, bg-yellow-50, bg-blue-600, bg-white modal) made
the /servers -> /fleet jump read as a different app and broke dark theme.
- StatCard/StatsGrid -> MetricCard KPI strip; progress bars -> ds Gauge
- status Badges -> Pill (versions, rollouts, heatmap, alerts, anomalies,
search, discovery)
- new token homes in _fleet-monitor.scss: .fleet-kpis/.fleet-warnrow/
.fleet-ico/.fleet-statbox/.fleet-predbox/.fleet-modal/.fleet-statrow
- heatmap buckets + forecast chart strokes -> theme vars
- _utilities.scss: shim text-green-500/red-500/yellow-600/yellow-700
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>