diff --git a/AGENTS.md b/AGENTS.md index a6db2545..2992b710 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,7 @@ # GitNexus — Code Intelligence -This project is indexed by GitNexus as **cameleer-server** (8893 symbols, 23049 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **cameleer-server** (9095 symbols, 23495 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/CLAUDE.md b/CLAUDE.md index 3f9831a0..0b75e758 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -85,7 +85,7 @@ When adding, removing, or renaming classes, controllers, endpoints, UI component # GitNexus — Code Intelligence -This project is indexed by GitNexus as **cameleer-server** (8893 symbols, 23049 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **cameleer-server** (9095 symbols, 23495 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/docs/handoff/2026-04-23-deployment-page-handoff.md b/docs/handoff/2026-04-23-deployment-page-handoff.md new file mode 100644 index 00000000..78b5c6e8 --- /dev/null +++ b/docs/handoff/2026-04-23-deployment-page-handoff.md @@ -0,0 +1,183 @@ +# Handoff — Unified App Deployment Page + +**Session:** 2026-04-22 → 2026-04-23 +**Branch:** `main` (43 commits ahead of `origin/main` before push — all committed directly per explicit user consent) +**Base commit (session start):** `1a376eb2` +**Head commit (session end):** `0a71bca7` + +## What landed + +Full implementation of the unified app deployment page replacing the old `CreateAppView` / `AppDetailView` split. Key artefacts: + +- **Spec:** `docs/superpowers/specs/2026-04-22-app-deployment-page-design.md` +- **Plan:** `docs/superpowers/plans/2026-04-22-app-deployment-page.md` +- **Routes:** `/apps` (list, unchanged), `/apps/new` + `/apps/:slug` (both render new `AppDeploymentPage`) + +### Backend delivered (cameleer-server) + +- Flyway V3 adds `deployments.deployed_config_snapshot JSONB` +- `DeploymentConfigSnapshot` record: `(UUID jarVersionId, ApplicationConfig agentConfig, Map containerConfig, List sensitiveKeys)` +- `DeploymentExecutor` captures snapshot on successful RUNNING transition (not FAILED) +- `PostgresDeploymentRepository.saveDeployedConfigSnapshot(UUID, DeploymentConfigSnapshot)` + `findLatestSuccessfulByAppAndEnv(appId, envId)` +- `ApplicationConfigController.updateConfig` accepts `?apply=staged|live` (default `live` for back-compat); staged skips SSE push; 400 on unknown +- `AppController.getDirtyState` → `GET /api/v1/environments/{envSlug}/apps/{appSlug}/dirty-state` returning `{dirty, lastSuccessfulDeploymentId, differences[]}` +- `DirtyStateCalculator` pure service (cameleer-server-core), scrubs volatile fields (`version`, `updatedAt`, `updatedBy`, `environment`, `application`) from agent-config comparison, recurses into nested objects +- Integration tests: `PostgresDeploymentRepositoryIT` (3), `DeploymentSnapshotIT` (2), `ApplicationConfigControllerIT` (6), `AppDirtyStateIT` (3), `DirtyStateCalculatorTest` (9) +- OpenAPI + `schema.d.ts` regenerated + +### UI delivered (cameleer-server/ui) + +New directory `ui/src/pages/AppsTab/AppDeploymentPage/`: + +``` +index.tsx # Main composition (524 lines) +IdentitySection.tsx # Name + slug + env pill + JAR + Current Version +Checkpoints.tsx # Collapsible disclosure of past successful deploys +PrimaryActionButton.tsx # Save / Redeploy / Deploying… state machine +AppDeploymentPage.module.css # Page-local styles +ConfigTabs/ + MonitoringTab.tsx # Engine, payload, log levels, metrics, sampling, replay, route control + ResourcesTab.tsx # CPU / memory / ports / replicas / runtime / networks + VariablesTab.tsx # Env vars (Table / Properties / YAML / .env via EnvEditor) + SensitiveKeysTab.tsx # Per-app keys + global baseline reference + TracesTapsTab.tsx # Live-apply with LiveBanner + RouteRecordingTab.tsx # Live-apply with LiveBanner + LiveBanner.tsx # Shared amber "changes apply immediately" banner +DeploymentTab/ + DeploymentTab.tsx # Composition: StatusCard + DeploymentProgress + StartupLogPanel + History + StatusCard.tsx # RUNNING / STARTING / FAILED indicator + replica count + URL + actions + HistoryDisclosure.tsx # Past deployments table with inline log expansion +hooks/ + useDeploymentPageState.ts # Form-state orchestrator (monitoring, resources, variables, sensitiveKeys) + useFormDirty.ts # Per-tab dirty computation via JSON.stringify compare + useUnsavedChangesBlocker.ts # React Router v6 useBlocker + DS AlertDialog +utils/ + deriveAppName.ts # Filename → app name pure function + deriveAppName.test.ts # 9 Vitest cases +``` + +Touched shared files: +- `ui/src/components/StartupLogPanel.tsx` — accepts `className`, flex-grows in container (dropped fixed 300px maxHeight) +- `ui/src/api/queries/admin/apps.ts` — added `useDirtyState`, `Deployment.deployedConfigSnapshot` type +- `ui/src/api/queries/commands.ts` — `useUpdateApplicationConfig` accepts `apply?: 'staged' | 'live'` +- `ui/src/router.tsx` — routes `/apps/new` and `/apps/:appId` to `AppDeploymentPage` +- `ui/src/pages/AppsTab/AppsTab.tsx` — shrunk 1387 → 109 lines (list only) + +### Docs delivered + +- `.claude/rules/ui.md` — Deployments bullet rewritten for the unified page +- `.claude/rules/app-classes.md` — `ApplicationConfigController` gains `?apply` note; `AppController` gains dirty-state endpoint; `PostgresDeploymentRepository` notes the snapshot column +- `docs/superpowers/specs/2026-04-22-app-deployment-page-design.md` +- `docs/superpowers/plans/2026-04-22-app-deployment-page.md` + +## Gitea issues opened this session (cameleer/cameleer-server) + +### [#147 — Concurrent-edit protection on app deployment page (optimistic locking)](https://gitea.siegeln.net/cameleer/cameleer-server/issues/147) +Deferred during brainstorming. Two browser sessions editing the same app have no last-write-wins protection. Proposed fix is `If-Match` / `ETag` on config + container-config + JAR upload endpoints using `app.updated_at`. Not blocking single-operator use. + +### [#148 — Persist deployment-page monitoring fields end-to-end](https://gitea.siegeln.net/cameleer/cameleer-server/issues/148) +**Important.** The Monitoring tab renders five controls that are currently **UI-only**: `payloadSize` + `payloadUnit`, `metricsInterval`, `replayEnabled`, `routeControlEnabled`. They do not persist to the agent because the fields don't exist on `com.cameleer.common.model.ApplicationConfig` and aren't part of the agent protocol. The old `CreateAppView` had the same gap — this is not a new regression, but the user has stated these must actually affect agent behavior. Fix requires cross-repo work (cameleer-common model additions + cameleer-server server wiring + cameleer agent protocol handling + agent-side gating behaviour). + +## Open gaps to tackle next session + +### 1. Task 13.1 — finish manual browser QA + +Partial coverage so far: save/redeploy happy path, ENV pill styling, tab seam, variables view switcher, toast (all landed + verified). Still unverified: + +- Checkpoint restore flow (hydrate form from past snapshot → Save → Redeploy) +- Deploy failure path (FAILED status → snapshot stays null → primary button still shows Redeploy) +- Unsaved-changes dialog on in-app navigation (sidebar click with dirty form) +- Env switch with dirty form (should discard silently) +- End-to-end deploy against real Docker daemon — see "Docker deploy setup" below +- Per-tab `*` dirty marker visibility across all 4 staged tabs + +### 2. Docker deploy setup (needed to fully exercise E2E) + +Current `docker-compose.yml` sets `CAMELEER_SERVER_RUNTIME_ENABLED: "false"` so `DisabledRuntimeOrchestrator` rejects deploys with `UnsupportedOperationException`. To actually test deploy end-to-end, pick one: + +- **Path A (quick):** `docker compose up -d cameleer-postgres cameleer-clickhouse` only, then `mvn -pl cameleer-server-app spring-boot:run` on the host + `npm run dev` for the UI. Server uses host Docker daemon directly. Runtime enabled by default via `application.yml`. +- **Path B (compose-native):** enable runtime in compose by mounting `/var/run/docker.sock`, setting `CAMELEER_SERVER_RUNTIME_ENABLED: "true"` + `CAMELEER_SERVER_RUNTIME_DOCKERNETWORK: cameleer-traefik`, pre-creating the `cameleer-traefik` network, adding `CAMELEER_SERVER_RUNTIME_JARDOCKERVOLUME` for shared JAR storage, and adding a Traefik service for routing. This is a fully separate task — would need its own plan. + +Recommend Path A for finishing QA; Path B only if you want compose to be fully deployable. + +### 3. Deferred code-review items + +All flagged during the final integration review. None are blockers; each is a follow-up. + +- **DEGRADED deployments aren't checkpoints** — `PostgresDeploymentRepository.findLatestSuccessfulByAppAndEnv` filters `status = 'RUNNING'` but the executor writes the snapshot before the status is resolved (so a DEGRADED deployment has a snapshot). Either include `DEGRADED` in the filter, or skip snapshot on DEGRADED. Pick one; document the choice. +- **`Checkpoints.tsx` restore on null snapshot is a silent no-op** — should surface a toast like "This checkpoint predates snapshotting and cannot be restored." Currently returns early with no feedback. +- **Missing IT: FAILED deploy leaves snapshot NULL** — `DeploymentSnapshotIT` tests the success case and general "snapshot appears on RUNNING" but doesn't explicitly lock in the FAILED → null guarantee. Add a one-line assertion. +- **`HistoryDisclosure` expanded log doesn't `scrollIntoView`** — on long histories the startup-log panel opens off-screen. Minor UX rough edge. +- **OpenAPI `@Parameter` missing on `apply` query param** — not critical, just improves generated Swagger docs. Add `@Parameter(name = "apply", description = "staged | live (default: live)")` to `ApplicationConfigController.updateConfig`. + +### 4. Minor tech debt introduced this session + +- `samplingRate` normalization hack in `useDeploymentPageState.ts`: `Number.isInteger(x) ? \`${x}.0\` : String(x)` — works around `1.0` parsing back as `1`, but breaks for values like `1.10` (round-trips to `1.1`). A cleaner fix is to compare as numbers, not strings, in `useFormDirty`. +- `useDirtyState` defaults to `?? true` during loading (so the button defaults to `Redeploy`, the fail-safe choice). Spurious Redeploy clicks are harmless, but the "Save (disabled)" UX would be more correct during initial load. Consider a loading-aware ternary if it becomes user-visible. +- `ApplicationConfigController.updateConfig` returns `ResponseEntity.status(400).build()` (empty body) on unknown `apply` values. Consider a structured error body consistent with other 400s in the codebase. +- GitNexus index stats (`AGENTS.md`, `CLAUDE.md`) refreshed several times during the session — these are auto-generated and will refresh again on next `npx gitnexus analyze`. + +### 5. Behavioural caveats to know about + +- **Agent config writes from the Dashboard / Runtime pages** still use `useUpdateApplicationConfig` with default `apply='live'` — they push SSE immediately as before. Only Deployment-page writes use `apply=staged`. This is by design. +- **Traces & Taps + Route Recording tabs** on the Deployment page write with `apply='live'` (immediate SSE). They do **not** participate in dirty detection. The LiveBanner explains this to the user. +- **Slug is immutable** — enforced both server-side (regex + Jackson drops unknown fields on PUT) and client-side (IdentitySection renders slug as `MonoText`, never `Input`). +- **Environment is immutable after create** — the deployment page has no env selector; the environment chip is read-only and colored via `envColorVar` per the env's configured color. +- **Dirty detection ignores `version`, `updatedAt`, `updatedBy`, `environment`, `application`** on agent config — these get bumped server-side on every save and would otherwise spuriously mark the page dirty. Scrubbing happens in `DirtyStateCalculator.scrubAgentConfig`. + +## Recommended next-session kickoff + +1. Run `docker compose up -d cameleer-postgres cameleer-clickhouse`, then `mvn -pl cameleer-server-app spring-boot:run` and `npm run dev` in two terminals. +2. Walk through the rest of Task 13.1 (checkpoint restore, deploy failure, unsaved dialog, env switch). +3. File any new bugs found. Address the deferred review items (section 3) in small PR-sized commits. +4. Decide which of #148's cross-repo work to tackle — cleanest path is: (a) extend `ApplicationConfig` in cameleer-common, (b) wire server side, (c) coordinate agent-side behaviour gating. +5. If you want compose-native deploy, open a separate ticket or spec for Path B from "Docker deploy setup" above. + +## Commit range summary + +``` +1a376eb2..0a71bca7 (43 commits) + ff951877 db(deploy): add deployments.deployed_config_snapshot column (V3) + d580b6e9 core(deploy): add DeploymentConfigSnapshot record + 06fa7d83 core(deploy): type jarVersionId as UUID (match domain convention) + 7f9cfc7f core(deploy): add deployedConfigSnapshot field to Deployment model + d3e86b9d storage(deploy): persist deployed_config_snapshot as JSONB + 9b851c46 test(deploy): autowire repository in snapshot IT (JavaTimeModule-safe) + a79eafea runtime(deploy): capture config snapshot on RUNNING transition + 9b124027 test(deploy): assert containerConfig round-trip + strict RUNNING in snapshot IT + 76129d40 api(config): ?apply=staged|live gates SSE push on PUT /apps/{slug}/config + e716dbf8 test(config): verify audit action in staged/live config IT + 76352c0d test(config): tighten audit assertions + @DirtiesContext on ApplicationConfigControllerIT + e4ccce1e core(deploy): add DirtyStateCalculator + DirtyStateResult + 24464c07 core(deploy): recurse into nested diffs + unquote scalar values in DirtyStateCalculator + 6591f2fd api(apps): GET /apps/{slug}/dirty-state returns desired-vs-deployed diff + 97f25b4c test(deploy): register JavaTimeModule in DirtyStateCalculator unit test + 0434299d api(schema): regenerate OpenAPI + schema.d.ts for deployment page + 60529757 ui(deploy): scaffold AppDeploymentPage + route /apps/new and /apps/:slug + 52ff385b ui(api): add useDirtyState + apply=staged|live on useUpdateApplicationConfig + d067490f ui(deploy): add deriveAppName pure function + tests + 00c7c0cd ui(deploy): Identity & Artifact section with filename auto-derive + 08efdfa9 ui(deploy): Checkpoints disclosure (hides current deployment, flags pruned JARs) + cc193a10 ui(deploy): add useDeploymentPageState orchestrator hook + 4f5a11f7 ui(deploy): extract MonitoringTab component + 5c48b780 ui(deploy): extract ResourcesTab component + bb06c4c6 ui(deploy): extract VariablesTab component + f487e6ca ui(deploy): extract SensitiveKeysTab component + b7c0a225 ui(deploy): LiveBanner component for live-apply tabs + e96c3cd0 ui(deploy): Traces & Taps + Route Recording tabs with live banner + 98a7b781 ui(deploy): StatusCard for Deployment tab + 063a4a55 ui(deploy): HistoryDisclosure with inline log expansion + 1579f10a ui(deploy): DeploymentTab + flex-grow StartupLogPanel + 42fb6c8b ui(deploy): useFormDirty hook for per-tab dirty markers + 0e4166bd ui(deploy): PrimaryActionButton + computeMode state-machine helper + b1bdb88e ui(deploy): compose page — save/redeploy/checkpoints wired end-to-end + 3a649f40 ui(deploy): router blocker + DS dialog for unsaved edits + 5a7c0ce4 ui(deploy): delete CreateAppView + AppDetailView + ConfigSubTab + d5957468 docs(rules): update ui.md Deployments bullet for unified deployment page + 6d5ce606 docs(rules): document ?apply flag + snapshot column in app-classes + d33c039a fix(deploy): address final review — sensitiveKeys snapshot, dirty scrubbing, transition race, refetch invalidations + b7b6bd2a ui(deploy): port missing agent-config fields, var-view switcher, env pill, tab seam + 0a71bca7 fix(deploy): redeploy button after save, disable save when clean, success toast +``` + +Plus this handoff commit + the GitNexus index-stats refresh.