Files
cameleer-server/docs/handoff/2026-04-23-deployment-page-handoff.md

184 lines
15 KiB
Markdown
Raw Normal View History

# 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<String,Object> containerConfig, List<String> 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.