1018 Commits
Author SHA1 Message Date
Juan Denis cf4872491f Merge pull request #52 from jhd3197/dev
Harden the installer, updater, and uninstaller across distros
v1.7.2
2026-06-25 09:22:51 -04:00
github-actions[bot] c6d75f148c chore: bump version to 1.7.2 [skip ci] 2026-06-25 13:10:30 +00:00
Juan DenisandClaude Opus 4.8 db16100a42 feat(installer): Phase 5 — additive Python bootstrap foundation
Hardening plan §8. A stdlib-only, import-safe, unit-tested Python package that
ports the stabilized bash install logic into testable orchestration. Per the
plan's migration path this is ADDITIVE — install.sh stays canonical and this is
NOT wired into it yet; it is the validated foundation a future thin-bootstrap
install.sh can call.

backend/serverkit_installer/:
  - _proc.py    — sudo-aware subprocess helpers, mirroring app/utils/system.py
                  WITHOUT importing app (the bootstrap runs before the venv
                  exists, so it must not pull in Flask).
  - distro.py   — OS family mapping (a faithful port of install.sh's
                  os_family_from, rhel-before-fedora) + container/WSL/systemd
                  predicates (mirroring scripts/lib/env.sh).
  - deps.py     — loads scripts/deps/manifest.yaml (its primary consumer) and
                  resolves base/python/node/docker/pkg-manager per family.
  - firewall.py — detect + pure open/close command builders (port of
                  scripts/lib/firewall.sh).
  - service.py  — init detect + pure start/stop/enable/reload builders (port of
                  scripts/lib/init.sh).
  - main.py     — composes a READ-ONLY install "plan" + `python -m
                  serverkit_installer plan|detect` CLI. No host mutation.

Tests: backend/tests/test_serverkit_installer.py — 46 pure-logic cases
(family mapping vs os_family_from, real-manifest resolution for all six
families, firewall/service command builders, env predicates, composed plan).
46 passed; py_compile + pyflakes clean. The CLI proves the manifest is genuinely
consumed (Debian -> apt + python3.11 + base packages), not dead config.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 01:06:44 -04:00
Juan DenisandClaude Opus 4.8 f744106d64 test(scripts): Phase 4 — vm update/uninstall tests + 7-distro CI matrix
Hardening plan §7. Catches distro-specific regressions before release.

- scripts/test/vm-update.sh: install → forced local update (full blue/green
  deploy/migrate/switch/health) → re-run to confirm the "Already up to date"
  version-gate. Same VM harness contract as vm-install.sh (log + status file).
- scripts/test/vm-uninstall.sh: install + seed data → default uninstall asserts
  data preserved (DB snapshot in backups, /var/lib survives) → reinstall →
  --purge asserts data removed. End-to-end proof of the unified uninstaller.
- scripts-ci.yml: now runs all three serverless suites (update+install+lib),
  lints the new lib/ + test/ files (error severity) and keeps scripts/lib/*
  warning-clean, and expands the cross-distro unit matrix to Ubuntu 22.04/24.04,
  Debian 12, Rocky 9, AlmaLinux 9, Fedora 40, and openSUSE Leap 15.5.
- test_install.sh: +choose_pkg_manager graceful-failure case (7.4); a no-op
  docker stub so the uninstall-matrix cases run where docker isn't installed.
- test_lib.sh: neutralize the ambient container=oci env var (RPM/SUSE images)
  and force an empty PATH for the "no package manager" case — both bugs the new
  matrix surfaced (the tests, not the code).
- README: fixed the stale Fedora box, documented the new VM scripts + suites.

Validated by running the exact CI container logic locally across all seven
images: test_update 13/13, test_install 15-16/0, test_lib 13/13 everywhere.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 01:00:45 -04:00
Juan DenisandClaude Opus 4.8 ff35375b50 feat(install): Phase 3 — multi-distro abstraction, init/env detection, templated unit
Hardening plan §6. Reduces the distro-specific if/else surface and handles
edge environments:

- Dependency manifest (scripts/deps/manifest.yaml): declarative per-family
  package/python/node/docker/init spec. Consumed directly by the Phase 5
  Python bootstrap; install.sh mirrors it inline (it can't parse YAML before
  Python exists in the curl|bash path).

- New sourceable libs (built in parallel, each shellcheck-clean + unit-tested):
  - scripts/lib/pkg.sh  — pkg_detect/refresh/install/is_installed across
    apt/dnf/yum/zypper/pacman/apk (PKG_MGR_OVERRIDE + PKG_DRY_RUN for tests).
  - scripts/lib/init.sh — init_detect + start/stop/enable/disable/reload/
    is_active across systemd/openrc/runit/sysvinit/none (INIT_OVERRIDE/DRY_RUN).
  - scripts/lib/env.sh  — is_container/is_wsl/has_systemd (override-friendly).

- ID_LIKE handling (G5/6.5): install.sh's os_family_from() (now a pure,
  testable function reading ${SERVERKIT_OS_RELEASE}) maps unknown IDs via
  ID_LIKE to debian/rhel/fedora/suse/arch/alpine — rhel before fedora so RHEL
  clones pick the RHEL Docker repo.

- zypper + pacman + apk (G6/6.6): choose_pkg_manager/refresh_pkg_index/pkg_add,
  provision_docker and provision_python gained suse/arch/alpine branches;
  Docker enable/start is now init-aware (systemd OR OpenRC) so it no longer
  aborts on Alpine.

- Templated systemd unit (6.7): serverkit-backend.service → templates/
  serverkit-backend.service.in, rendered (render_service_unit) at install and
  in refresh_config so a custom SERVERKIT_DIR/venv/log path is honored.
  install_service warns-and-skips enable when has_systemd is false (G5).

- nginx TLS floor as a reversible conf.d snippet (6.8): install.sh + update.sh
  prefer /etc/nginx/conf.d/serverkit-tls.conf when nginx.conf doesn't already
  declare ssl_protocols (RHEL/Fedora/SUSE), falling back to the in-place edit
  on Debian/Ubuntu to avoid a 'duplicate ssl_protocols' error. uninstall
  removes only that snippet.

Tests: new scripts/test/test_lib.sh (13) + 4 new test_install.sh cases
(os_family_from, render_service_unit, harden_global_tls A/B). 13+15+10 pass;
shellcheck error+warning clean across all new/changed shell.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 00:46:38 -04:00
Juan DenisandClaude Opus 4.8 aa6ee2737f feat(update): Phase 2 — version-aware, dry-run-honest, rollback-safe updater
Updater robustness from the hardening plan §5:

- Version comparison before updating (G3): is_already_current compares the
  local VERSION to the target release tag (release mode) or the local checkout
  HEAD to the remote branch HEAD via `git ls-remote` (branch/main mode). When
  current, both the hybrid and docker flows print "Already up to date" and exit
  0 — unless --force. Offline/indeterminate always proceeds rather than wrongly
  blocking.

- Dry-run fidelity: new run_in_dir reports the working directory a command
  would run in, replacing the `run_or_dry cd X && run_or_dry compose ...` chain
  that printed compose commands with no cwd context.

- Rollback hardening: migrate_database now runs against the NEW slot's DB copy
  (slot-absolute path) instead of the /opt/serverkit symlink that still
  resolves to the live old slot — so a rollback hands the old code a
  byte-for-byte-unchanged database (PostgreSQL/shared DBs migrate directly, as
  before). rollback_docker guards `git reset` on a non-empty SHA with a clear
  warning. Both rollback paths now run a post-rollback health check and say
  plainly when even the rollback is unhealthy.

- Telemetry/logging: halt() always prints the full log path; both summaries
  gained an "old → new", mode (release/branch/main, +docker), and duration
  line, plus the log path.

Tests: test_update.sh gains versions_equal, is_already_current force/offline
short-circuits, and a flask-stub proof that migrate_database targets the new
slot's DB (T10). 10 passed / 2 skipped locally; shellcheck error+warning clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 00:31:03 -04:00
Juan DenisandClaude Opus 4.8 754bfb4a67 feat(install): Phase 1 hardening — firewall automation, py3.11/3.12, unified uninstall
Critical install/update/uninstall hotfixes from the hardening plan
(docs/plans/INSTALLER_UPDATER_UNINSTALLER_HARDENING_PLAN.md §4):

- Firewall automation (G1): new sourceable scripts/lib/firewall.sh detects
  firewalld/ufw/nftables/iptables and opens 80/443; install.sh runs
  configure_firewall, update.sh re-ensures it idempotently. Exactly what we
  open is recorded in /etc/serverkit/install-state.json (scripts/lib/state.sh)
  so uninstall undoes only our rules. Fixes the #1 "installed but unreachable"
  failure on fresh RHEL-family boxes.

- Accept Python 3.11 OR 3.12 (G2): locate_python now iterates
  python3.12/3.11/3 and provision_python installs the distro package
  (python3.11 on Debian 12, 3.12 on Ubuntu/Fedora, 3.11/3.12 on RHEL) before
  ever falling back to a source compile.

- Use $PYTHON_BIN for secret generation instead of a bare python3.

- Distro-first Node 20 (replacing unconditional NodeSource): install the
  distro nodejs+npm, fall back to NodeSource only when it's missing or < 18,
  and validate node>=18 before continuing.

- Unify uninstall (G4): new scripts/lib/uninstall.sh is the single canonical
  teardown routine shared by uninstall.sh and `serverkit uninstall` (they had
  drifted — one ran `down -v`, one didn't). Default preserves user data
  (volumes kept, DB snapshotted to backups); --purge/--delete-volumes removes
  volumes+DB+data dirs; --keep-data preserves /etc + /var/lib + backups;
  --yes/--dry-run added; honors SERVERKIT_DIR. Removes the apt lock-wait
  drop-in on uninstall.

Tests: new scripts/test/test_install.sh (11 cases, source-able install.sh)
covers python/node detection, firewall dry-run + state, and the uninstall
data-handling matrix. Existing test_update.sh still green. shellcheck clean
(error + warning) on all new/changed shell.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 00:24:39 -04:00
Juan DenisandClaude Opus 4.8 fef4d12d16 ci(scripts): run updater unit tests across Debian + Rocky
The updater's logic had zero test coverage on any distro, and ServerKit
targets several Linux families — so a portability regression only ever showed
up on a server. Add a matrix job that runs bash -n + scripts/test/test_update.sh
inside debian:12 and rockylinux:9 containers (Ubuntu is already covered by the
lint job). Verified locally: 9/9 pass on bash 5.2 (Debian) and 5.1 (Rocky).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 23:52:15 -04:00
Juan DenisandClaude Opus 4.8 33c2483f7e feat(update): self-updating bootstrap + concurrent-run lock
Two reliability gaps remained after the failure cascade:

1. Stale-updater trap. The updater that runs is whatever is installed, so an
   old update.sh fails in ways the current code already handles and can't fix
   itself — exactly what left a box stuck. maybe_reexec_latest_updater fetches
   the newest update.sh for the target ref and re-execs into it (guarded by
   SERVERKIT_UPDATER_REEXECED, skipped offline/dry-run/opt-out, only after a
   bash -n check). From this version on, 'serverkit update' is reliable no
   matter how stale the box is.

2. No concurrency guard. acquire_update_lock takes an flock so two updates
   can't clobber the same blue/green slot or race docker compose; the lock
   auto-releases on exit and degrades gracefully where flock/the path is
   unavailable.

Both are covered by scripts/test/test_update.sh (guard no-ops; lock
refuses a held lock, succeeds when free).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 23:52:09 -04:00
Juan DenisandClaude Opus 4.8 2549d4bbc7 feat(update): failure cascade + make the updater unit-testable
Harden update.sh against the class of failure that left a box silently stuck
on the old version, and make it testable without a real deploy.

Robustness (cascade):
- L5 loud failures: `set -Eeuo pipefail` (the -E/errtrace makes the ERR trap
  fire inside functions too) + a report_failure ERR trap that names the phase,
  exit code, line and command, plus always-on logging to
  /var/log/serverkit/update-*.log. No more silent drop-to-prompt.
- L4 docker-path rollback: snapshot the previous git SHA, :rollback image tags
  and dist.rollback before mutating; if the new backend is not healthy within
  90s, revert to the snapshot and halt (was: warn and continue).
- L6 self-heal: pin COMPOSE_PROJECT_NAME (kills the symlink-rename warning) and
  reclaim a stale inactive blue/green slot left by a prior failed hybrid run.

Testability:
- Parameterize the system dirs (SERVERKIT_NGINX_DIR / _LETSENCRYPT_DIR /
  _SYSTEMD_DIR / _CONFIG_DIR) and add SERVERKIT_NO_LOG, so refresh_config can
  run against fixtures instead of the host /etc.
- Make update.sh source-able via a BASH_SOURCE guard so functions can be
  exercised directly.
- scripts/test/test_update.sh: plain-bash unit tests (no deps, no server) that
  reproduce the missing-serverkit.conf regression and cover the
  detection/slot/report logic.
- .github/workflows/scripts-ci.yml: bash -n + shellcheck + the unit tests on
  changes to scripts/**, install.sh, serverkit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 23:34:52 -04:00
Juan DenisandClaude Opus 4.8 28b651f85d fix(update): stop refresh_config dying on a missing serverkit.conf
The panel-domain recovery greps /etc/nginx/sites-available/serverkit.conf,
but HTTP-only boxes never have that file. Under `set -euo pipefail` the
no-match/missing-file grep (exit 1/2) propagated and silently killed the
updater at the "Refreshing Configuration" phase — before the atomic switch
and before PREVIOUS_DIR is set, so no error and no rollback. The update
appeared to "do nothing" and the version never advanced.

Guard the command substitution with `|| true` (matching the idiom used a
few lines below for panel-domain).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 23:34:24 -04:00
Juan Denis a06960df0b Merge pull request #51 from jhd3197/dev
Refactor: unify core surfaces and overhaul the SCSS frontend
v1.7.1
2026-06-24 21:53:49 -04:00
github-actions[bot] 3476555996 chore: bump version to 1.7.1 [skip ci] 2026-06-25 01:47:14 +00:00
Juan DenisandClaude Opus 4.8 e1088aff71 feat(frontend): skeleton loaders, branded AppLoader, favicon refresh
Replace spinners with skeleton placeholders across page/section loaders
(EmptyState, DataTable, and ~page surfaces), add a reusable Skeleton
component + _skeleton.scss, rework AppLoader into a branded "server
powering on" loader, and refresh the favicon set (incl. SVG icon link).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 21:44:40 -04:00
Juan DenisandClaude Opus 4.8 b00fff7759 refactor(unify): §4 — one Observability namespace + tab group
Backend:
- The monitoring / metrics / telemetry(events) / uptime / fleet / status-page
  read surfaces are re-mounted under /api/v1/observability/<domain> as true
  aliases (same blueprints, distinct names). Original prefixes stay; the public
  status page route /api/v1/status/public/<slug> is untouched.

Frontend:
- The Monitoring tab group becomes "Observability": Monitoring · Events
  (the SystemEvent stream, formerly the standalone "Telemetry" page) · Status
  Pages, sharing one PageTopbar. /observability lands on Monitoring.
- The standalone Telemetry route + sidebar item are folded into the group
  (sidebar "Monitoring" → "Observability"; Events covered by matchPrefixes;
  removed from ADVANCED_ITEM_IDS). Command palette updated.
- Fleet Monitor stays under Servers (server-scoped) and GPU stays its own
  hardware page — intentional, per the plan.

Deferred (plan §4 deep merge, documented): the unified events/metrics/checks/
alerts/thresholds tables + collector migration into the job system. The
read-surface namespace + IA consolidation ship now; the schema merge is a large
data migration best done against a live metrics store.

Verified: 3 backend namespace tests + telemetry suite pass (22); frontend build
+ eslint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:58:55 -04:00
Juan DenisandClaude Opus 4.8 afd042896b refactor(unify): §3 — canonical DeploymentJob links + unified /deployments surface
Additive foundation + one read namespace for the three parallel deploy records
(versioned build Deployment, webhook GitDeployment, DeploymentJob executions).

Backend:
- DeploymentJob gains deployment_id / git_deployment_id / webhook_id FKs +
  commit_hash / image_tag / container_id, so it can be the canonical execution
  record pointing at the release ledgers it produces (surfaced in to_dict).
  Migration 039 (idempotent, additive).
- New /api/v1/deployments surface:
    GET /apps/:id/history?source=build|webhook|all  → federated, source-tagged
    GET /:id                                         → build Deployment detail
  and the DeploymentJob surface re-mounted at /api/v1/deployments/jobs (alias of
  /deployment-jobs). Per-system blueprints (/builds, /deploy, /git) unchanged.

Deliberately NOT done (plan §3 Phase 1/2 deep merge, with rationale): folding
GitDeployment into Deployment+DeploymentJob and async-ifying the currently
synchronous build/webhook deploys. Recon confirmed those paths are synchronous
with distinct failure/timeout semantics and GitDeployment carries rollback state
(snapshot_data, rolled_back_to_id); flipping them to the queue can't be verified
without a live build/runner, so it's deferred behind the additive links above.

Verified: 6 new tests + previews/config-snapshot regression pass (35).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:52:04 -04:00
Juan DenisandClaude Opus 4.8 92c435d436 refactor(unify): §7 — WordPress reachable through the generic /apps surface
Additive API consolidation (plan §7 Phases 3–4) that exposes a WordPress site
through its Application id, treating WordPressSite as the 1:1 extension table it
already is:
- GET  /apps/:id/wordpress            → {is_wordpress, site_id, env_type, ...}
- GET  /apps/:id/db-snapshots         → WP DB snapshots (was §8-deferred)
- POST /apps/:id/db-snapshots         → create
- DELETE /apps/:id/db-snapshots/:id   → delete
All grant-aware via the apps.py access seam (ResourceGrantService).

Deliberately NOT done (plan §7 Phase 1/2, with rationale): blanket-setting
app_type='wordpress' on WordPressSite-linked apps. Recon showed many WP sites are
app_type='docker', and 14+ docker-compose/nginx/scale/sleep code paths key on
app_type=='docker' — changing it would silently break start/stop/restart/logs/
status/domain config for containerized WP. The codebase already treats the
WordPressSite record (not app_type) as the source of truth (wordpress.py
_is_wp_app), which is the correct architecture; flipping app_type would regress
it. Frontend WordPress→Services merge likewise deferred (the /wordpress/:id route
is site-id-keyed).

Verified: 6 new surface tests + WP/resource-grant regression suites pass (23).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:46:14 -04:00
Juan DenisandClaude Opus 4.8 15fce59543 refactor(unify): §5 — single SSL surface (basic + advanced)
Unifies "SSL Certificates" and "Advanced SSL" into one lifecycle surface.

Backend:
- The advanced cert operations (wildcard DNS-01, SAN, custom upload, TLS
  profiles, health, expiry alerts) now mount under /api/v1/ssl alongside the
  basic certbot routes — no path collisions. /api/v1/ssl/advanced kept as a
  deprecated alias (same blueprint, second registration).

Frontend:
- api client advanced methods repointed to the unified /ssl/* paths.
- SSLCertificates.jsx (already the single SSL page) gains the two cert-creation
  paths that were previously UI-inaccessible: a "Wildcard (DNS-01)" mode in the
  New Certificate modal (Cloudflare token / Route 53 keys) and an "Upload"
  action + modal for bring-your-own PEM cert/key/chain.

Deferred (documented follow-up): a unified Certificate registry model + disk
scan, and surfacing health/profiles panels — the API is unified and the client
methods exist; these are read-side enhancements.

Verified: 3 backend route-unification tests + existing SSL suites pass (19);
frontend build + eslint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:40:08 -04:00
Juan DenisandClaude Opus 4.8 2cbbb61dbf refactor(unify): §6 Phase C6b — Postfix relay backed by EmailProviderConnection
Completes the email unification: the outbound relay is now one smtp
EmailProviderConnection flagged uses_relay, so a single table drives both the
Notification Bus and the Postfix relay. The legacy single-row email_relay_config
is folded in on first boot and kept only as a read fallback.

- EmailRelayService rewritten over the unified table: get/save/disable/test all
  operate on the uses_relay smtp connection; password stays Fernet-encrypted in
  credentials_json; masked round-trips preserve the stored secret. Postfix apply
  unchanged (no-op when Postfix absent).
- migrate_legacy_config(): idempotent fold of email_relay_config → connection
  (password copied verbatim — same Fernet key); wired into the boot-time legacy
  migration block.
- API: PUT /notifications/admin/email-providers/:id toggles usage flags
  (uses_notifications/uses_relay/relay_priority); uses_relay honored for SMTP only.

Verified: 6 new relay tests + usage-flag + notification-bus suites pass (30 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:36:08 -04:00
Juan DenisandClaude Opus 4.8 45e01f8731 refactor(unify): §6 Phase C6a — email provider usage flags
Foundation for folding the single-row email_relay_config into the multi-row
EmailProviderConnection so one table drives notifications and the Postfix relay.

- EmailProviderConnection gains uses_notifications (default true), uses_relay
  (default false), relay_priority (default 0); surfaced in to_dict.
- Notification selection (default_provider) now restricted to uses_notifications
  rows, so relay-only connections are never used to send notifications.
- New relay_provider() selects the active SMTP connection by relay_priority.
- add_provider() accepts the flags; uses_relay honored only for provider=smtp
  (API providers can't be a Postfix smarthost).
- Migration 038 (idempotent additive columns).

Additive — existing providers default to uses_notifications=true / no relay.
Verified: 4 new tests + notification-bus suite pass (24 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:32:41 -04:00
Juan DenisandClaude Opus 4.8 45e83e9a09 refactor(unify): §8 Phase B5 — generic backup-policy CRUD for every target
Completes the §8 API consolidation: one admin surface drives a BackupPolicy for
ANY target — application, wordpress_site, database, files, server — by
(target_type, target_id):

  GET/PUT  /api/v1/backups/policies/<type>/<id>          (PUT accepts
           target_subtype + target_meta to describe database/files targets)
  POST     /api/v1/backups/policies/<type>/<id>/run
  GET      /api/v1/backups/policies/<type>/<id>/runs
  POST     .../runs/<run_id>/restore  |  /verify
  DELETE   .../runs/<run_id>

This is the only way to drive the database/files/server targets (which have no
per-resource page). The grant-aware per-resource mounts
(/apps/:id/backup-policy, /wordpress/sites/:id/backup-policy) are unchanged.

Deferred to §7 (WordPress-as-app): surfacing WP DatabaseSnapshot under
/apps/:id/db-snapshots, and the frontend Backups.jsx rewrite, both of which
depend on the app↔site unification.

Verified: 5 new tests; full backup/snapshot suite green (63 passed, 2 skipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:27:08 -04:00
Juan DenisandClaude Opus 4.8 d87e69b44f refactor(unify): §8 Phase B4 — surface DeploymentSnapshot as "Config Checkpoint"
Recorded §8 decision: a deployment config snapshot is shown as a "Config
Checkpoint" in the UI to end the overloaded "snapshot" wording. Distinct from
data-protection backups and from WordPress DB snapshots, which are untouched.

Backend:
- snapshots blueprint serves every route under /apps/:id/config-checkpoints
  in addition to /apps/:id/snapshots (dual @route — both resolve to one view).

Frontend:
- DeployTab section "Deployment Timeline" → "Config Checkpoints" with reworded
  hint; ConfigDiffModal + DeploymentTimeline empty/peer copy use "checkpoint".
- api/snapshots.js gains getConfigCheckpoint(s)/Diff/restore aliases.
- ProtectionStatusCard: "Snapshots run on the configured schedule" → "Backups"
  (that card describes backups, not config checkpoints).

Verified: 2 alias tests + existing config-snapshot suite pass; frontend build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:24:13 -04:00
Juan DenisandClaude Opus 4.8 b90f6669d2 refactor(unify): §8 Phase B3 — global backup policy/run listing API
Adds the cross-target queries the unified Backups page needs:
- GET /api/v1/backups/policies  (filters: target_type, enabled) — replaces the
  legacy filesystem schedule list with one view of every BackupPolicy.
- GET /api/v1/backups/runs (filters: target_type, status, policy_id, limit≤500)
  — every BackupRun across all policies, each augmented with its target's
  type/id/subtype so the UI filters without a second lookup.

Both admin-guarded like the rest of /backups.
Verified: 6 new API tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:20:43 -04:00
Juan DenisandClaude Opus 4.8 f357742d80 refactor(unify): §8 Phase B2 — database + files executors in policy service
Folds the legacy BackupService database/files capabilities into the unified
BackupPolicyService so one policy/run system performs every backup kind.

- _resolve_target() now handles database (connection descriptor from
  target_meta + target_subtype engine), files (path list), and rejects
  server (not yet implemented) with a clear message.
- _execute_backup() routes database → BackupService.backup_database and
  files → BackupService.backup_files, building consistent run metadata
  (kind=full, includes, primary_archive).
- Restore: _restore_database (symmetric with restore_database) and
  _restore_files (safe extract into a restores/ staging dir, never blind
  overwrite of a basename-only path-list archive); run_restore_job dispatches
  by target_type.

Note: remote-copy upload still applies to dir-based runs (app/WP); single-file
database/files runs are local-only for now (documented follow-up).

Verified: 8 new executor tests (BackupService mocked) + B1 + existing
backup-protection suite pass (26 passed, 2 skipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:19:32 -04:00
Juan DenisandClaude Opus 4.8 5d606a6030 refactor(unify): §8 Phase B1 — generalize BackupPolicy to all target types
First slice of the backups/snapshots unification. Makes the policy-driven
BackupPolicy/BackupRun system able to describe every data-protection target,
not just application/wordpress_site — the foundation for folding the legacy
BackupService (database/files) into one system.

- BackupPolicy gains target_subtype + target_meta_json (+ get/set helpers,
  surfaced in to_dict). VALID_TARGET_TYPES = application, wordpress_site,
  database, files, server.
- BackupPolicyService.validate_target_type(); get_or_create_policy() accepts
  and (idempotently) refreshes target_subtype/target_meta for non-resource
  targets like database/files.
- Migration 037 (idempotent, additive columns).

Additive only — no behavior change for existing app/WP policies.
Verified: 6 new tests + existing backup-protection suite pass (18 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:16:45 -04:00
Juan DenisandClaude Opus 4.8 8c57636455 refactor(unify): §2 — rename WordPress "Projects" → "Pipelines"
Feature-unification plan §2. Ends the naming collision between generic
/projects (workspace organization) and WordPress "Projects" (environment
pipelines). Label + URL change only; deeper WP/app unification is §7.

Frontend:
- Pipeline list moves to /wordpress/pipelines; detail to
  /wordpress/pipelines/:id[/:tab]. WORDPRESS_TABS, command palette, page
  titles, breadcrumbs, empty-state copy, and the WordPressProject tab base
  all updated to "Pipelines".
- Legacy /wordpress/projects[...] URLs redirect to the pipelines space via
  LegacyWpPipelineRedirect (deep links preserved).
- wordpressApi.getPipelines() added; getProjects() kept as back-compat alias.

Backend:
- environment_pipeline_bp mounted a second time under
  /api/v1/wordpress/pipelines as a true alias of /wordpress/projects.
- Test: /pipelines resolves identically to /projects.

Verified: frontend build clean, 5 alias tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:09:35 -04:00
Juan DenisandClaude Opus 4.8 3f18a0ebd3 refactor(unify): §1 — single Services detail surface; /apps→/services
Feature-unification plan §1 (Services UI = Applications backend). Ends the
"Applications vs Services" terminology split by collapsing the two detail
pages into one canonical surface and aliasing the backend.

Frontend:
- Merge ApplicationDetail's exclusive tabs (Container Ops, WAF, Build, Deploy,
  Previews) into the canonical ServiceDetail (PageTopbar chrome). Type-gating
  preserved exactly: ops=Docker, waf=Docker+Python, build/deploy/previews=all.
- serviceTypes.js tab lists extended for the merged tabs.
- /apps/:id and /apps/:id/:tab now redirect to /services/:id[/:tab] via
  LegacyAppRedirect (deep links preserved); ApplicationDetail.jsx deleted.
- Drop the /apps page-title entry and "Application Details" dynamic title.
- api.getServices()/getService() aliases over getApps()/getApp().

Backend:
- Mount apps_bp a second time under /api/v1/services as a true alias of
  /api/v1/apps (Flask blueprint re-registration with name='services').
- Test: /api/v1/services resolves identically to /api/v1/apps.

Verified: frontend build clean, eslint 0 errors, 3 alias tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:06:05 -04:00
Juan Denis a283df0cf1 Split Secrets & Webhooks into Vaults and Webhooks
Refactor UI to split the legacy "Secrets & Webhooks" page into two focused pages: Vaults (organization tab) and Webhooks (standalone page). Added new pages frontend/src/pages/Vaults.jsx and frontend/src/pages/Webhooks.jsx, removed frontend/src/pages/SecretsWebhooks.jsx, and updated routes in App.jsx to register /vaults and /webhooks while redirecting legacy /secrets routes to the new locations. Updated CommandPalette, organization tabs, and sidebar items to reflect Vaults/Webhooks and adjusted ADVANCED_ITEM_IDS. UI comments explain intent and migration. This keeps secret storage under Organization and makes webhooks a dedicated console.
2026-06-24 13:56:01 -04:00
Juan DenisandClaude Opus 4.8 a4135de92d fix(frontend): Phase 5 §8.5 — narrow the legacy-.card lint rule to stop false positives
The no-restricted-syntax `.card` selector used `\bcard\b`, which flagged any
className containing "card" as a word — including unrelated compound classes like
`settings-card`, `sk-spec-card`, `auth-card`, and `wp-site-card-skeleton`, none of
which are the legacy `.card` class the rule's message targets. Narrowed it to the
legacy card family as a LEADING token (`card`, `card-header`, `card-body`, …),
removing ~220 false-positive warnings while still flagging real `.card` usage
(225 remain). Frontend ESLint warnings: 1616 (plan baseline) → 1190; 0 errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 10:50:30 -04:00
Juan DenisandClaude Opus 4.8 bdffd91361 chore(frontend): Phase 5 §8.5 — add pre-commit ESLint guard
Adds a shared git hook (.githooks/pre-commit) that lints staged frontend JS/JSX
and blocks the commit on ESLint ERRORS only (warnings allowed). It exits early
for commits with no frontend JS/JSX (backend, SCSS, docs), so it never disrupts
those. Enable once per clone: `git config core.hooksPath .githooks` (or
`sh frontend/setup-hooks.sh`).

Chose a committed .githooks script over husky/lint-staged: this is a monorepo
(git root has the Python backend; only frontend/ is a node package with no root
package.json), so a dependency-free hook avoids an npm-install-time git-config
mutation and keeps backend committers unaffected. Documented in AGENTS.md.

Tested: blocks a staged file with a parse error (exit 123), passes when clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 10:46:28 -04:00
Juan DenisandClaude Opus 4.8 b70277ef67 fix(frontend): Phase 5 §8.2 A3 — add type="button" to raw action buttons
Raw <button> elements default to type="submit" inside a form, causing accidental
submits. Added an explicit type="button" to the 234 raw buttons (across 60
files) that have an onClick and no type — i.e. action buttons, which are never
the intended default-submit. Default-submit buttons (no onClick) and the shadcn
ui/* wrappers were left untouched. Applied via a brace/quote-aware scan so
multi-line tags and style={{...}} attrs are handled correctly.

Verified: npm run build passes; dashboard/services/settings screenshots render
unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 10:42:47 -04:00
Juan DenisandClaude Opus 4.8 fd2a3cadd6 refactor(frontend): Phase 3 §6.4/§6.2 — migrate WordPress list to ResourceListPage
Second ResourceListPage consumer (the page Services' chrome was copied from).
Replaced WordPress.jsx's duplicated .wp-list toolbar/bulk-bar + inline sk-dtable
table with ResourceListPage; the table is now DataTable columns. Added a `header`
slot to ResourceListPage for the one-time credentials banner; the tag-filter
chips go through `toolbarExtra`. Kept the custom loading skeleton and lite-tier
ResourceGate as early returns, and the Import/Create modals as children. All
behavior preserved (filter chips, search, tag filter, select-all + per-row
checkboxes, bulk ops, open/wp-admin links, row-click routing).

Verified: npm run build passes, 0 lint errors; WordPress list screenshot (mock
data) renders identically to before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 10:33:56 -04:00
Juan DenisandClaude Opus 4.8 972849dc6c refactor(frontend): Phase 3 §6.4/§6.2 — ResourceListPage + migrate Services to DataTable
Per docs/plans/FRONTEND_UX_TAILWIND_CLEANUP_PLAN.md §6.4 (ResourceListPage) and
§6.2 (table → DataTable).

- Add components/layouts/ResourceListPage.jsx: the shared list-page chrome —
  status-filter SegControl + search toolbar, bulk-actions bar, the DataTable, and
  the loading / empty / filtered-empty states. Markup mirrors the existing
  .wp-list design so SCSS applies unchanged.
- Services.jsx: replaced its hand-rolled .wp-list chrome + inline sk-dtable table
  with ResourceListPage. The table is now DataTable columns; interactive cells
  (checkbox, row actions) stop click propagation so they don't trigger the row
  navigate. All behavior preserved (filter chips with counts, search, select-all
  + per-row checkboxes, bulk bar, source/status/env cells, row-click routing).

Verified: npm run build passes, 0 lint errors; Services screenshot (mock data)
renders identically to before (also fixed the section's stale wait-selector).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 10:28:51 -04:00
Juan DenisandClaude Opus 4.8 ecde757115 refactor(frontend): Phase 4 §7.3 — split ServerDetail/ApplicationDetail/WordPressDetail monoliths
Per docs/plans/FRONTEND_UX_TAILWIND_CLEANUP_PLAN.md §7.3, following the Docker
split. Pure move-plus-imports refactors (no logic/JSX/state changes); the tabs
were already top-level + prop-driven, so the build validates every import.

- ServerDetail.jsx  2459 → 390 lines → components/serverdetail/ (serverDetailShared
  + ServerOverviewTab/AlertsTab/ServerDockerTab/CronTab/CloudflaredTab/
  ServerMetricsTab/ServerSettingsTab; Server* prefixes avoid collisions with the
  folder's existing PackagesTab/ServicesTab/SystemStatusCard).
- ApplicationDetail.jsx  1737 → 275 lines → new components/appdetail/ (appDetailShared
  + Overview/Packages/Gunicorn/Commands/Build/Logs/Settings/DeployTab).
- WordPressDetail.jsx  3992 → 464 lines → new components/wordpress/detail/
  (wpDetailShared + 15 tab files; cross-tab modals re-exported, e.g. OverviewTab
  imports CreateEnvironmentModal/SearchReplaceModal from their owning tabs).

Verified: combined npm run build passes, 0 lint errors; rendered all three detail
shells under Playwright (mock data) — header, full tab nav, and Overview content
render correctly (no pageerror/error-boundary). Combined with the build (which
resolves every tab import) this confirms the shell→tab wiring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 10:09:43 -04:00
Juan DenisandClaude Opus 4.8 019fdb2cfe refactor(frontend): Phase 4 §7.3 — split Docker.jsx monolith into per-tab files
Per docs/plans/FRONTEND_UX_TAILWIND_CLEANUP_PLAN.md §7.3. Pure refactor (move +
imports only, no logic change). Docker.jsx goes from 2521 → 334 lines (a thin
shell: status/server/stats/tab state + layout). Extracted to components/docker/:
- dockerHelpers.js  — ServerContext/useServer + the pure container helpers/constants
- dockerShared.jsx  — IconAction, TrashIcon, ContainerResourceBars, DownloadIcon
- ContainersTab.jsx — ContainersTab + ContainerInspector + Run button/modal + logs/exec modals
- ComposeTab.jsx, ImagesTab.jsx, NetworksTab.jsx, VolumesTab.jsx, PruneButton.jsx
The tabs were already decoupled (shared ServerContext + onStatsChange prop, no
closures over the shell), so the split is mechanical.

Verified: npm run build passes, 0 lint errors, and drove ALL FIVE tabs
(Containers/Compose/Images/Networks/Volumes) open under Playwright with mock data
— each renders correctly (tables, status/port pills, resource bars, row actions,
toolbar buttons) identical to pre-split.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 09:39:22 -04:00
Juan DenisandClaude Opus 4.8 bf21504358 refactor(frontend): Phase 3 §6.1 — migrate the large-page modals to shared Modal
Converts the remaining hand-rolled .modal-overlay modals in the big pages to the
shared <Modal>: Docker (6: compose logs, run, exec, pull, create network/volume),
WordPressDetail (5: clone, search-replace, delete, create env, snapshot),
Domains (2: add domain, enable SSL), Servers (1: manage groups),
ApplicationDetail (2: build config, deploy config), WordPressProject (1: create
env). Same pattern: forms+actions in the body, sizes mapped, close-animation
null-guards where the body derefs open-state.

Only 2 .modal-overlay remain, both intentionally left — they are custom
right-side slide-in DRAWERS (Servers AddServerModal/.server-setup-modal and the
Templates detail panel) that need the Drawer primitive, not a centered Modal.

Verified: npm run build passes (each agent built independently + final combined
build).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 03:16:53 -04:00
Juan DenisandClaude Opus 4.8 f3fd9c5e76 refactor(frontend): Phase 3 §6.1 — migrate ~37 hand-rolled modals to shared Modal
Per docs/plans/FRONTEND_UX_TAILWIND_CLEANUP_PLAN.md §6.1. Converted the
.modal-overlay/.modal boilerplate to the shared Radix-backed <Modal> (focus
trap, Escape, backdrop, built-in close) across 20 files: AttachDomainModal,
ChangeUrlModal, ConfigDiffModal (xl), SettingsTab, Templates, WordPress (lg),
Workspaces, WorkspaceDetail, StatusPages, ServerTemplates, CloudProvision,
CronJobs, SSLCertificates, FTPServer, Email, FileManager, Git (lg/xl), Backups,
QueueDetail, QueueOperations. Forms keep their submit button + .modal-actions in
the Modal body; bodies that deref open-state objects are guarded for the close
animation; sizes mapped from old .modal-lg/.modal-xl.

Also hoisted .status-modal/.status-modal-grid out of .status-pages-page in
_status-pages.scss — the shared Modal portals to <body>, so page-scoped rules
would not have reached it.

Left intentionally: the Templates detail panel (custom slide-in drawer) and
ProjectDetail (already uses the Radix Dialog primitive directly).

Verified: npm run build passes; databases-modal pilot confirmed rendering via
Playwright; eslint adds no new errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 03:12:26 -04:00
Juan DenisandClaude Opus 4.8 a877e49585 refactor(frontend): Phase 3 §6.1 — migrate databases modals to shared Modal
Per docs/plans/FRONTEND_UX_TAILWIND_CLEANUP_PLAN.md §6.1 (priority #1 cluster).
Converted the 5 hand-rolled .modal-overlay modals in databases/modals.jsx
(CredentialsResult + Create MySQL/PostgreSQL Database/User) to the shared
components/Modal.jsx (Radix-backed: focus trap, Escape, backdrop). The form +
.modal-actions stay in the Modal body so the type=submit button remains inside
<form>. Logic unchanged.

Verified: npm run build passes; drove the Create MySQL database modal open under
Playwright (mock data) — renders correctly via .sk-modal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 03:00:51 -04:00
Juan DenisandClaude Opus 4.8 9c0109b880 refactor(frontend): Phase 5 §8.4 — Settings account/admin switch uses SegControl
Per docs/plans/FRONTEND_UX_TAILWIND_CLEANUP_PLAN.md §8.4. Replaced the hand-rolled
two-<button> role=tablist 'My Account / Admin' switch in Settings.jsx with the
shared components/ds/SegControl, so settings uses the same segmented-control
primitive as the rest of the app.

Verified: npm run build passes; settings screenshot confirms the segmented
control renders correctly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 02:54:29 -04:00
Juan DenisandClaude Opus 4.8 3e1ca209fe fix(frontend): sanitize sidebar icon SVG in SidebarSettings too
Follow-up to the Sidebar XSS fix (c3eb51f) — the Sidebar customization preview
in SidebarSettings.jsx injected item.icon via dangerouslySetInnerHTML without
sanitizing. Routed it through sanitizeSvgInner like the main Sidebar. (The
SqlEditor and file-preview highlighters were checked and already escape-first,
so A5/A6 need no change.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 02:52:31 -04:00
Juan DenisandClaude Opus 4.8 3fcb128cb3 refactor(frontend): Phase 3 §6.6 — remove the useConfirm-driven local ConfirmDialogs
Follows the ConfirmProvider checkpoint. Removed the now-inert per-page
<ConfirmDialog> instances that were driven by useConfirm's confirmState across
18 files (ApplicationDetail, Docker, ServerDetail, Terminal, WordPressDetail,
Backups, CronJobs, Databases, Queue*, SSLCertificates, ServiceDetail,
ContainerOpsPanel, ProtectionPanel, BackupsTab, PreviewList, PackagesTab,
MigrationHistoryTab); simplified each useConfirm() destructure to { confirm } and
dropped the unused ConfirmDialog imports. confirm() now renders only via the
app-level provider.

~20 other files keep their own local-useState-controlled ConfirmDialogs (not
useConfirm-driven) — intentionally left; migrating those is a separate behavior
change.

Verified: npm run build passes; no dangling confirmState refs; Docker page
renders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 02:50:59 -04:00
Juan DenisandClaude Opus 4.8 6a0d57587a refactor(frontend): Phase 3 §6.6 — add ConfirmProvider (app-level confirm dialog)
Per docs/plans/FRONTEND_UX_TAILWIND_CLEANUP_PLAN.md §6.6. Adds a single
app-level <ConfirmDialog> via ConfirmProvider (wraps the dashboard). useConfirm
is now context-aware: with the provider present, confirm() routes to the one
dialog and the legacy confirmState/handleConfirm/handleCancel become inert
no-ops (so the 39 pages' own <ConfirmDialog> render closed/harmless until
removed). Falls back to local state outside a provider so nothing breaks.

Backward-compatible checkpoint; the per-page dialog removal follows.

Verified: npm run build passes; 0 lint errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 02:46:33 -04:00
Juan DenisandClaude Opus 4.8 ad9acb89e3 refactor(frontend): Phase 5 §8.5 — drop 142 unused React imports (JSX transform)
Per docs/plans/FRONTEND_UX_TAILWIND_CLEANUP_PLAN.md §8.5. React 18's automatic
JSX runtime makes 'import React' unnecessary; removed it from the 142 files
eslint flagged as 'React defined but never used' (named imports like useState
preserved). Driven by an eslint --format json pass so only flagged files were
touched.

Lint warnings: 1603 -> 1461 (0 errors). Verified: npm run build passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 02:44:16 -04:00
Juan DenisandClaude Opus 4.8 c3eb51f3d2 refactor(frontend): Phase 5 §8.2 — sanitize sidebar SVG icons + AI shortcut a11y
Per docs/plans/FRONTEND_UX_TAILWIND_CLEANUP_PLAN.md §8.2 (A1/A8, A2).
- Sidebar icons were injected via dangerouslySetInnerHTML. Core icons are
  trusted constants, but plugin-contributed nav items flow through the same path
  and could supply arbitrary SVG. Added utils/sanitizeSvg.js: an allowlist
  sanitizer (DOMParser-based, caches results) that keeps only safe SVG shape
  elements/attributes and drops scripts/event handlers/external refs. Wired both
  Sidebar icon sites through it — zero visual change (verified via screenshot).
- AI Alt+A shortcut: added a visible title tooltip + aria-keyshortcuts on the
  chat bubble (discoverable), and made the global handler skippable via
  localStorage 'sk_ai_shortcut' = 'off'.

Verified: npm run build passes; 0 lint errors; dashboard screenshot confirms
sidebar icons still render.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 02:40:43 -04:00
Juan DenisandClaude Opus 4.8 d6b631ec34 refactor(frontend): Phase 4 §7.2 — disambiguate the two WafPanel components
Per docs/plans/FRONTEND_UX_TAILWIND_CLEANUP_PLAN.md. Two different components
were both named WafPanel.jsx (app-level vs Cloudflare-zone WAF), a real source
of confusion. Renamed:
- components/apps/WafPanel.jsx -> AppWafPanel.jsx
- components/cloudflare/WafPanel.jsx -> CloudflareWafPanel.jsx
Updated the single importer of each (ApplicationDetail, CloudflareZoneSettings).

Verified: npm run build passes; no stray WafPanel references remain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 02:32:27 -04:00
Juan DenisandClaude Opus 4.8 360d268fe5 refactor(frontend): Phase 3 §6.5 — replace ad-hoc loading divs with EmptyState
Per docs/plans/FRONTEND_UX_TAILWIND_CLEANUP_PLAN.md (acceptance: no
<div className="loading"> remains). Replaced all 14 ad-hoc loading divs across
13 files with the shared <EmptyState loading title="..." /> (centered spinner +
label), preserving each message verbatim. EmptyState was already imported in
Docker/Services; added the default import elsewhere.

Verified: npm run build passes; grep for className="loading" returns nothing;
eslint adds no new errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 02:30:24 -04:00
Juan DenisandClaude Opus 4.8 b872b9def0 refactor(frontend): Phase 4 §7.4 — sync command palette routes
Per docs/plans/FRONTEND_UX_TAILWIND_CLEANUP_PLAN.md. STATIC_PAGES had drifted
from the real route table:
- Drop the 'DNS Zones' /dns entry (it redirects to /domains); fold its
  dns/records/nameserver/zones keywords into the Domains entry.
- Add the missing pages: Projects, Shared Variables, Workspaces, GPU Monitor,
  Queue, Jobs, Telemetry, Secrets (all verified present in App.jsx).

Verified: npm run build passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 02:26:26 -04:00
Juan DenisandClaude Opus 4.8 9bd6d18f2c refactor(frontend): Phase 5 §8.3/§8.2 — useLockBodyScroll hook + markdown link hardening
Per docs/plans/FRONTEND_UX_TAILWIND_CLEANUP_PLAN.md Phase 5.
- Add hooks/useLockBodyScroll.js: a ref-counted body-scroll lock that composes
  nested locks correctly (a modal over an already-locked drawer no longer
  restores scroll prematurely). Replaces the duplicated
  document.body.style.overflow toggles in DashboardLayout (mobile nav drawer),
  PreviewDrawer (file preview), and ChatDrawer (mobile AI sheet).
- lib/ai/markdown.js: escape quotes in link URLs. The renderer already
  HTML-escapes all input and only emits allowlisted tags, but `"`/`'` in a
  crafted URL could break out of the href attribute — now percent-encoded.

Verified: npm run build passes; eslint adds no new errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 02:24:27 -04:00
Juan DenisandClaude Opus 4.8 dcf9365ff3 refactor(frontend): Phase 4 §7.1 — delete dead redirected pages
Per docs/plans/FRONTEND_UX_TAILWIND_CLEANUP_PLAN.md Phase 4.
- pages/Applications.jsx: orphaned. /apps already redirects to /services and
  /apps/:id renders ApplicationDetail; nothing imported this 481-line page.
- pages/Firewall.jsx: orphaned. /firewall already redirects to
  /security/firewall; the live UI is components/security/FirewallTab.jsx.
- components/status/ContainerStatusPill.jsx: only consumed by dead Applications.

Kept _applications.scss and _firewall.scss — verified both are shared by live
components (ApplicationDetail/ServiceDetail/ContainerOpsPanel and FirewallTab),
so not orphaned. App.jsx route redirects retained so bookmarks don't 404.

Verified: npm run build passes; no remaining references to deleted files.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 02:20:53 -04:00
Juan DenisandClaude Opus 4.8 064e00c7f0 refactor(frontend): Phase 3 §6.3 — consolidate byte/relative-time formatters
Per docs/plans/FRONTEND_UX_TAILWIND_CLEANUP_PLAN.md Phase 3 utility consolidation.
Replaces the scattered, drifted-apart local helpers with the canonical utils
added in Phase 1.

formatBytes (was 14 local copies):
- utils/formatBytes.js realigned to the codebase convention (1024 divisor +
  KB/MB/GB labels; `iec` opt switches labels to KiB/MiB). It was unused until
  now, so displayed values are preserved.
- Removed local formatBytes/formatSize/formatMemory from ApplicationDetail,
  Backups, Databases, Monitoring, Terminal, ServerDetail (x2), BackupsTab,
  QuarantineTab, SystemTab, DiskUsageBar, SnapshotTable. Decimals matched per
  call site (e.g. BackupsTab keeps 2 decimals); empty/falsy fallbacks preserved
  via defaultValue. fileTypes.js and logHelpers.js now re-export the canonical
  (their named exports are consumed elsewhere).

formatRelativeTime (was 6 local copies):
- ActivityFeed, PipelineView (x2), Dashboard migrated to utils/time.js.
- serviceTypes.js and logHelpers.js re-export the canonical.
- EnvironmentCard left as-is (intentionally distinct "Today/Yesterday/X days
  ago" vocabulary, not the "Xm ago" format).

Behavioral deltas are cosmetic only: trailing-zero trim ("1.0 GB"->"1 GB"), the
date-fallback threshold normalized to 30d, and sub-hour times now show precise
"Xm ago". Units and numeric values are unchanged.

Verified: npm run build passes; eslint adds no new errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 02:17:44 -04:00