Files
cameleer-server/docs/handoff/2026-04-23-deployment-page-handoff.md
hsiegeln 837e5d46f5
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 2m9s
CI / docker (push) Successful in 1m17s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 38s
docs(deploy): session handoff + refresh GitNexus index stats
Handoff summarises the unified deployment page implementation (spec,
plan, 43 commits, opened Gitea issues #147 and #148), open gaps, and
recommended kickoff for the next session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 00:17:26 +02:00

15 KiB

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.getDirtyStateGET /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.tsuseUpdateApplicationConfig 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.mdApplicationConfigController 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)

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

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 checkpointsPostgresDeploymentRepository.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 NULLDeploymentSnapshotIT 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 around1.0parsing back as1, but breaks for values like 1.10(round-trips to1.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.
  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.