324 Commits
Author SHA1 Message Date
Juan Denis 8bf6488b9c docs: redesign restyle COMPLETE - mark Applications/overlays/sweeps done 2026-06-11 02:12:17 -04:00
Juan Denis 98f41294b1 docs: mark WP detail restyle done; record flat-tabs decision (S6.2) 2026-06-11 01:30:17 -04:00
Juan Denis aa6981c6c5 docs: mark Git/Workflow/Backups body restyles done; record resolved is_active gap 2026-06-11 00:49:00 -04:00
Juan Denis 0530d531ab docs: mark Cron/Terminal/Workspaces/Email/Security/StatusPages body restyles done 2026-06-11 00:19:16 -04:00
Juan Denis 7e272492ef docs: mark FTP/Monitoring/ServerDetail/ServiceDetail+Templates body restyles done 2026-06-10 23:46:10 -04:00
Juan Denis 819630b02a docs: mark FileManager body restyle done in the redesign map 2026-06-10 23:15:43 -04:00
Juan DenisandClaude Opus 4.8 b758fda2fd feat(ui): restyle the DNS Zones + SSL bodies to the new look
Finish the Domains trio visually:
- DNS Zones: records table -> dense .sk-dtable; record type rendered as a colored .dns-rtype chip (A/CNAME/MX/TXT/NS/SOA…); content cell uses .sk-cell-mono. Drop unused Badge import.
- SSL Certificates: per-cert status Badge -> Pill (green Valid / amber Expiring). Drop unused Badge import.
Both already themed from Phase 0; these add the redesign's chip/pill language.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 00:25:09 -04:00
Juan DenisandClaude Opus 4.8 506945c44b feat(ui): restyle the Services list to a KPI strip + dense table
StatStrip -> MetricCard KPI strip (Running/Stopped/Total/Last Deploy with tone icons + subs); per-row status -> Pill; list header -> mono uppercase dense, list card on surface/border-soft tokens. All existing logic kept: search/type/status/sort filters, multi-select + bulk start/stop/restart, repo pill, per-row action buttons, and wordpress/service navigation. Drop now-unused StatStrip/Stat import.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 23:57:55 -04:00
Juan DenisandClaude Opus 4.8 d18d20ce36 feat(ui): restyle the WordPress site list to a dense table
Convert the .wp-site-card grid into a dense .sk-dtable: Site tile + name/port, Environments, Version (mono), Status (Pill green/gray), tag chips, and Open-Site/WP-Admin links. Add a status SegControl (All/Running/Stopped) alongside the existing tag filter; row click still opens the site detail. Omitted SSL/plugins/visits/uptime/server columns (not in the get_sites payload — no list backend) per restyle-first. Drop an unused Badge import.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 20:11:22 -04:00
Juan DenisandClaude Opus 4.8 e2faf9dc6e feat(ui): restyle the Servers list table to the demo's dense look
Visual restyle of the existing .servers-table (no column/capability changes — bulk-select, group, telemetry, and row actions all kept): mono uppercase headers, and thin token-colored telemetry gauges (CPU=accent-bright, RAM=cyan, Disk=green). Region/Load columns omitted (mock-only, no backend field) per restyle-first.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 19:18:28 -04:00
Juan DenisandClaude Opus 4.8 e2968b2197 feat(ui): migrate the WordPress group to the top-bar layout (sidebar now flat)
Final sidebar sub-menu moved to the demo's top-bar structure (REDESIGN_MAP §6 decision 3):
- Add shared WORDPRESS_TABS (WordPress / Pipeline); render PageTopbar on WordPress (sites list) and WordPressProjects (pipeline).
- Remove the wordpress subItems from the sidebar — NO sidebar item has a sub-menu anymore; the rail is fully flat, matching the demo.
- WordPress list: both headers (loading + main) swapped; Import Site + Create Site actions kept.
- Pipeline list header swapped (title 'Pipeline').
- The Pipeline tab is now always visible (was wpInstalled-gated in the sidebar).
Six infra groups total now use the top-bar layout: Domains, Servers, Services, Files, Monitoring, Marketplace, WordPress. Body restyles still pending per group.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 19:07:35 -04:00
Juan DenisandClaude Opus 4.8 7cc27cb82f feat(ui): migrate Monitoring + Marketplace groups to the top-bar layout
Final two sidebar sub-menus moved to the demo's top-bar structure (REDESIGN_MAP §6 decision 3):
- Monitoring group: shared MONITOR_TABS (Monitoring/Status Pages); PageTopbar on both pages (Refresh + Start/Stop + Create-Page actions kept).
- Marketplace group: shared MARKET_TABS (Marketplace/Downloads); PageTopbar replaces the marketplace hero; Downloads header swapped (Import ZIP + Refresh kept).
- Remove the monitoring + marketplace subItems from the sidebar.
Only WordPress (Pipeline) still has a sidebar sub-menu; everything else is now flat + top-bar. Body restyles still pending per group.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 18:35:21 -04:00
Juan DenisandClaude Opus 4.8 faa6355c75 feat(ui): migrate the Files page group to the top-bar layout
Move Files + FTP to the demo's top-bar structure (REDESIGN_MAP §6 decision 3):
- Add shared FILE_TABS (Files / FTP Server); render PageTopbar on both pages.
- Remove the files subItems from the sidebar (route /ftp unchanged, reachable from the tab).
- FileManager: top bar added as the first child of the full-bleed fullscreen browser (already a flex column; body keeps filling) — the toolbar, tree, preview, upload tray and all file ops are untouched.
- FTPServer: .page-header swapped for PageTopbar; the conditional install/test/start/stop/restart actions moved verbatim into the top bar.
- Fix a pre-existing unescaped-apostrophe lint error in FTPServer.
Follow-up: restyle the FileManager + FTP bodies.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 16:26:55 -04:00
Juan DenisandClaude Opus 4.8 c6d44ac502 feat(ui): restyle New Service mode chooser to the demo's method cards
Restyle .new-service-page__mode-card into the demo's centered method-card look (icon chip on top, title, description, corner check on the active card) and tokenize the old teal/indigo active-state literals. GitHub-OAuth repo picker, manifest detection, and the template flow are all preserved. Follow-up: template-catalog grid + Docker-Image/From-Scratch methods.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 16:14:08 -04:00
Juan DenisandClaude Opus 4.8 4e95c2257f feat(ui): migrate the Services page group to the top-bar layout
Move Services + its sub-pages to the demo's top-bar structure (REDESIGN_MAP §6 decision 3):
- Add shared SERVICE_TABS (Services/New Service/Templates/Deploy Activity); render PageTopbar on all 4 group pages so the tab group persists.
- Remove the services subItems from the sidebar (routes /services/new, /templates, /deployments unchanged, reachable from the tabs).
- Services: header -> PageTopbar (meta shows total/running; New Service action kept).
- NewService: drop the now-redundant breadcrumb; Connections action kept.
- Templates: search box moved into the top-bar actions.
- Deployments: New Service + Live/Paused + Refresh actions kept.
Follow-up: restyle the bodies (NewService method-cards + template catalog, Services dense table, ServiceDetail tabs).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 13:07:48 -04:00
Juan DenisandClaude Opus 4.8 8e61ad08d8 feat(ui): migrate the Servers page group to the top-bar layout
Move Servers + its 4 sub-pages to the demo's top-bar structure (REDESIGN_MAP §6 decision 3):
- Add shared SERVER_TABS (Servers/Agent Fleet/Fleet Monitor/Cloud Servers/Config Templates) and render PageTopbar on all 5 pages so the tab group persists.
- Remove the servers subItems from the sidebar (routes unchanged, reachable from the tabs).
- Main Servers page: top bar added ABOVE the full-bleed ops workspace (container -> column); the fleet-health rail, status/group filtering, workbar, bulk actions and pairing are fully preserved — no capability removed.
- Sub-pages (AgentFleet/FleetMonitor/CloudProvision/ServerTemplates): .page-header swapped for PageTopbar, action buttons preserved.
- Fix a pre-existing unescaped-quote lint error in FleetMonitor.
Follow-up: restyle the Servers list body to the demo's dense table + ServerDetail tabs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 04:30:51 -04:00
Juan DenisandClaude Opus 4.8 0eab32096b feat(ui): share the top-bar tab group across Domains/DNS/SSL
Finish the domain page group so the Domains/DNS Zones/SSL tabs persist on all three:
- Extract DOMAIN_TABS to components/domains/domainTabs.jsx; Domains imports it.
- DNSZones + SSLCertificates pages now render the shared <PageTopbar tabs={DOMAIN_TABS}> in place of their old headers (actions preserved).
- Fix a pre-existing unescaped-apostrophe lint error in SSLCertificates.
DNS/SSL page bodies (lists/forms) are not yet restyled — headers only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 04:03:29 -04:00
Juan DenisandClaude Opus 4.8 68695a779c feat(ui): migrate Domains to the demo's top-bar layout
First infra page moved to the demo structure (REDESIGN_MAP §6 decision 3):
- Add shared PageTopbar ds primitive (icon + title + routed sub-nav + actions).
- Domains: top bar with Domains/DNS Zones/SSL tabs (replaces the removed sidebar sub-menu), KPI strip (MetricCard), segmented filter (SegControl), .sk-dtable list, and a detail Drawer with SSL/linked-site/status specs + all actions (visit/verify/SSL/delete). Add-domain + enable-SSL modals preserved.
- Remove the domains subItems from sidebarItems.js (/dns, /ssl routes unchanged, reachable from the top-bar tabs).
- Omit registrar/DNSSEC/nameservers columns (no WHOIS backend yet) per restyle-first.
Follow-up: give DNSZones + SSLCertificates pages the same top bar so the tab group persists.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 03:46:46 -04:00
Juan DenisandClaude Opus 4.8 94877ffeca docs: add redesign change map
Per-view change map for the UI redesign: thesis, modularity contract, Phase 0 foundation, shared primitives, per-page deltas, backend gap register, decisions (incl. infra pages adopt the demo's top-bar layout, not sidebar sub-menus), and phased sequence.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 03:35:41 -04:00
Juan DenisandClaude Opus 4.8 6f70eb9ad1 feat(workspaces): make python.py grant-aware — every app blueprint now 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>
2026-06-05 23:08:44 -04:00
Juan DenisandClaude Opus 4.8 35642187c6 feat(workspaces): extend per-site ACL grants across all app blueprints (#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>
2026-06-05 23:04:06 -04:00
Juan DenisandClaude Opus 4.8 2d83440040 feat(workspaces): viewer/editor grant roles + operate-endpoint access (#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>
2026-06-01 13:13:18 -04:00
Juan DenisandClaude Opus 4.8 4dca43b6b0 feat(workspaces): sharing UI for per-site ACL grants (#33)
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>
2026-06-01 12:03:38 -04:00
Juan DenisandClaude Opus 4.8 d9a5ec7e92 feat(workspaces): per-resource access grants — per-site ACL backend (#33)
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>
2026-06-01 11:59:34 -04:00
Juan DenisandClaude Opus 4.8 35c7a73d8c feat(workspaces): manage servers in the workspace Resources modal (#33)
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>
2026-06-01 11:18:46 -04:00
Juan DenisandClaude Opus 4.8 8ea5f50408 feat(workspaces): scope the WordPress sites hub to the active workspace (#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>
2026-06-01 11:15:15 -04:00
Juan DenisandClaude Opus 4.8 e89415f3e0 feat(workspaces): reassign apps/servers between workspaces (#33)
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>
2026-06-01 09:32:30 -04:00
Juan DenisandClaude Opus 4.8 567d6059f7 feat(workspaces): workspace-scoped brand color recolors the panel (#33)
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>
2026-06-01 09:19:57 -04:00
Juan DenisandClaude Opus 4.8 ddff69020b feat(workspaces): active-workspace selector + ambient X-Workspace-Id 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>
2026-06-01 08:54:01 -04:00
Juan DenisandClaude Opus 4.8 299a004cdf feat(workspaces): workspace_id scoping foundation + safe backfill (#33)
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>
2026-06-01 03:36:24 -04:00
Juan DenisandClaude Opus 4.8 1926416f27 feat(wordpress): monthly client reports — persisted per-month rollups (#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>
2026-06-01 03:04:03 -04:00
Juan DenisandClaude Opus 4.8 98ffb5096e docs(wordpress): assess + defer the XL/infra-blocked tail (#32/#33/#34)
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>
2026-06-01 02:30:23 -04:00
Juan DenisandClaude Opus 4.8 6adc6e626c feat(wordpress): emit WP lifecycle events (#35)
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>
2026-06-01 02:27:52 -04:00
Juan DenisandClaude Opus 4.8 15fb6ebae0 feat(wordpress): safe update manager — snapshot, rollback, schedule (#29)
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>
2026-06-01 02:22:51 -04:00
Juan DenisandClaude Opus 4.8 5e2d4afbda feat(wordpress): per-site security depth — integrity, debug, WP-Cron (#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>
2026-06-01 01:57:43 -04:00
Juan DenisandClaude Opus 4.8 2b5db90f89 feat(wordpress): per-site plugin/theme/core vulnerability scanning (#28)
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>
2026-06-01 01:45:20 -04:00
Juan DenisandClaude Opus 4.8 6949ceac06 feat(wordpress): selective-push — specific-table selectors in PromoteModal (#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>
2026-06-01 01:21:51 -04:00
Juan DenisandClaude Opus 4.8 484aeb575a feat(wordpress): per-site traffic + error analytics (#25)
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>
2026-06-01 01:10:04 -04:00
Juan DenisandClaude Opus 4.8 6ef998515d feat(wordpress): per-site uptime + auto-incidents (#26)
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>
2026-06-01 00:47:26 -04:00
Juan DenisandClaude Opus 4.8 eca7cd2767 feat(wordpress): WP-aware alerting — site down/up → channels + events (#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>
2026-06-01 00:26:57 -04:00
Juan DenisandClaude Opus 4.8 8bc5cc5512 feat(wordpress): one-click create wizard — PHP + page/object cache (#15)
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>
2026-06-01 00:12:23 -04:00
Juan DenisandClaude Opus 4.8 1080d31118 feat(wordpress): per-site PHP version + limits panel (#24)
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>
2026-05-31 23:38:15 -04:00
Juan DenisandClaude Opus 4.8 6b67271677 feat(wordpress): page cache + per-site Redis object cache (Docker-correct)
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>
2026-05-31 23:30:17 -04:00
Juan DenisandClaude Opus 4.8 1f3ff4fcaf feat(wordpress): existing-site import (.sql) and clone-to-new-independent-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>
2026-05-31 23:09:02 -04:00
Juan DenisandClaude Opus 4.8 62adcdf28d feat(wordpress): passwordless WP-admin login, multisite detection, site 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>
2026-05-31 22:54:06 -04:00
Juan DenisandClaude Opus 4.8 4393a47512 feat(wordpress): finish Phase-A glue — create hardening, real URLs, offsite 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>
2026-05-31 22:39:57 -04:00
Juan DenisandClaude Opus 4.8 4f79e04645 feat(wordpress): health card, update manager, SSL panel, and archive/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>
2026-05-31 22:04:39 -04:00
Juan DenisandClaude Opus 4.8 dc54273ef0 feat(wordpress): Phase-A glue — WP-CLI actions UI, sanitization wiring, 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>
2026-05-31 21:37:38 -04:00
Juan DenisandClaude Opus 4.8 cf31b2658e fix(wordpress): resolve /sites blueprint collision; backup-before-delete + 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>
2026-05-31 21:19:58 -04:00
Juan DenisandClaude Opus 4.8 eab1a35794 docs: sync docs with dev, add CHANGELOG/SECURITY, document Windows agent
dev was ~149 commits ahead of the last release with no changelog and several
stale docs. This brings the user-facing docs in line with what actually ships.

New:
- CHANGELOG.md — curates the unreleased panel work (agent fleet, plugins,
  status pages, cloud provisioning, RHEL support, installer hardening).
- agent/CHANGELOG.md — the agent release workflow links to this on every
  published release; it was a broken link. Now documents the agent's
  independent release line.
- SECURITY.md — vulnerability reporting policy + agent trust model
  (agent.key is host-equivalent; Features.Exec off by default; single-worker).

Fixes:
- Stale version stamps: docs/README.md and docs/API.md said 0.9.0 -> 1.6.7.
- Supported OS: README/INSTALLATION said Ubuntu/Debian only; installer also
  covers Fedora and RHEL/Rocky/AlmaLinux.
- Dead links/placeholders: API.md wiki link, INSTALLATION.md "yourusername".
- Windows agent + short-code pairing were undocumented; added to README,
  agent/README.md, and docs/pairing.md (incl. corrected host-stable
  credential-key derivation — USERNAME was dropped in agent 1.6.14).
- Documented the single gevent-websocket-worker deployment constraint in
  CLAUDE.md and docs/ARCHITECTURE.md.
- CONTRIBUTING.md: removed reference to a non-existent frontend test suite.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 14:38:15 -04:00