Conversation
Clarify and expand the create-pr skill instructions: require conventional-commit style for PR titles (type(scope): summary), pick the dominant type, use lowercase imperative summaries, and note CI/version-bump implications. Increase the recommended Summary from 2-3 to 4-6 sentences and add detailed guidance on tone, context, reasoning, and what to include (and avoid). Add a final note that the skill should stop after writing the .pr file (do not run gh/git push); print the file path and a one-line readiness note.
…ete + archive #1 Collapse the duplicate WordPress hub routes. Both wordpress_bp and wordpress_sites_bp were mounted at /api/v1/wordpress and both defined GET/POST /sites, /sites/<id>, environments, plugins, themes and update, so the second blueprint's handlers were silently shadowed. wordpress_bp (the Docker stack model the live UI creates against with {name, adminEmail}) now solely owns the hub surface; wordpress_sites_bp keeps only its unique routes (sync, snapshots, clone-db, git). Verified 0 path+method collisions in the live URL map. #4 Backup-before-delete + archive. delete_site(create_backup=True) now captures a final files+DB backup via the Docker-aware backup_wordpress() (written to BACKUP_DIR, outside the site root, so it survives rmtree) before teardown, and returns the backup info. Added reversible archive_site/unarchive_site plus the POST /sites/<id>/archive and /unarchive routes, and a ?create_backup=false opt-out on delete. Frontend gains archiveSite/unarchiveSite/deleteSite(opts). Also fixed a latent bug: _teardown_wp_site called compose_down(remove_volumes=) but the real parameter is volumes=, which would have raised TypeError. Also adds docs/WORDPRESS_ROADMAP.md (35-task managed-WordPress roadmap). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…g, promotion rollback, deep-links Implements roadmap tasks #5, #9, #10, #12 — wiring existing primitives into the WordPress site object. #5 Surface orphaned WP-CLI maintenance actions. Adds flushCache/searchReplace/ harden methods to the WordPress ApiService and Quick Actions UI in WordPressDetail (Purge Cache, Search & Replace modal with dry-run, Harden behind a confirm). The backend routes already existed and had no UI. #9 Stop dropping the sanitization profile. promote_database and sync_from_production now resolve the selected sanitization_profile_id (via a new _apply_sanitization_profile_options helper) and merge its rules — anonymize, table truncation/exclusion, custom search-replace — into the clone options, instead of honoring only the boolean `sanitize`. User-scoped; composes with the existing flag; no-ops when no profile is selected. #10 Close the promotion rollback loop. Adds EnvironmentPipelineService.rollback_promotion (restores a promotion's pre_promotion_snapshot into the target environment — container import or host restore — sets status='rolled_back', locks/unlocks, logs activity) and the route POST /wordpress/projects/<prod>/promotions/<id>/rollback, plus a rollbackPromotion ApiService method. Pre-promotion snapshots were captured but never restorable. #12 Deep-link DB/Files/Logs from a site. Adds Open Files (File Manager at the site root), Open Database (DB manager pre-scoped via ?db=, hidden when db_name is null), and View Logs (global LogsDrawer for the container) to the WordPressDetail topbar. Databases.jsx MySQLTab now honors a ?db= query param to auto-open the QueryRunner. Verified: backend boots and the rollback route registers; frontend builds; the three changed frontend files lint with 0 errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…delete/rollback UI Implements roadmap #6, #7, #8 and surfaces the #4/#10 backends in the UI. #6 Consolidated Site Health card in the WordPressDetail overview: loads getProjectHealth + getProjectDiskUsage on mount and shows overall status, WP/PHP version, container/database/HTTP checks (HealthDot) and the previously orphaned DiskUsageBar — with loading/error/standalone-None handled gracefully. #7 WordPress update manager. New update_themes service method + POST /sites/<id>/themes/update route (registered before /themes/<theme>/activate), and getWordPressInfo/updatePlugins/updateThemes ApiService methods. OverviewTab shows a core "Update available" badge + Update button; PluginsTab and ThemesTab show per-row update badges/buttons (keyed off the WP-CLI `update` field) plus a bulk "Update all" bar. All update state is computed live via WP-CLI. #8 Per-site SSL panel. Shows the primary domain and live certificate status (via GET /ssl/advanced/health/<domain>) and an "Enable SSL" action that issues a Let's Encrypt cert through the existing /ssl/certificates route. SSL status is derived live (no DB column / migration); localhost / private-IP / no-domain sites degrade to an explanatory hint instead of a dead button. #4 UI: a Danger Zone with Archive/Unarchive (toggles on site.status) and a Delete Site modal (typed-name confirmation + create-backup checkbox) wired to the existing archive/unarchive/delete endpoints. #10 UI: a Promotion History list in the pipeline tab (self-fetched) with a Rollback button shown only for completed promotions that have a pre-promotion snapshot, wired to rollbackPromotion via the existing ConfirmDialog. SCSS added for update badges/bulk bar, health/SSL helpers, and promotion history. Verified: frontend build passes; backend boots and the themes/update route + update_themes method register; changed JS files add 0 new lint errors. No DB migration introduced. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ffsite snapshots, push-to-deploy, API keys Completes roadmap #2, #3, #11, #13, #14. #2 Canonical Docker create now finalizes the install. create_site waits for the container to be ready, runs `wp core install` (creates a real admin user) and applies container-valid hardening via the Docker-aware wp_cli bridge (DISALLOW_FILE_EDIT, XMLRPC_REQUEST off, shuffle-salts) — NOT host-filesystem ops, which don't apply to the named-volume model. The generated admin password is returned and surfaced once in the create page (so the new admin is usable). Best-effort: site creation still succeeds if the container is slow to finalize. #3 _enrich_site_data derives the site URL from the primary domain (https when ssl_enabled, else http), falling back to http://localhost:<port> only when no domain is attached. Fixes Open Site / Open WP Admin / the SSL panel. #11 Offsite snapshots + retention. New DatabaseSyncService.upload_snapshot_offsite (best-effort S3/B2 upload, no-op unless a remote provider + auto_upload are configured) is called after every snapshot is created (manual + pre-promotion). New prune_expired_snapshots backfills expires_at (default 30d, tagged snapshots kept) and deletes expired file+row; an hourly 'snapshot-retention' scheduler thread runs it. No migration (expires_at/status columns already exist). #13 Push-to-deploy for WordPress. WebhookService._handle_push_event now fans out to every WordPressSite whose connected repo+branch matches the push and has auto_deploy=true, calling GitWordPressService.deploy_from_commit. Repo URLs are matched scheme/.git/creds-agnostically via a new _normalize_repo_url helper. Best-effort; never breaks the generic-app webhook path. #14 WordPress REST is now API-key reachable. All 12 wordpress_sites.py routes switched from bare @jwt_required() to @auth_required() (accepts API key or JWT), reading the user via get_current_user().id instead of get_jwt_identity() (which returns None under an API key). Ownership filters unchanged. Verified: all backend files compile; the app boots and the retention scheduler, route swaps, and helpers load; _normalize_repo_url maps ssh/https/creds forms identically; frontend builds; WordPress.jsx lints clean. No DB migration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…te tags Phase B batch 1 — roadmap #16, #19, #20. #16 Passwordless "Auto Login" into wp-admin (the signature managed-WP feature), no heavy plugin. New WordPressService.create_login_url uses the Docker-aware wp_cli bridge to install the wp-cli-login package (idempotent) and mint a one-time magic URL (wp login create --url-only); login slug resolved via wp eval rather than hardcoding /wp-admin. New POST /sites/<id>/login (admin-gated) resolves or provisions a managed admin tied to the operator's panel email (create_user role=administrator), persists admin_user, writes a 'wordpress.admin_login' AuditLog, and returns the URL. A prominent primary "Auto Login" button in the site topbar opens it; Dashboard remains as fallback. #19 Multisite is now detected for real via `wp core is-multisite` (new is_multisite helper): surfaced in get_wordpress_info, set at create_site, and refreshed+persisted on the single-site detail load (never on the hot list endpoint). #20 Site tags/labels for agency organization. New JSON-in-TEXT tags column on WordPressSite (auto-added on boot by _fix_missing_columns; plus an idempotent 009 Alembic revision for determinism), exposed via to_dict, set via PATCH /sites/<id>/tags (normalized/deduped). Frontend: tag chips on each site card and a client-side tag filter bar on the WordPress list. Verified: backend boots (009 migration applies, tags column live, routes + methods present); frontend builds; changed JS lints with 0 errors. No frontend or backend regressions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dent-site Phase B batch 2 — roadmap #17 and #18 (#21 deferred: blocked without a public base domain). #17 Existing-site import (.sql-first MVP). New "Import Site" action on the WordPress hub opens a modal (name + original URL + .sql/.sql.gz dump). The backend POST /sites/import stands up a fresh WordPress+MySQL Docker stack via create_site, streams the dump into the container DB via DatabaseSyncService.import_to_container (root user, gzip-aware), rewrites the URL (wp option update home/siteurl + wp search-replace --all-tables), flushes caches, re-detects multisite, and clears the stale generated admin. wp-content-zip and SFTP pull are explicitly out of scope for the MVP. #18 Clone a site to a NEW independent top-level site with FRESH credentials. New WordPressService.clone_site stands up a brand-new stack via create_site, container-to-container clones the source DB with a URL search-replace, best-effort copies wp-content via docker cp, then provisions a fresh administrator (new generated password) so the clone does not share the source's creds (the DB import overwrites users). POST /sites/<id>/clone + a "Clone" button in the site topbar + a one-time credentials banner (with an "Open new site" link). No production_site_id => truly independent. Both reuse the existing create/clone/wp-cli primitives; DB creds read from each stack's .env (root user, literal db name 'wordpress' since the columns are NULL for Docker sites). FormData upload uses the api client's existing multipart path. Verified: backend boots, both routes register, all service methods load; frontend builds; changed JS lints with 0 errors. No DB migration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Below 768px the persistent sidebar was set to display:none with no replacement, leaving every route unreachable on phones/tablets. It now collapses into an off-canvas drawer driven by a fixed mobile top bar. - MobileTopBar: fixed header with hamburger toggle + brand (white-label aware, solid wordmark), shown only < 768px - Sidebar becomes a translateX drawer at min(280px, 86vw); focus trap, Escape-to-close, inert when closed (React 18 safe), 44px close button, aria-label + id wired to the toggle's aria-controls - DashboardLayout owns open state via useMediaQuery, closes on route change, locks body scroll, renders the scrim - Semantic z-index ladder (scrim/drawer/header), 100dvh + safe-area insets, viewport-fit=cover, prefers-reduced-motion fallback - Main content offset below the fixed header for normal and full-page routes Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Strategic design context for the impeccable workflow: product register, users/purpose, "precise, trustworthy, controlled" personality, anti-references, design principles, and WCAG 2.1 AA targets. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The app had no reduced-motion handling despite ~60 keyframe animations and pervasive transitions (auto-firing star burst, etc.). Honor the OS "reduce motion" setting globally while keeping loading state legible. - Global @media (prefers-reduced-motion: reduce) block collapses transitions and decorative animations to ~instant - Loading spinners (.spinner-ring, [class*="spinner"]) stay turning at a steady 0.8s infinite rotation, since the spin IS the status cue - Gate the sidebar star's auto-firing celebration behind a reactive useMediaQuery('(prefers-reduced-motion: reduce)') check - Add $ease-out-quart/quint/expo easing tokens (no bounce/elastic) and point the mobile drawer transition at $ease-out-quint Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rect) Phase C batch 1 — roadmap #22 and #23. The roadmap assumed nginx fastcgi/proxy cache + host php-fpm, but managed sites are self-contained wordpress:*-apache containers on localhost:port with no nginx in front — so both are implemented the Docker-correct way, entirely in-container via the wp_cli bridge. #22 Full-page cache via the cache-enabler plugin (Apache disk cache), installed + activated through wp_cli with WP-aware skip rules (wp-admin/login, logged-in + cart/checkout/account cookies, query strings). New routes GET/POST/DELETE /sites/<id>/page-cache; the enable flag is stored in WordPressSite.sync_config (no migration); the existing Purge Cache button now also purges the page cache. #23 Per-site Redis object cache. enable_object_cache ensures a redis service in the site's compose stack (NEW sites ship redis via the template; EXISTING sites get a redis service injected into docker-compose.yml + an additive `compose up -d` with no downtime), then installs the redis-cache plugin (bundled Predis, no phpredis needed) and points WP at host 'redis'. Status derived live from `wp redis status`. New routes GET/POST/DELETE /sites/<id>/object-cache; Purge Cache now also runs `wp redis flush`. UI: "Enable Page Cache" and "Enable Object Cache" toggles in the site Quick Actions, reusing the existing .quick-action-btn class (no new/shared SCSS). Verified: backend boots, all 6 routes register, all 8 service methods load, template YAML valid; frontend builds; changed JS lints with 0 errors. No DB migration. (Container-level behavior not runtime-tested — no Docker here.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase C #24. The roadmap referenced the host PHP-FPM API, but managed sites are wordpress:*-apache containers (PHP baked into the image tag), so this is done the Docker-correct way via the wp_cli bridge. New "PHP" tab on the site detail page showing the LIVE PHP version and key ini limits (memory_limit, upload_max_filesize, post_max_size, max_execution_time, max_input_time) read from inside the running container via `wp eval` (read-only, zero risk). A "Change PHP Version" panel switches PHP by rewriting the compose image tag to wordpress:<core>-php<x.y>-apache and recreating the container (DB/files preserved, brief downtime) behind a confirm; supported set 8.1/8.2/8.3. New routes GET/POST /sites/<id>/php; getPhpInfo/setPhpVersion client methods. The panel reuses existing app-panel/app-info-grid markup — no new SCSS (no conflict with the concurrent style session). Application.php_version is left untouched (not meaningful for Docker sites); the live container value is the source of truth. Writing arbitrary PHP limits is explicitly deferred (needs a conf.d ini + a compose bind-mount for durability across recreates). Verified: backend boots, both /php routes register, service methods load; frontend builds; changed JS lints with 0 errors. No DB migration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two refinement passes on the dashboard shell, committed together
because their edits interleave in Sidebar.jsx and _sidebar.scss.
Accessibility hardening:
- App-wide focus-visible baseline in _reset.scss, replacing the blanket
`* { outline: none }` that suppressed all focus rings. Pointer focus
stays ringless; keyboard focus gets a consistent accent ring.
- User-menu trigger is now a real <button> (aria-haspopup/expanded,
aria-controls, Escape-to-close with focus return).
- Nav expand buttons get aria-expanded + labels; theme/view buttons get
aria-pressed + accessible names; menu items get type="button".
- Dashboard app rows expose a real <Link> for keyboard/screen-reader
navigation (row stays mouse-clickable).
- Decorative icons marked aria-hidden; per-component focus rings on nav
items, menu items, and dashboard actions.
Quieter brand chrome:
- Removed gradient-text from the wordmark and white-label text; now
solid $text-primary (drops the background-clip:text ban).
- Replaced the auto-firing, bounce-eased star celebration (particle
burst, rings, shine sweep, bg flash) with a calm hover affordance;
deleted the animation effect/state and 8 dead keyframes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make WordPressService.create_site a single one-click orchestrator. It now accepts php_version / enable_page_cache / enable_object_cache and: - Bakes the chosen PHP version into the INITIAL image tag (concrete 6.4-php<x.y>-apache) so the site boots on the right PHP with no post-create container recreate, then finalizes + _harden_docker_site, then best-effort enables page cache (#22) and Redis object cache (#23) AFTER `wp core install` (redis already ships in the stack). Cache enablement never fails the create — failures degrade to a non-fatal warning; the one-time admin password is still surfaced once. - The live create modal (inline in WordPress.jsx) gains a PHP version selector (Default / 8.1 / 8.2 / 8.3) and page-cache / object-cache toggles, threaded through createForm -> POST /sites as phpVersion / enablePageCache / enableObjectCache (PHP validated server-side). Mechanism: declare a hidden VERSION template var and switch the image line to bare ${VERSION} in wordpress.yaml + wordpress-external-db.yaml so a concrete tag is written at install. This also fixes a latent #24 bug where the unresolved ${VERSION:-...} literal made set_php_version drop the WP-core pin; set_php_version now also falls back to the known core for legacy compose files. No DB migration — PHP/object-cache/SSL read live; the page-cache flag rides the existing sync_config column. Deferred: SSL/TLS + custom-domain at create. Managed sites are http://localhost:PORT containers with no per-site reverse proxy and no domain attached at create — the same missing public-domain/routing infra that blocks #21 and that #8 left frontend-only. TLS remains operable post-create via SiteSSLPanel (#8) once a domain is routed. Verified: backend py_compile, frontend lint-clean + production build, a 3-lens adversarial review (no blockers), and Docker Hub confirms all 6.4-php8.{1,2,3}-apache tags exist. Not runtime-exercised on a real Docker host (Windows dev). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…xt tokens - Dashboard connection state is no longer color-alone: the green/amber dot now pairs with a text label (LIVE / RECONNECTING) in the server readout and is announced via role="status". Label uses a readable neutral (contrast-safe in both themes); the dot carries the status color. Removes the orphaned .status-dot-live rules. - StatusBadge: destructive status dot was bg-white (off-pattern); now bg-red-400 to match the semantic hue of the other status dots. - _variables.scss: document that the $text-*-raw values feed compile-time fade() washes (neutral zinc ramp) and are intentionally distinct from the lightened runtime --text-* tokens, so the split reads as deliberate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#27) Route WordPress health transitions to the configured notification channels and the outbound webhook bus, edge-triggered so an autonomous poller never spams. EnvironmentHealthService.check_health now captures the prior health_status and, ONLY on a state transition (up<->down edge), fires _notify_health_ transition -> _dispatch_health_alert which: - preserves the existing WorkflowEventBus 'health_check_failed' emit (still fires every failing poll; back-compat for event-trigger workflows); - NEW: NotificationService.send_all to every enabled channel (Discord/Slack/Telegram/email/generic). unhealthy->critical, degraded->warning (delivered by default), recovery->info (delivered only to channels that opt into info). Sent off-thread because send_all does blocking HTTP/SMTP and must not stall the health check; - NEW: emits wordpress.site_down / wordpress.site_up via EventService, added to EVENT_CATALOG, so user webhook subscriptions (incl. wordpress.*) deliver with HMAC + retry and the events auto-appear in the subscription UI. Edge-triggering is the dedup (no in-memory cooldown, which would be single-worker-fragile): a continuously-down site alerts once, recovery once. No schema/migration and no frontend — channel delivery is governed by the existing Notifications settings. Also seeds #35's WP lifecycle-event registry. Deferred: the "error spike" rule needs per-site error metrics that #25 will provide. Verified: backend py_compile. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Standardize empty states across Servers, Domains, Databases, Docker, and Git onto the shared <EmptyState> component, matching WordPress/Services. Replaces five divergent idioms (docker-empty, raw empty-state divs, inline SVGs, servers-empty-workspace) so every "nothing here" layout reads the same: icon + title + description + optional action. Remove hard-ban design tells flagged in the audit: - Decorative gradients on icons (WordPress site/detail icons, Databases hero + engine status icons) -> solid brand colors. - Glassmorphism backdrop-filter blur on the Servers health panel and command bar (no-op over opaque surfaces). - Side-stripe borders (>1px colored left-borders, a named ban) on Servers alerts/notifications, Docker container cards, and Git status/deployment cards + SSH note -> full borders tinted with the same status color. No behavior changes; frontend build verified. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Second pass, applying the same treatment as the infra pages to the rest of the app. Migrate hand-rolled empty states across Applications, ApplicationDetail, Backups, CronJobs, DNSZones, Email, Firewall, FTPServer, SSLCertificates, Security, FileManager, Marketplace, Templates, ServerTemplates, Workspaces, StatusPages, Monitoring, CloudProvision, ServerDetail, ServiceDetail, WordPressProject, and WordPressProjects onto the shared <EmptyState> component (icon + title + description + optional action, plus its loading variant for "Loading..." placeholders). Inline SVGs and ad-hoc divs are replaced with consistent lucide icons. Strip hard-ban design tells found in the audit: - Decorative gradients on icon/avatar backgrounds and hero/card surfaces (Marketplace hero + rainbow stripe, extension/featured/template cards, monitoring hero, status-page background, WordPress project icon) -> solid tokens. - Side-stripe borders (>1px colored left-borders, a named ban) on Firewall/ FTP status cards, Security event/findings items, status-page incident rows + public incidents, template variable items, WordPress env/pipeline cards -> full borders tinted with the same status color. - Decorative backdrop-filter blur on an extension badge; kept it on real modal/drawer/drag backdrop overlays where it is purposeful. Rich "feature not installed / unavailable" full-page states (with help lists, resource requirements, troubleshooting) left as a deliberate tier. Frontend build verified. WordPressDetail.jsx intentionally excluded here (entangled with an in-progress feature on the working tree). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make a managed WordPress site appear on a status page with a real uptime % and auto-incidents, driven by an autonomous server-side health poller. Scheduler: a single-worker-guarded daemon thread (_start_health_check_ scheduler, 300s) runs EnvironmentHealthService.check_health for every RUNNING production site (skips archived/stopped so an intentional stop is not an outage). This keeps health_status fresh autonomously — so #27's transition alerts fire without the browser health card open — and drives bound status-page components. HealthCheck samples are pruned to 90 days (once/day) to bound growth. Schema (Alembic 010 + the boot-time _fix_missing_columns auto-add): - status_components.wordpress_site_id (-> wordpress_sites) - status_incidents.component_id (-> status_components) The existing HealthCheck table is reused as the uptime sample store. StatusPageService.sync_component_from_health maps the health verdict (healthy->operational, degraded->degraded, unhealthy->major_outage; unknown skipped), records a HealthCheck, recomputes a real COUNT-based uptime % (24h/7d/30d/90d; only fully-healthy checks count), and auto-opens an incident on ENTERING a major outage / auto-resolves on LEAVING it (operational OR degraded, so a degraded intermediate never strands the incident). Routes GET/POST/DELETE /sites/<id>/status-page read the binding + attach (production-only) / detach (resolves & unlinks the incident first -> no dangling FK on PostgreSQL, no stale public incident). New Uptime tab on the site detail (health + uptime % + attach/detach). Security: internal probe config (check_target, intervals) is stripped from the unauthenticated public-page projection, and a health-driven component stores no internal localhost:port target. Review fixes (3-lens adversarial pass): incident-resolve edge for the degraded-recovery path; detach FK/orphan cleanup in delete_component; public check_target leak; production-only attach; uptime-prune; degraded-uptime semantics documented. Verified: backend py_compile, frontend lint + build. Not runtime-exercised on a real Docker host (Windows dev). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
/docker is a FULL_PAGE_ROUTE, so .main-content drops its padding and the page manages its own. The workspace shell and the "Docker Not Available" state both relied on that missing padding, leaving their headers flush against the viewport top. - .docker-page (unavailable state): add the standard route padding ($space-10, $space-5 on mobile) so the header gets the same breathing room as every other page. - .docker-page-new.dx-page (workspace shell): bump the top from $space-3 to a $space-6 / $space-5 frame. Verified in-browser on the unavailable state; build passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A per-site Analytics tab showing traffic and error analytics, computed on-demand from the site's Apache access log — Docker-correct. Architectural pivot: managed WP sites are wordpress:*-apache containers on localhost:PORT with NO per-site nginx, so the roadmap's "nginx access log" premise does not hold. The official image symlinks Apache's access log to the container's stdout, so `docker logs` is the source. New WpAnalyticsService.get_traffic(container_name, hours) pulls `docker logs --tail 20000 --since <h>` (hard 15s timeout; reads only stdout to separate the access log from the stderr error_log; returncode-checked so a stopped/missing/remote-agent container degrades to an accurate note) and parses the Apache combined log on-demand — no store, collector, or migration. Returns requests, unique visitors, bandwidth, status distribution (2xx/3xx/4xx/5xx), 404s, bot %, error rate, top URLs (grouped by route; query strings stripped so visitor tokens are never surfaced), and a continuous hourly requests/errors series. Route GET /sites/<id>/analytics?hours= (owner/admin-guarded), client getSiteAnalytics, and a new Analytics tab on the site detail (stat cards + a recharts requests/errors AreaChart + status codes + Top URLs) with graceful empty/timeout/unavailable states. Also supplies the 5xx/error-rate signal #27's "error spike" rule needs. Deferred (not in the default access log): PHP fatals (need wp-content/debug.log via a WP_DEBUG toggle, #30), response time / slow pages (need a %D LogFormat), cache hit ratio (#22/#23). History is point-in-time over the retained container log; a persistent time-series collector is a clean follow-up. Review fixes (3-lens): docker-logs timeout + memory cap (single-worker safety); accurate unavailable note via returncode; query-string token stripping. Verified: backend py_compile, frontend lint + build. Not runtime-exercised on a real Docker host (Windows dev). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ServerKitLogo SVG hardcoded id="skBrandGradient" for its fill gradient. Two instances render at once (sidebar + mobile top bar), so the id collides in the DOM. url(#skBrandGradient) resolves to the first match, which is the mobile top bar's logo — hidden at 0x0 on desktop. A gradient with the default objectBoundingBox units and a zero-size owner has no coordinate space, so the fill painted nothing and the sidebar logo showed only its white background square. Generate a unique gradient id per instance with useId() so each logo binds to its own gradient. The glyph now paints the --accent-primary master color again across the sidebar, mobile bar, and auth screens. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reworks the Settings > Activity tab away from stat-card tiles and icon'd log entries toward a tighter, terminal-adjacent readout: - Activity summary becomes a single inline stats line (active today / actions this week / total users) instead of three stat cards. - Audit log entries render as compact log rows (role="list") with inline key=value details, dropping the per-row action icon and detail cards. - _users.scss sheds the old card/icon styling for the leaner row layout. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Modal (#31) PromoteModal already had files-only (code), db-only (database), and specific-folders (include_plugins/themes/mu_plugins/uploads). The only missing selective-push control was specific-tables, now added to the database section as two comma-separated inputs: - Exclude tables — omitted from the promotion entirely - Truncate tables — structure promoted, rows dropped Both split into arrays on submit. The backend already honored them end-to-end (POST .../promote forwards config.exclude_tables/truncate_tables -> promote_database/promote_full -> clone_options -> _transform_dump), so this is pure UI wiring, no backend change. Verified: frontend lint + build. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cross-reference a site's installed plugin/theme/core slug+version against the keyless community WPVulnerability API and surface findings per site. WpVulnerabilityService reads installed versions via the Docker-aware WP-CLI bridge (wp core version + get_plugins/get_themes directly — not the host-filesystem-gated get_wordpress_info, which fails for volume-backed Docker sites), queries https://www.wpvulnerability.net/{plugin,theme,core}/ (no key, 1h in-memory cache, descriptive UA, 8s timeout), matches the installed version against each advisory operator bounds (core advisories all apply — the feed is version-scoped), and extracts CVE id / severity (cvss3 -> cvss short-code -> score) / fixed-in / reference. Persisted as a new WordPressVulnerability child model (FK -> wordpress_sites.id) + a last_vuln_scan_at column (Alembic 011, idempotent). The scan runs in a background thread (Lynis-style) so the single worker never blocks; a new Vulnerabilities tab polls for status and renders findings with severity badges, CVE links, and a summary. On-demand only. Security (3-lens review): reference_url restricted to http(s) (blocks javascript: hrefs); feed slug percent-encoded; unrecognized version operator skips rather than over-flags; the major Docker volume-backed core-read gating was fixed so normal sites scan. Verified: backend py_compile, frontend lint + build. Not runtime-exercised on a real host. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…30) A per-site Security tab (Docker-correct, all via the WP-CLI bridge): - File integrity: a background wp core verify-checksums + wp plugin verify-checksums --all (in-container, reaches the volume; off the request thread, UI polls) flagging tampered/unexpected files. - Debug toggle: WP_DEBUG/SCRIPT_DEBUG with WP_DEBUG_DISPLAY=false and WP_DEBUG_LOG=/tmp/wp-debug.log — OUTSIDE the web root, so the log is never publicly fetchable (the default wp-content/debug.log is apache-served, a real info leak this avoids). The write is gated so a read-only/stopped site reports an honest failure. - WP-Cron: DISABLE_WP_CRON status + due-event list, run-due-now, enable/disable (with a clear "needs a real system cron" warning). Routes mirror the per-site pattern (owner-or-admin GET, @admin_required mutations). Deferred: the brute-force wp-login/xmlrpc rate-limit jail — Fail2ban/nginx limit_req both need a host-side per-site access log the localhost:PORT apache-container model does not expose (same gap as #21/#25). Verified: backend py_compile, frontend lint + build, focused security review (debug-log web-exposure caught + fixed). Not runtime-exercised on a real host. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
) Background safe update (Docker-correct, via wp_cli): record pre-update versions -> wp db export a DB snapshot (600s timeout via a new wp_cli timeout param so a large DB is never truncated; ABORTS if the snapshot fails) -> apply updates only to components with an available update minus an exclusion list -> a quiet, side-effect-free health probe (wp eval + HTTP probe; does NOT fire #27 alerts) -> if the update regressed a previously-healthy site, AUTO-ROLLBACK: version-pin each updated component back (--force --skip-plugins --skip-themes, so it runs even on a fatally-broken site) + re-import the snapshot -> re-check -> persist a WordPressUpdateRun report. The snapshot is kept for manual restore whenever the site does not end verified-healthy. A per-site cron schedule + exclusion list is driven by a new daemon-thread scheduler bounded by a concurrency semaphore (a shared 3am cron can't stampede the host). New model WordPressUpdateRun + auto_update_schedule/auto_update_exclude columns (Alembic 012). New Updates tab (run-now, schedule, exclusions, history). Also fixes a latent #26/#27 bug: EnvironmentHealthService.check_health returned 'unknown' for every production site (_get_compose_path needs a container_prefix they lack), making uptime/incidents/alerts inert for the primary site type — now falls back to the Application root_path compose. Deferred: staging-first promotion (needs a staging env + env-pipeline orchestration). Verified: backend py_compile, frontend lint-clean (full build was blocked only by a concurrent agent's in-progress AI import, not #29 code), 2-lens adversarial review (blockers fixed: production health gating, DB-import timeout; majors fixed: scheduler stampede, snapshot-abort). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add WordPress lifecycle events to EVENT_CATALOG (wordpress.created / deleted / backup_completed / updated / update_rolled_back / deployed / deploy_failed, joining #27's site_down/up) + a reusable EventService.emit_wp(event_type, site, **extra) helper (best-effort; never breaks the WP op). Emitted at concrete service points: create_site (wordpress.created), the #29 safe-update (wordpress.updated / update_rolled_back), and git deploy (wordpress.deployed / deploy_failed + the pre-deploy backup_completed). The catalog drives the subscription UI and EventSubscription.matches_event supports a wordpress.* wildcard, so these are immediately subscribable as outbound webhooks (HMAC + retry) and fire event-triggered workflows via the existing WorkflowEventBus — the trigger half of the Done-when. Deferred: WordPress workflow ACTION nodes (needs the workflow node-type registry + builder UI) and the serverkit wp CLI binary. Verified: py_compile. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document why the final three roadmap tasks are deferred rather than shipped rushed/blind, each with the concrete Docker-correct path + the specific blocker: - #32 per-site SFTP: Docker-correct design is a per-site SFTP sidecar (atmoz/sftp mounting wordpress_html, injected like #23 redis), but it exposes a security-sensitive, unverifiable-from-dev file-access surface + a port/container per site. Deferred for a real-host pass. - #33 agency scale: XL epic (workspace_id FKs + per-site ACL + RBAC rework + workspace branding + monthly reports) — cross-cutting, high-blast-radius; split into its own milestone. The report data sources (#26/#28/#29) now exist. - #34 horizontal scaling: XL AND blocked on the same missing per-site reverse-proxy/upstream layer as #21, plus a stateless-WP refactor. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AI is a core ServerKit primitive (not a plugin): an Intercom-style bubble + slide-in drawer on every dashboard page, with a tools+page-context assistant mode and a plain simple chat mode, SSE streaming, conversation persistence, and guarded write actions (the model proposes; the user approves). Plugins EXTEND the assistant rather than rebuilding it: backend via app.plugins_sdk.ai (reg.tool + register_context_provider, discovered from a plugin.json ai block with install/enable/disable hooks); frontend via useServerkitAI() and a contributions.ai category (suggested_prompts, tool_renderers). The marketplace gains an ai category for discovery. Backend: ai_service (Prompture Conversation + ProviderEnvironment + guardrails), ai_tool_registry singleton, ai_tools_builtin (core.* tools), the /api/v1/ai blueprint (SSE via a worker-thread + queue bridge, single-gevent-worker safe; in-thread confirmation gate), AiConversation/AiMessage/AiPendingAction models + migration 013, encrypted ai_* settings. Frontend: AIContext + components/ai/*, AI settings tab. Powered by Prompture (installed editable; requirements pinned). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reusable pieces behind the new full-screen database UI: - dbAdapter: unifies MySQL/PostgreSQL/SQLite/Docker behind one listTables/getStructure/runQuery interface, with per-dialect identifier quoting and a LIMIT/OFFSET browse-query builder - SourceTree: lazy, status-aware tree (engine -> database -> tables) - ConsoleTab: SQL console (Ctrl+Enter, read-only/write toggle, per-connection history, CSV export) - TableDataTab: paginated data grid + Structure view - ResultsGrid: shared sortable result table (sticky header, NULL handling) - BackupsTab: backups list with engine filter - modals: create database / user dialogs Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the five-tab Databases page with a DataGrip-style, full-bleed Database Explorer built on the file-manager shell: - unified source tree for all engines with inline + right-click actions (open console, back up, drop, query, copy name) - tabbed workspace: click a table to open a paginated data-grid tab, open SQL consoles per database, and a Backups tab - toolbar New menu (console / create database / create user, disabled when the engine is not installed) and a live status bar - frontend-only: table browsing runs SELECT ... LIMIT/OFFSET via the existing query endpoints; no API changes Rewrites _databases.scss into the explorer design system (tree, tabs, results grid, console, context menu, status bar) using existing tokens; respects prefers-reduced-motion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Grows the shared card stylesheet and StatCard component so page-level surfaces can drop their bespoke card SCSS and reuse one vocabulary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Moves Security page and its Overview tab onto the shared StatCard/card styles and trims the now-redundant per-page SCSS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reworks Git, Services, Firewall, FTP, Deployments, Monitoring, and Marketplace pages onto the shared StatCard/card system and removes their now-redundant page-specific SCSS (net large reduction in duplicated styles). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#33) The self-contained reports slice of the #33 agency-scale milestone, built without the high-blast-radius workspace_id/ACL/branding rework. - WpReportsService aggregates one calendar month per site: uptime % + a daily series + incidents (re-aggregated from #26 HealthCheck samples for the exact month), update runs (#29), DatabaseSnapshot backups, and current vulnerability posture (#28) + health/disk — each labelled persisted-history vs point-in-time. - Persisted (WordPressReport, Alembic 014 off 013_ai_assistant) + synchronous upsert per (site, YYYY-MM), so point-in-time sources (vulns, health) are captured as-of generation while true-historical sources are re-queried per month. Client grouping derives from the client-* tag (#20) — no workspace_id FK. Model lives in wordpress_site.py + lazy-imported in the service so the concurrent AI agent's models/__init__.py is untouched. - Routes GET /reports, POST /reports/generate (admin), DELETE /reports/<id>; client getReports/generateReport/deleteReport; a Reports tab with a recharts daily-uptime chart, summary cards, Print->Save-as-PDF (scoped @media print, no PDF dep) and JSON export. - 9 pytest tests; 3-lens adversarial review (4 of 5 findings fixed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The cross-cutting backbone the rest of the #33 agency-scale milestone needs, landed as a contained, narrow-only foundation rather than a 36-route rewrite. - Nullable workspace_id FK on applications + servers (Alembic 015 off 014) with an idempotent raw-SQL backfill: create a Default workspace, attach every workspace-less row, and make every existing user a member so nobody loses visibility once scoping is activated. Also creates the workspace_id indexes that upgraded installs would otherwise lack. - Centralized scope helper in WorkspaceService: ensure_default_workspace, resolve_workspace_id (validates a requested workspace; membership-checked with admin bypass; rejects deactivated accounts on the workspace path), and scope_query. - Opt-in + narrow-only: a request filters by workspace only when it carries an X-Workspace-Id header / ?workspace_id=. scope_query INTERSECTS the workspace filter with the existing per-user ownership filter, so it can only narrow visibility, never broaden it — with no context the behavior is byte-identical to before (admin->all, owner->own, servers global). This closes the footgun where the backfill would otherwise let a non-admin enumerate all resources via the Default workspace. - Wired into apps.py + servers.py (list + create): replaces the duplicated ownership branch and stamps new resources with the active-or-default workspace. Deferred to follow-on slices: broad route adoption, per-site ACL, the workspace<->User role reconciliation, server-side branding, and a frontend workspace selector. Verified: 10 new pytest tests (scope-narrowing, a can't-broaden-access regression, deactivated-user gate, raw-SQL backfill against real SQLite + idempotency), full suite 120 passed, a real end-to-end alembic upgrade to 015 on a fresh DB (columns + indexes), and a 2-lens adversarial review (3 findings, all fixed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…iven context (#33) A stale or forbidden X-Workspace-Id (deleted workspace, removed member) now degrades to no-scope instead of 403/404, so an ambient workspace-context header can never break the apps/servers lists. Safe by construction: falling back to no-scope only ever shows the user their own resources and stamps the default workspace on create — it never grants access to, or creates in, a foreign workspace. Also drops the error-tuple from the four call sites. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…header (#33) Makes the workspace scoping foundation user-visible — the research flagged the missing 'current workspace' context as the core gap #33 had to close. - WorkspaceSwitcher (sidebar): self-contained selector that reads/writes an active_workspace_id in localStorage, fetched from the user's workspaces. Hides for single-workspace installs and drops a stale selection on load. - services/api/client.js: sends the active workspace ambiently as X-Workspace-Id on every request (endpoints that don't honor it ignore it); cleared on logout. - Switching reloads so all lists re-fetch under the new scope. Backend resolve_workspace_id is lenient (prior commit) so a stale header can never break the apps/servers lists. Verified: frontend lint-clean + production build; the header->scoping path is covered by the backend API tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Selecting a client workspace now recolors the whole panel to that client's brand — the visible payoff of the workspace effort. - A workspace's primary_color (already on the model) is now editable via a color picker added to the Workspaces create modal. - WorkspaceSwitcher mirrors the active workspace's color into a workspace_accent localStorage key; ThemeContext applies it via the existing applyAccentToDOM with precedence over the user's personal accent (workspace_accent || accentColor), recoloring the logo gradient, buttons, and highlights. - Deterministic (no effect-ordering races): switching reloads and the value is read once on mount. Cleared on logout and on a stale selection; reverts to the user accent on 'All workspaces'. Default color is the app accent, so a workspace is unbranded unless a custom color is chosen. Verified: frontend lint-clean (no new warnings) + production build. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the organizing loop — existing resources were stuck wherever the backfill put them (Default); now they can be moved into client workspaces. - PUT /apps/<id>/workspace (owner-or-admin on the app) and PUT /servers/<id>/workspace (developer+), both requiring the TARGET be a workspace the caller can access (member or admin). A null target moves the resource back to Default. - UI: an 'Apps' management modal on each Workspaces card (mirrors the Members modal) with move-in / move-out. It fetches with allWorkspaces (an empty X-Workspace-Id header) so it can see apps in other workspaces to move them in, sidestepping the ambient scope. - Fixed a real bug caught by tests: the app ownership check compared app.user_id to the stringified get_jwt_identity() (int != str -> spurious 403); now uses user.id. Verified: 2 new endpoint tests (ownership + target-membership + null->Default + role gates), full suite 122 passed, frontend lint-clean + production build. Servers reassignment is API-only for now; the modal is apps-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ce (#33) Closes the inconsistency where the workspace switcher filtered apps/servers but not the WordPress hub. - WP sites carry no workspace_id of their own, so get_sites(workspace_id=...) scopes join-based through the site's parent Application.workspace_id; list_sites resolves the active workspace and passes it. - The hub is global today, so with no workspace context behavior is unchanged (additive, narrow-only). - No frontend change: the WP hub already fetches through the header-injecting api.request, so it auto-scopes on the ambient X-Workspace-Id. Verified: a new API test (global vs workspace-scoped), full suite 123 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the reassignment feature symmetrically — the server reassign endpoint shipped earlier (e89415f) was API-only; now it has UI. - getServers gains an allWorkspaces bypass (empty X-Workspace-Id header) and a setServerWorkspace client method (mirrors apps). - The per-workspace modal is now a 'Resources' modal with both Applications and Servers sections, each with move-in / move-out. Verified: frontend lint-clean (no new warnings) + production build. The server endpoint itself is already covered by backend tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
) An owner/admin can share a specific application (transitively its WordPress site / DB / domains) with a specific user who doesn't own it. Access-level for now; per-grant view/edit roles are reserved via a role column. - New ResourceGrant model (Alembic 016, unique per user x resource) + ResourceGrantService (grant/revoke/list/user_has_grant/granted_ids). - Enforcement at the cleanest seams: scope_query gained grant_resource_type so granted apps appear in the apps list (own OR granted, then workspace-narrowed); get_app and the WordPress _owner_or_admin_app helper became grant-aware, so one change opens every WP per-site route to a grantee. - Grant-management API (GET/POST/DELETE /apps/<id>/grants, owner-or-admin; can't grant to the owner; revoke is resource-scoped so you can't revoke another app's grant by id). Uses user.id (int), not the stringified get_jwt_identity(). Verified: 4 new ACL tests (visibility, get_app, WP route access, management perms + idempotency + revoke), full suite 127 passed, end-to-end alembic upgrade to 016. Deferred: a Sharing UI (next), and broadening grant-access to the generic app operate endpoints (start/stop/update stay owner-or-admin; delete stays owner-only). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the per-site ACL feature with UI on top of the backend (d9a5ec7). - A per-app 'Share' action in the Workspaces Resources modal opens an inline sub-view: current grantees with Revoke, plus a grant-access list of users (the owner and existing grantees are filtered out). - Client methods getAppGrants / grantAppAccess / revokeAppAccess. Verified: frontend lint-clean (no new warnings) + production build. The grant endpoints are covered by backend tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#33) Completes the chosen access-level ACL design: a grant now lets the user actually operate the resource, gated by a real role. - Grants carry a role: editor (view + operate) or viewer (read-only). grant POST validates role in {viewer, editor} (default editor); ResourceGrantService gains grant_role(); the Sharing UI gains a role selector. - Two access helpers split the app endpoints: _can_access_app (owner/admin/ANY grant) gates reads (get_app, /linked, /logs, /container-logs, /containers, /status); _can_edit_app (owner/admin/EDITOR grant) gates mutations (/link, /unlink, /environment, PUT, /start, /stop, /restart). - DELETE stays owner-or-admin (an editor can't delete — tested). - The same sweep fixed a latent int-vs-str ownership bug across ~14 app endpoints (compared app.user_id to the stringified get_jwt_identity(); now user.id). Verified: 3 more ACL tests (viewer read-only + can't operate, editor operates + can't delete, role validation/default), full suite 130 passed, lint + build. Still owner-only: env-vars/databases/domains blueprints (follow-up sweep). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#33) A grant now reaches every app-related blueprint, not just apps.py + WordPress, so a shared application is fully manageable by its grantees. - Promoted the access helpers to a shared seam: ResourceGrantService.can_access_app (owner/admin/any grant -> reads) and can_edit_app (owner/admin/editor grant -> mutations); apps.py now delegates to them. - Swept 33 ownership checks across 6 blueprints (categorized read vs mutate via a parallel mapping workflow): builds.py (12), domains.py (7, incl. SSL + domain CRUD), deploy.py (6), private_urls.py (5), templates.py (2), databases.py (1). Reads -> any grant; mutates -> editor grant. - Fixed the same latent int-vs-str ownership bug in all 33 (now user.id). Deferred: python.py's single get_app_or_404 helper gates read+mutate+delete together and needs per-method splitting before it's safe to make grant-aware. Verified: cross-blueprint integration test (grantee reaches builds deployments; stranger 403), full suite 131 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…w covered (#33) Re-reading python.py showed every mutating endpoint is independently @admin_required (runs before the shared get_app_or_404 gate), so that gate only governs the 4 read GETs (packages/env/status/gunicorn-config). Pointing it at can_access_app safely opens those to grantees while the admin-only mutations stay untouched — no per-method split needed. With this, every app-related blueprint is grant-aware. Also fixes the int-vs-str identity comparison in the helper. Verified: a python read-gate test (stranger 403, grantee passes), full suite 132 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Expose and surface containerised databases and introduce managed app base port control. Backend: add endpoint to list databases across all Docker apps, robustly resolve compose container names, include docker DB user when querying, compute migration needs from live heads and self-heal orphaned alembic_version entries by stamping to head, and add the managed_app_base_port system setting. Template service now honours the managed_app_base_port and WordPress template default HTTP port updated to 8300. Frontend: add react-icons brand icons and DatabaseBrands component, show Docker-hosted DBs under their engine nodes with a docker badge, platform-aware console shortcut label, pass DB user to docker table/query API calls, add API client header merge fix, UI for managing base port in settings, enhanced migration history UI (backup/apply/resync flows, confirm dialog, toasts), and various style tweaks and color additions.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is the big one — three headline features land together, plus a quiet design-system cleanup that finally pays down a pile of duplicated styles. ServerKit gains a built-in AI assistant (a chat bubble + drawer powered by Prompture) that can actually do things through RBAC-gated, human-confirmed tools; WordPress management graduates from "we host the container" to a real per-site control plane — uptime, analytics, vulnerability scanning, safe updates, and monthly client reports; and workspaces grow real multi-tenancy with per-resource sharing, so you can hand a client viewer or editor access to a single site without giving them the keys to everything. The Databases page was rebuilt from scratch as a full-screen explorer, and a wide swath of pages moved onto a shared card system, shedding hundreds of lines of per-page SCSS. Workspace scoping was deliberately built opt-in: nothing filters by workspace until a request carries a context header, so the foundation changed zero behavior until the UI lit it up. And the AI piece is intentionally core, not a plugin — plugins extend it through a small SDK rather than each shipping their own chatbot.
Highlights
.sqlimport, and clone-to-new-site.Technical changes
AI assistant (core primitive, powered by Prompture)
services/ai_service.pywires Prompture per-instance viaProviderEnvironment(neveros.environ), rebuilds the system prompt each turn with the current page context plus plugin-contributed context providers, and persists chats via PromptureConversationexport()/from_export.services/ai_tool_registry.pyis a process-global singleton (mirrorsagent_registry) aggregating built-incore__*tools and plugin tools; each request builds a freshToolRegistryfiltered by the user's RBAC and chat mode.is_write=True) route through a human-in-the-loopConfirmationGatethat pauses the streaming worker thread until the user approves/denies over/chat/confirm. A tool the user can't use is never offered to the model.<plugin-slug>__<name>/core__<name>(double underscore, capped at 64 chars) to satisfy OpenAI/Anthropic function-name constraints and prevent collisions.plugins_sdk/ai.pyexposes aPluginToolBinderso a plugin can register tools, suggested prompts, and context providers from aplugin.jsonaiblock — extending the core assistant instead of shipping its own.PromptInjectionDetector+PIIRedactor. Frontend:contexts/AIContext.jsx,components/ai/*(ChatBubble, ChatDrawer, ToolCallCard, ConfirmActionCard, MessageList, …), SSE streaming inlib/ai/sse.js,usePageContext+lib/ai/pageContextMap.js, andsettings/AISettingsTab.jsx. Backendmodels/ai.py; migration013_ai_assistant. Prompture added torequirements.txt.WordPress per-site control plane
wp_analytics_service(traffic/error analytics),wp_vulnerability_service(plugin/theme/core scanning),wp_security_service(integrity, debug, WP-Cron),wp_update_service(snapshot / rollback / scheduled updates),wp_reports_service(monthly rollups),db_sync_service, plusgit_wordpress_servicehooks.services/wordpress_service.py(+797) andapi/wordpress.py(+698) add the create wizard (PHP + page/object cache), passwordless wp-admin login, multisite detection, site tags,.sqlimport, clone-to-new-independent-site, offsite snapshots, push-to-deploy, API keys, and per-site PHP/limits.event_service.models/wordpress_site.py+ migrations009–012and014back tags, status monitoring, vulnerabilities, update runs, and reports.components/wordpress/PromoteModal.jsxgains selective-push with specific-table selectors. Page cache + per-site Redis object cache are wired Docker-correctly viatemplates/wordpress.yamlandwordpress-external-db.yaml.api/wordpress_sites.pyshrinks (~497 lines removed) as logic consolidates into the service layer and the duplicate/sitesblueprint collision is resolved (backup-before-delete + archive added in the same pass).Workspaces + per-resource ACL (#33)
services/workspace_service.pyadds three scoping seams —ensure_default_workspace,resolve_workspace_id,scope_query— so the admin-vs-owner-vs-workspace branch isn't re-implemented per route. Scoping is opt-in: a request only filters when it carriesX-Workspace-Id/?workspace_id=; with no context, existing ownership behavior is preserved exactly.resolve_workspace_idis deliberately lenient — an unparsable, unknown, deactivated-account, or not-permitted context degrades toNone(no scoping) instead of raising, so a stale header (deleted workspace / removed member) can't break the UI. Falling back toNoneonly ever shows the user their own resources and stamps the default workspace on create.services/resource_grant_service.py+models/workspace.pyResourceGrantimplement per-site ACLs:can_access_app/can_edit_app(viewer = read, editor = write/operate), idempotentgrant, and arevokethat verifies the grant belongs to the named resource so a caller can't revoke another resource's grant by id.scope_querywidens a non-admin's list by explicit grants only — never by workspace membership alone.apps.py,builds.py,databases.py,deploy.py,domains.py,private_urls.py,templates.py,python.py. App/server reassignment between workspaces added; the WordPress sites hub is scoped to the active workspace.components/WorkspaceSwitcher.jsxsets an ambientX-Workspace-Idheader viaservices/api/client.js; workspace brand color recolors the panel throughcontexts/ThemeContext.jsx;pages/Workspaces.jsxgains the Resources modal (manage/reassign servers) and the sharing UI (viewer/editor grants). Migrations015_workspace_scope(with safe backfill) and016_resource_grants.Databases explorer
pages/Databases.jsxrebuilt as a full-screen explorer (net ~2K lines removed) composed from newcomponents/databases/*—SourceTree,ConsoleTab,ResultsGrid,TableDataTab,BackupsTab,dbAdapter.js, andmodals.jsx.UI / design system
components/StatCard.jsx+styles/components/_cards.scssexpanded into a shared card system; Security, Overview, Deployments, FTPServer, Firewall, Git, Marketplace, Monitoring, and Services adopt it, deleting ~600 lines of per-page SCSS.components/MobileTopBar.jsx,hooks/useMediaQuery.js),prefers-reduced-motionsupport, keyboard-a11y hardening, unified empty states + "AI-slop" copy cleanup, a denser Activity tab, and a status-badge dot fix.components/ServerKitLogo.jsxnow generates a unique gradient id per instance, fixing the logo rendering blank when more than one is mounted.Docs / infra
PRODUCT.mddesign context;docs/WORDPRESS_ROADMAP.md(35 numbered tasks, the "glue not absence" Phase-A thesis, with the XL/infra-blocked tail assessed and deferred); minor create-pr skill guideline tweaks;.gitignoreupdates.