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>
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 newAppDeploymentPage)
Backend delivered (cameleer-server)
- Flyway V3 adds
deployments.deployed_config_snapshot JSONB DeploymentConfigSnapshotrecord:(UUID jarVersionId, ApplicationConfig agentConfig, Map<String,Object> containerConfig, List<String> sensitiveKeys)DeploymentExecutorcaptures snapshot on successful RUNNING transition (not FAILED)PostgresDeploymentRepository.saveDeployedConfigSnapshot(UUID, DeploymentConfigSnapshot)+findLatestSuccessfulByAppAndEnv(appId, envId)ApplicationConfigController.updateConfigaccepts?apply=staged|live(defaultlivefor back-compat); staged skips SSE push; 400 on unknownAppController.getDirtyState→GET /api/v1/environments/{envSlug}/apps/{appSlug}/dirty-statereturning{dirty, lastSuccessfulDeploymentId, differences[]}DirtyStateCalculatorpure 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.tsregenerated
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— acceptsclassName, flex-grows in container (dropped fixed 300px maxHeight)ui/src/api/queries/admin/apps.ts— addeduseDirtyState,Deployment.deployedConfigSnapshottypeui/src/api/queries/commands.ts—useUpdateApplicationConfigacceptsapply?: 'staged' | 'live'ui/src/router.tsx— routes/apps/newand/apps/:appIdtoAppDeploymentPageui/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—ApplicationConfigControllergains?applynote;AppControllergains dirty-state endpoint;PostgresDeploymentRepositorynotes the snapshot columndocs/superpowers/specs/2026-04-22-app-deployment-page-design.mddocs/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-clickhouseonly, thenmvn -pl cameleer-server-app spring-boot:runon the host +npm run devfor the UI. Server uses host Docker daemon directly. Runtime enabled by default viaapplication.yml. - Path B (compose-native): enable runtime in compose by mounting
/var/run/docker.sock, settingCAMELEER_SERVER_RUNTIME_ENABLED: "true"+CAMELEER_SERVER_RUNTIME_DOCKERNETWORK: cameleer-traefik, pre-creating thecameleer-traefiknetwork, addingCAMELEER_SERVER_RUNTIME_JARDOCKERVOLUMEfor 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.findLatestSuccessfulByAppAndEnvfiltersstatus = 'RUNNING'but the executor writes the snapshot before the status is resolved (so a DEGRADED deployment has a snapshot). Either includeDEGRADEDin the filter, or skip snapshot on DEGRADED. Pick one; document the choice. Checkpoints.tsxrestore 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 —
DeploymentSnapshotITtests the success case and general "snapshot appears on RUNNING" but doesn't explicitly lock in the FAILED → null guarantee. Add a one-line assertion. HistoryDisclosureexpanded log doesn'tscrollIntoView— on long histories the startup-log panel opens off-screen. Minor UX rough edge.- OpenAPI
@Parametermissing onapplyquery param — not critical, just improves generated Swagger docs. Add@Parameter(name = "apply", description = "staged | live (default: live)")toApplicationConfigController.updateConfig.
4. Minor tech debt introduced this session
samplingRatenormalization hack inuseDeploymentPageState.ts:Number.isInteger(x) ? \${x}.0` : String(x)— works around1.0parsing back as1, but breaks for values like1.10(round-trips to1.1). A cleaner fix is to compare as numbers, not strings, inuseFormDirty`.useDirtyStatedefaults to?? trueduring loading (so the button defaults toRedeploy, 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.updateConfigreturnsResponseEntity.status(400).build()(empty body) on unknownapplyvalues. 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 nextnpx gitnexus analyze.
5. Behavioural caveats to know about
- Agent config writes from the Dashboard / Runtime pages still use
useUpdateApplicationConfigwith defaultapply='live'— they push SSE immediately as before. Only Deployment-page writes useapply=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, neverInput). - Environment is immutable after create — the deployment page has no env selector; the environment chip is read-only and colored via
envColorVarper the env's configured color. - Dirty detection ignores
version,updatedAt,updatedBy,environment,applicationon agent config — these get bumped server-side on every save and would otherwise spuriously mark the page dirty. Scrubbing happens inDirtyStateCalculator.scrubAgentConfig.
Recommended next-session kickoff
- Run
docker compose up -d cameleer-postgres cameleer-clickhouse, thenmvn -pl cameleer-server-app spring-boot:runandnpm run devin two terminals. - Walk through the rest of Task 13.1 (checkpoint restore, deploy failure, unsaved dialog, env switch).
- File any new bugs found. Address the deferred review items (section 3) in small PR-sized commits.
- Decide which of #148's cross-repo work to tackle — cleanest path is: (a) extend
ApplicationConfigin cameleer-common, (b) wire server side, (c) coordinate agent-side behaviour gating. - 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.