1. Add a 'Pending deploy' Badge next to the app name when there are
local edits or the saved state differs from the last deploy. Makes
the undeployed-changes state visible even when the user isn't looking
at the tab asterisks.
2. Move Start/Stop buttons from StatusCard into the page header, next
to Delete. Runs off the latest deployment's status — Stop when
RUNNING/STARTING/DEGRADED, Start (triggers a redeploy of the last
version) when STOPPED. DeploymentTab and StatusCard shed their
onStop/onStart props.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DeploymentExecutor already persists errorMessage on FAILED transitions
but the UI never rendered it — users saw "FAILED" with no explanation.
Add a bordered error block above the action row when a deployment is
FAILED, preserving whitespace and wrapping long Docker error bodies
(e.g. 409 conflict JSON).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wrap Avatar in a span with box-shadow outline in the environment's
chosen color (slate/red/amber/green/teal/blue/purple/pink). Applied to
both the list row and the detail header. Keeps the Avatar's name-hash
interior so initials remain distinguishable; the ring just signals
which env you're looking at at a glance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace empty-body ResponseEntity.status(BAD_REQUEST).build() with
ResponseStatusException so Spring returns the usual error body shape
with a descriptive reason string, matching the idiom used by
UserAdminController, AppSettingsController, ThresholdAdminController.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously `dirtyState?.dirty ?? true` caused a stale `Redeploy` label
to flash briefly while the first fetch was in flight. Gate the default
on isLoading so the button starts as `Save (disabled)` until the
endpoint resolves — spurious Redeploy clicks were harmless but the
loading-state UX was wrong.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the Number.isInteger normalization hack in useDeploymentPageState
that mapped 1.0 → "1.0" but broke for values like 1.10 (which round-trip
to 1.1). Instead, useFormDirty now parseFloats samplingRate on both sides
before comparing, so "1", "1.0", and "1.00" all compare equal regardless
of how the backend serializes the number.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds @Parameter description so the generated OpenAPI spec / Swagger UI
explains what 'staged' vs 'live' means instead of just surfacing the
bare param name. Follow-up: run `cd ui && npm run generate-api:live`
against a live backend to refresh openapi.json + schema.d.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On long deployment histories the StartupLogPanel would render off-screen
when the user clicked a row. Ref + useEffect scrolls the panel into view
with block:'nearest' so expanding a row that's already in view doesn't
cause a disorienting jump.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Existing IT only exercises the startContainer-throws path, where the
exception bypasses the entire try block. Add a test where startContainer
succeeds but getContainerStatus never returns healthy — this covers the
early-exit at the HEALTH_CHECK stage, which is the common real-world
failure shape and closest to the snapshot-write point.
Shortens healthchecktimeout to 2s via @TestPropertySource so the test
completes in a few seconds.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
handleRestore previously returned silently when deployedConfigSnapshot
was null, leaving the user wondering why their click did nothing. Show
a warning toast explaining that the checkpoint predates snapshotting.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Snapshot is written by DeploymentExecutor before the RUNNING/DEGRADED
split, so DEGRADED rows already carry a deployed_config_snapshot. Treat
them as checkpoints — partial-healthy deploys still produced a working
config worth restoring. Aligns repo query with UI filter.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
- Bug 1: default serverDirtyAgainstDeploy to true (not false) while
dirtyState query is loading — prevents the button showing 'Save'
instead of 'Redeploy' on apps with no successful deployment yet.
- Bug 2: normalize samplingRate from server as '<n>.0' when the value
is a whole-number float so serverState matches form after save,
eliminating spurious dirty detection that kept Save enabled.
- Bug 3: add success toast after handleSave completes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Issue 1: add List<String> sensitiveKeys as 4th field to DeploymentConfigSnapshot; populate
from agentConfig.getSensitiveKeys() in DeploymentExecutor; handleRestore hydrates from
snap.sensitiveKeys directly; Deployment type in apps.ts gains sensitiveKeys field
- Issue 2: after createApp succeeds, refetchQueries(['apps', envSlug]) before navigate so the
new app is in cache before the router renders the deployed view (eliminates transient Save-
disabled flash)
- Issue 3: useDeploymentPageState useEffect now uses prevServerStateRef to detect local edits;
background refetches only overwrite form when no local changes are present
- Issue 5: handleRedeploy invalidates dirty-state + versions queries after createDeployment
resolves; handleSave invalidates dirty-state after staged save
- Issue 10: DirtyStateCalculator strips volatile agentConfig keys (version, updatedAt, updatedBy,
environment, application) before JSON comparison via scrubAgentConfig(); adds
versionBumpDoesNotMarkDirty test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
AppsTab.tsx shrunk from 1387 to 109 lines — router now owns /apps/new
and /apps/:slug via AppDeploymentPage; list-only file retained.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add deployedConfigSnapshot field to Deployment interface (mirrors server shape)
- Remove the Task 10.3 cast in handleRestore now that the type has the field
- New useUnsavedChangesBlocker hook (react-router useBlocker, v7.13.1)
- Wire AlertDialog into AppDeploymentPage for in-app navigation guard
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DeploymentTab composes StatusCard, DeploymentProgress, StartupLogPanel,
and HistoryDisclosure for the latest deployment. StartupLogPanel gains an
optional className prop, drops the fixed maxHeight, and its .panel rule
uses flex-column + min-height:0 so a parent can drive its height.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Collapsible deployment history table (sorted newest-first) with
click-to-expand StartupLogPanel for any historical deployment row.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Status badge, replica count, URL, JAR/checksum grid, and stop/start
actions for the latest deployment. CSS added to AppDeploymentPage.module.css.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ports the ConfigSubTab traces/taps and route recording content into
standalone tab components. Each write goes straight to live agents via
useUpdateApplicationConfig (apply='live'). A local draft state prevents
stale reads during the async flush. LiveBanner is rendered at the top of
both tabs to communicate the live-apply semantics.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a warning banner that communicates live-apply semantics (changes
bypass the Save/Redeploy cycle). Uses --warning-bg / --warning-border
DS tokens. CSS class .liveBanner added to AppDeploymentPage.module.css.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pure presentational tab receiving SensitiveKeysFormState via value/onChange.
Calls useSensitiveKeys() internally to show global baseline (readonly).
Local useState for the new-key input buffer. Reuses skStyles from
SensitiveKeysPage.module.css for consistent pill/badge layout.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pure presentational tab receiving VariablesFormState via value/onChange.
Rows use the new .envVarsList / .envVarRow CSS grid (1fr 2fr auto).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pure presentational tab receiving ResourcesFormState via value/onChange.
Local useState buffers for newPort/newNetwork keep the "add next item"
inputs isolated from form state. isProd prop gates the memory-reserve field.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pure presentational tab receiving MonitoringFormState via value/onChange.
Also adds shared config-tab styles to AppDeploymentPage.module.css
(configInline, toggleEnabled/Disabled, portPills, inputSizes, envVarsList/Row).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Picks up GET dirty-state, PUT config ?apply=staged|live, and
deployedConfigSnapshot on Deployment for the deployment config-diff UI.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wires DirtyStateCalculator behind an HTTP endpoint on AppController.
Adds findLatestSuccessfulByAppAndEnv to PostgresDeploymentRepository,
registers DirtyStateCalculator as a Spring bean (with ObjectMapper for
JavaTimeModule support), and covers all three scenarios with IT.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- compareJson now recurses when both nodes are ObjectNode, so nested maps
(tracedProcessors, routeRecording, routeSamplingRates) produce deep paths
like agentConfig.tracedProcessors.proc-1 instead of a blob diff
- Extract nodeToString helper: value nodes use asText() (strips JSON quotes),
null becomes "(none)", arrays/objects get compact JSON
- Apply nodeToString in both diff-emission paths (top-level mismatch + leaf)
- Add three new tests: nullAgentConfigInSnapshot, nestedAgentField_reportsDeepPath,
stringField_differenceValueIsUnquoted (8 tests total, all pass)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pure-logic dirty-state detection: compares desired JAR + agent config + container
config against the DeploymentConfigSnapshot from the last successful deployment.
Returns a structured DirtyStateResult with per-field differences. 5 unit tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add @DirtiesContext(AFTER_CLASS) so the SpyBean-forked context is torn
down after the 6 tests finish, preventing permanent cache pollution
- Replace single-row queryForObject with queryForList + hasSize(1) in both
audit tests so spurious extra rows will fail explicitly
- Assert auditCount == 0 in the 400 test to lock in the no-audit-on-bad-input invariant
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the misleading putConfig_staged_auditActionIsStagedAppConfig test
(which only checked pushResult.total == 0, a duplicate of _savesButDoesNotPush)
with two real audit-log assertions: one verifying "stage_app_config" is written
for apply=staged and a new companion test verifying "update_app_config" for the
live path. Uses jdbcTemplate to query audit_log directly (Option B).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When apply=staged, saves to DB only — no CONFIG_UPDATE dispatched to agents.
When apply=live (default, back-compat), preserves today's immediate-push behavior.
Unknown apply values return 400. Audit action is stage_app_config vs update_app_config.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the missing containerConfig assertion to snapshot_isPopulated_whenDeploymentReachesRunning
(runtimeType + appPort entries), and tightens the await predicate from .isIn(RUNNING, DEGRADED)
to .isEqualTo(RUNNING) — the mock returns a healthy container so RUNNING is deterministic.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Injects PostgresApplicationConfigRepository into DeploymentExecutor and
calls saveDeployedConfigSnapshot at the COMPLETE stage, before
markRunning. Snapshot contains jarVersionId, agentConfig (nullable),
and app.containerConfig. The FAILED catch path is left untouched so
snapshot stays null on failure. Verified by DeploymentSnapshotIT.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace manual `new PostgresDeploymentRepository(jdbcTemplate, new ObjectMapper())` with
`@Autowired PostgresDeploymentRepository repository` to use the Spring-managed bean whose
ObjectMapper has JavaTimeModule registered. Also removes the redundant isNotNull() assertion
whose work is done by the field-level assertions that follow.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Appends DeploymentConfigSnapshot deployedConfigSnapshot to the Deployment
record and adds a matching withDeployedConfigSnapshot wither. All
positional call sites (repository mapper, test fixture) updated to pass
null; Task 1.4 will wire real persistence and Task 1.5 will populate
the field on RUNNING transition.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>