Commit Graph

1584 Commits

Author SHA1 Message Date
hsiegeln
9ecc9ee72a ui(deploy): pending-deploy badge + Start/Stop in page header
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>
2026-04-23 00:51:11 +02:00
hsiegeln
9c54313ff1 ui(deploy): surface deployment failure reason in StatusCard
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>
2026-04-23 00:49:29 +02:00
hsiegeln
e5eb48b0fa ui(admin): env-colored ring on environment avatars
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>
2026-04-23 00:48:51 +02:00
hsiegeln
b655de3975 fix(config): structured 400 body on unknown apply value
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>
2026-04-23 00:45:31 +02:00
hsiegeln
4e19f925c6 ui(deploy): loading-aware default for dirty-state baseline
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>
2026-04-23 00:42:48 +02:00
hsiegeln
8a7f9cb370 fix(deploy): compare samplingRate as number in dirty detection
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>
2026-04-23 00:41:33 +02:00
hsiegeln
b5ecd39100 docs(api): document ?apply query param on updateConfig (Swagger)
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>
2026-04-23 00:39:10 +02:00
hsiegeln
629a009b36 ui(deploy): scrollIntoView when expanding a history row
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>
2026-04-23 00:38:23 +02:00
hsiegeln
ffdaeabc9f test(deploy): lock in FAILED→null snapshot for health-check-fail path
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>
2026-04-23 00:37:37 +02:00
hsiegeln
703bd412ed fix(deploy): toast when restoring checkpoint with no snapshot
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>
2026-04-23 00:34:45 +02:00
hsiegeln
4d4c59efe3 fix(deploy): include DEGRADED deploys as restorable checkpoints
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>
2026-04-23 00:34:25 +02:00
hsiegeln
837e5d46f5 docs(deploy): session handoff + refresh GitNexus index stats
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
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
hsiegeln
0a71bca7b8 fix(deploy): redeploy button after save, disable save when clean, success toast
- 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>
2026-04-23 00:06:00 +02:00
hsiegeln
b7b6bd2a96 ui(deploy): port missing agent-config fields, var-view switcher, env pill, tab seam
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 23:45:19 +02:00
hsiegeln
d33c039a17 fix(deploy): address final review — sensitiveKeys snapshot, dirty scrubbing, transition race, refetch invalidations
- 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>
2026-04-22 23:29:01 +02:00
hsiegeln
6d5ce60608 docs(rules): document ?apply flag + snapshot column in app-classes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 23:17:25 +02:00
hsiegeln
d595746830 docs(rules): update ui.md Deployments bullet for unified deployment page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 23:16:59 +02:00
hsiegeln
5a7c0ce4bc ui(deploy): delete CreateAppView + AppDetailView + ConfigSubTab
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>
2026-04-22 23:16:38 +02:00
hsiegeln
3a649f40cd ui(deploy): router blocker + DS dialog for unsaved edits
- 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>
2026-04-22 23:13:36 +02:00
hsiegeln
b1bdb88ea4 ui(deploy): compose page — save/redeploy/checkpoints wired end-to-end
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 23:10:55 +02:00
hsiegeln
0e4166bd5f ui(deploy): PrimaryActionButton + computeMode state-machine helper
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 23:05:46 +02:00
hsiegeln
42fb6c8b8c ui(deploy): useFormDirty hook for per-tab dirty markers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 23:05:22 +02:00
hsiegeln
1579f10a41 ui(deploy): DeploymentTab + flex-grow StartupLogPanel
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>
2026-04-22 23:03:52 +02:00
hsiegeln
063a4a5532 ui(deploy): HistoryDisclosure with inline log expansion
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>
2026-04-22 23:03:02 +02:00
hsiegeln
98a7b7819f ui(deploy): StatusCard for Deployment tab
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>
2026-04-22 23:02:44 +02:00
hsiegeln
e96c3cd0cf ui(deploy): Traces & Taps + Route Recording tabs with live banner
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>
2026-04-22 23:00:14 +02:00
hsiegeln
b7c0a225f5 ui(deploy): LiveBanner component for live-apply tabs
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>
2026-04-22 22:59:11 +02:00
hsiegeln
f487e6caef ui(deploy): extract SensitiveKeysTab component
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>
2026-04-22 22:57:02 +02:00
hsiegeln
bb06c4c689 ui(deploy): extract VariablesTab component
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>
2026-04-22 22:56:31 +02:00
hsiegeln
5c48b780b2 ui(deploy): extract ResourcesTab component
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>
2026-04-22 22:56:05 +02:00
hsiegeln
4f5a11f715 ui(deploy): extract MonitoringTab component
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>
2026-04-22 22:55:25 +02:00
hsiegeln
cc193a1075 ui(deploy): add useDeploymentPageState orchestrator hook
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 22:53:06 +02:00
hsiegeln
08efdfa9c5 ui(deploy): Checkpoints disclosure (hides current deployment, flags pruned JARs)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 22:51:39 +02:00
hsiegeln
00c7c0cd71 ui(deploy): Identity & Artifact section with filename auto-derive
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 22:49:43 +02:00
hsiegeln
d067490f71 ui(deploy): add deriveAppName pure function + tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 22:46:52 +02:00
hsiegeln
52ff385b04 ui(api): add useDirtyState + apply=staged|live on useUpdateApplicationConfig
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 22:45:42 +02:00
hsiegeln
6052975750 ui(deploy): scaffold AppDeploymentPage + route /apps/new and /apps/:slug
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 22:43:54 +02:00
hsiegeln
0434299d53 api(schema): regenerate OpenAPI + schema.d.ts for deployment page
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>
2026-04-22 22:42:10 +02:00
hsiegeln
97f25b4c7e test(deploy): register JavaTimeModule in DirtyStateCalculator unit test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 22:38:57 +02:00
hsiegeln
6591f2fde3 api(apps): GET /apps/{slug}/dirty-state returns desired-vs-deployed diff
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>
2026-04-22 22:35:35 +02:00
hsiegeln
24464c0772 core(deploy): recurse into nested diffs + unquote scalar values in DirtyStateCalculator
- 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>
2026-04-22 22:25:04 +02:00
hsiegeln
e4ccce1e3b core(deploy): add DirtyStateCalculator + DirtyStateResult
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>
2026-04-22 22:20:49 +02:00
hsiegeln
76352c0d6f test(config): tighten audit assertions + @DirtiesContext on ApplicationConfigControllerIT
- 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>
2026-04-22 22:18:44 +02:00
hsiegeln
e716dbf8ca test(config): verify audit action in staged/live config IT
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>
2026-04-22 22:13:53 +02:00
hsiegeln
76129d407e api(config): ?apply=staged|live gates SSE push on PUT /apps/{slug}/config
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>
2026-04-22 22:07:36 +02:00
hsiegeln
9b1240274d test(deploy): assert containerConfig round-trip + strict RUNNING in snapshot IT
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>
2026-04-22 21:54:57 +02:00
hsiegeln
a79eafeaf4 runtime(deploy): capture config snapshot on RUNNING transition
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>
2026-04-22 21:51:00 +02:00
hsiegeln
9b851c4622 test(deploy): autowire repository in snapshot IT (JavaTimeModule-safe)
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>
2026-04-22 21:43:40 +02:00
hsiegeln
d3e86b9d77 storage(deploy): persist deployed_config_snapshot as JSONB
Wire SELECT_COLS, mapRow deserialization, and saveDeployedConfigSnapshot
update method. Adds PostgresDeploymentRepositoryIT with roundtrip,
null-default, and clear-to-null tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 21:39:04 +02:00
hsiegeln
7f9cfc7f18 core(deploy): add deployedConfigSnapshot field to Deployment model
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>
2026-04-22 21:31:48 +02:00