V2 → V3 frontend cutover (signal-native migration)#5308
Draft
norman-abramovitz wants to merge 453 commits intodevelopfrom
Draft
V2 → V3 frontend cutover (signal-native migration)#5308norman-abramovitz wants to merge 453 commits intodevelopfrom
norman-abramovitz wants to merge 453 commits intodevelopfrom
Conversation
CF-level users page at /cloud-foundry/:cnsi/users now drives <app-signal-list>
via CfUsersSignalConfigService instead of the legacy ListConfig path. First
half of the dual-level users migration; the per-space users tab follows in
the next commit. New backend handler (no V3 emulation existed for users) +
shared frontend infrastructure both ship in this commit.
Backend (new — there was no users handler before):
- StUser, StUserOrgRole, StUserSpaceRole, StUsersResponse DTOs added to
native_types.go. StUser carries V3-only PresentationName + Origin
alongside the V2-compatible identity fields, plus pre-bucketed
OrgRoles/SpaceRoles arrays so the frontend doesn't pay a per-row scan
through the role list. StUserSpaceRole carries both spaceGuid and
orgGuid so the per-space tab's filter is one .some() call without a
second join.
- GET /pp/v1/cf/users/:cnsi in native_users_reads.go. Drains /v3/users
(identity) and /v3/roles (grants) once each, buckets roles by
user.guid into per-(user,scope) entries, strips the V3 prefix
("organization_manager" -> "manager"; "space_developer" ->
"developer") so cells render compactly. Soft-fail on the role drain
— empty role buckets render rather than 502'ing the page on a
transient CAPI hiccup. Falls back to a /v3/spaces drain only when a
role's organization relationship is missing (V3 always carries it
on space roles, but defensive). Two response shapes mirror the
service-offerings handler: ?return=summary emits StratosPagedResponse
for the future CnsiUsersSource consumer; default emits flat
StUsersResponse for direct fetches.
- Route registered in native_routes.go.
Frontend shared infrastructure:
- StUser / StUserOrgRole / StUserSpaceRole / StUsersResponse mirror DTOs
added to stratos-types.ts. orgRoles + spaceRoles are non-nullable in
the contract since the backend always emits []; consumers can .length
without a guard.
- CnsiUsersSource — thin CnsiEntitySource subclass with
entityName='users'. Reserved for a future multi-CNSI users wall;
current pages fetch StUsersResponse directly via the config service.
- CfUsersSignalConfigService — single-CNSI, optionally space-scoped.
Modelled on CfRoutesSignalConfigService (which also fetches its own
list and supports the optional sub-scope pattern). The service owns
its own fetch against /pp/v1/cf/users/:cnsi (users aren't carried on
EndpointDataService — the home-page cache covers orgs/apps/spaces;
users live separately because the join is heavier). initialize() is
the CF-wide entry point; initializeForSpace(cnsiGuid, spaceGuid) is
the per-space entry point that locks the scope and projects the user
list through a `users.spaceRoles.some(sr => sr.spaceGuid === lock)`
computed. orgNameByGuid + spaceNameByGuid signals expose the
EndpointDataService orgs() / spaces() lookups so cells resolve names
instead of GUIDs (no_raw_guids feedback rule). No write methods this
round — Manage Roles / Remove User stay legacy.
Frontend page (commit 1):
- CloudFoundryUsersComponent (overwritten). Standalone, OnPush,
imports SignalListComponent. Calls usersConfig.initialize(cfGuid).
Columns: Username, Origin, Org Roles (compound — per-org segment
rendered as "<orgName>: <roles>" with a link to the org page when
the name has resolved; "—" when no roles), Space Roles (compound —
"<orgName> / <spaceName>: <roles>" with a link to the space page),
Created. No favorite column (users aren't favoritable in legacy)
and no actions kebab (Manage Roles / Remove stay on separate
legacy routes). Defaults: pageSize=25, viewMode='table' to match
the legacy ViewType.TABLE_ONLY behaviour for this denser-table-
friendly entity. Sort default: username ascending; nameFilter
searches username substring.
- The route entry at cloud-foundry-section.routes.ts already pointed
at this component (path unchanged); the component file itself was
rewritten in place.
Explicit non-goals:
- Manage Roles, Remove User, Invite Users — all stay on the legacy
ngrx flow at /users/manage, /users/remove, /users/invite. Those
routes are unchanged.
- UAA / authentication code untouched.
- Per-space users tab ships in the next commit.
Verification:
- backend: cd src/jetstream && go build ./plugins/cloudfoundry/ — clean.
- frontend: bun run lint — 0 new warnings on touched files.
- vitest: 4/4 across the new spec
(cloud-foundry-users.component.spec.ts).
- playwright: live page on adepttech-shape local data renders 12 users,
1-12 of 12 pagination, columns populated with Username (admin,
network-policy, CATS-*, etc.), Origin (uaa for the CATS users, "—"
for the rest), Org Roles + Space Roles compound segments resolving
org/space names via the EndpointDataService cache, Created
timestamps formatted via the same helper the routes page uses.
v2-v3-field-mapping.md backfill (Stratos-shape StUser fields paragraph
in the Users section) lands in the same KS doc as the prior migrations.
Per-space users tab at /cloud-foundry/:cnsi/organizations/:org/spaces/:space/users
now drives <app-signal-list> via CfUsersSignalConfigService instead of the
legacy ListConfig path. Sibling work to the CF-level users migration in
the previous commit. No backend changes — both pages reuse the new
GET /pp/v1/cf/users/:cnsi handler.
Scope:
- CloudFoundrySpaceUsersComponent overwritten as standalone OnPush.
Calls usersConfig.initializeForSpace(cfGuid, spaceGuid) — the service's
computed `users` signal projects the CNSI-wide list through a
spaceRoles.some(sr => sr.spaceGuid === lockedSpaceGuid) filter so
only users with at least one role grant in this space render. The
same singleton state is shared with the CF-level page; visiting the
CF-level page after a per-space tab resets the lock back to empty
(initialize() clears it before initializeForSpace re-applies).
- Columns trim the CF-level shape to four:
Username (text, sort by username; falls back to presentationName
then guid for unnamed identities). Origin (text, '—' when empty).
Space Roles — narrowed to THIS space only via filter on the user's
spaceRoles[] bucket; renders the bucket's prefix-stripped role
names joined ("manager, developer"). The Org Roles column from
the CF-level page is intentionally dropped here — every visible
user already holds a role in this org by definition, so the column
would be redundant.
Created (date, formatted via the same helper the CF-level page uses).
No favorite, no actions kebab — Manage Roles / Remove User stay
legacy.
- Defaults match the CF-level page: pageSize=25, viewMode='table',
sort by username ascending. Name filter only.
- The legacy page-sub-nav "Manage Roles" / "Invite User" buttons are
dropped here for parity with the CF-level signal-native page;
reintroducing them as SignalListConfig.headerActions bindings is
follow-up work tied to wiring header-action support into the shared
signal-list component.
Explicit non-goals:
- Manage Roles + Remove User flows stay on the legacy stepper paths
(/users/manage, /users/remove). Those routes are unchanged.
- Multi-CNSI / wall-style users page deferred (CnsiUsersSource added
in the previous commit reserves the contract for it).
- Per-space write paths (Add User by Username, Invite Email, etc.)
stay legacy.
Verification:
- frontend: bun run lint — 0 new warnings on touched files.
- vitest: 3/3 across the new spec
(cloud-foundry-space-users.component.spec.ts).
- playwright: live page on adepttech-shape local data renders 3 users
(admin, rmq-smoke-tests-1-USER-..., testuser1) for the chosen
test space (CSnSysOkvwBD6A-UQyQW6gmKPhI / opensource / openproject),
Space Roles column shows scope-narrowed grants ("manager, developer"
for admin, "manager" for the others), Origin shows uaa, no Org
Roles column. Page is a strict subset of the CF-level user list
for the same CNSI as expected.
Two coupled SignalListConfig API extensions, both demoed on the cf
users pages.
1. headerActions — page-level action button slot:
Introduces SignalListHeaderAction[] on SignalListConfig so
signal-native pages can render Create / Add / Invite / Manage
buttons above/alongside the toolbar — closing the regression
where users / spaces / marketplace migrations dropped legacy
<app-page-sub-nav> buttons. Each entry exposes label, optional
material icon, optional reactive disabled / visible signals,
primary (filled) vs default (outlined) emphasis, optional tooltip,
and an invoke() handler that may return a promise. Async errors
are caught and surfaced via TailwindSnackBarService — mirrors
the row-actions kebab error path. Undefined or empty
headerActions collapses the slot entirely (zero visual change
for existing pages).
2. compound maxVisible — graceful collapse for high-cardinality
compound cells:
Adds optional maxVisible + collapsedLabel to SignalListColumn
compound configs. Background: cf admin user has 2507 space role
grants whose compound segments overflowed rows and pushed
adjacent cells out of view. With maxVisible set, only the first
N segments render and a "…and N more" affordance appears; click
expands to show all segments plus a "…show fewer" indicator;
click again collapses. Per-(row, column) state keying so two
compound columns on one row expand independently. Unset =
unlimited (zero behavior change for existing pages).
Both features wired up to cf users for demonstration:
- Invite User header action stub (CF-level + per-space) — placeholder
snackbar until the real Invite User flow migrates off legacy.
- Manage Roles header action stub (per-space only) — same shape.
- Org Roles + Space Roles columns gain maxVisible: 5 with
domain-specific labels ("…and N more orgs", "…and N more spaces").
Per-space users page renders space roles as kind: 'text', so
maxVisible doesn't apply there.
Closes the framework gaps tracked in
memory/project_signallist_header_actions.md and
memory/project_signallist_row_overflow.md. Unblocks Create/Add
buttons on every remaining signal-native page (cells, build-packs,
stacks, security-groups, quota-definitions, future detail pages)
and protects future compound columns from row-overflow regressions.
Verification:
- bun run lint: 0 new warnings on touched files (133 pre-existing
warnings repo-wide, unchanged).
- bun run vitest signal-list: 21/21 pass (was 14, added 7 for
header actions + 4 for compound maxVisible — note one bucket).
- bun run vitest cf-users: 5/5 pass.
- bun run vitest cf-space-users: 4/4 pass.
- Live: cf users page renders Invite User button + Org/Space role
collapse with "…and 49 more orgs" / "…and 2502 more spaces"
on the admin row; click each expands independently; click
"…show fewer" collapses.
Commit 18da064 (closes R5 from project_app_wall_followups) added registerFilterExtractor + startStatsPolling + appStats + endpointNames + orgNames + spaceNames + viewMode + filterField + clearFilters + start/stop/restart/restage* call sites to ApplicationWallComponent.ngOnInit, but the spec's makeStubSignalConfigService factory at line 33-53 was not updated. Three tests blew up on TypeError: this.appsConfig.* is not a function. Fix is mechanical: add the missing keys to the mock factory with appropriate signal/Map defaults and vi.fn() stubs. No runtime logic changed — this only restores spec coverage that drifted when 18da064 landed.
FWT-956 framework primitive #1: extend the per-CNSI entity source base class with single-entity accessor (byGuid) and cold-fetch (loadOne) for direct-URL detail-page navigation. byGuid(guid) returns Signal<T | undefined> as a computed lookup over the cached items array. Free for sources whose list page has already drained. loadOne(guid) is the cold-fetch path. Three-tier de-dup: - if the item is already cached, no-op - if a whole-drain load() is in flight, await it (no parallel single-GET against the same data) - otherwise dispatch _doLoadOne(guid) with a per-guid in-flight Promise<void> map so concurrent loadOne(sameGuid) calls share one HTTP request Subclasses opt into single-item HTTP by implementing the optional urlForOne(guid) abstract method. Sources without a single-item backend endpoint fall back to the full drain via load() — works, just over-fetches. 10 new vitest cases cover: byGuid undefined-before-load, byGuid post-load lookup, byGuid reactivity across drains, loadOne no-op when cached, loadOne fallback to load() when urlForOne absent, loadOne single-GET when urlForOne present (cnsiGuid stamping preserved), loadOne replaces existing items, loadOne waits for in-flight drain, loadOne de-dups concurrent same-guid calls, loadOne allows different-guid parallelism.
FWT-956 framework primitive #2: shared layout component for signal-native detail pages. Mirrors the signal-list pattern's toolbar shape so detail and list surfaces look consistent when shown together. SignalDetailConfig interface: - breadcrumbs: Signal<readonly SignalDetailBreadcrumb[]> - status: Signal<SignalDetailStatus | undefined> (color-coded pill) - headerActions: readonly SignalDetailHeaderAction[] (same shape as SignalListConfig.headerActions from FWT-929 for visual and semantic parity) - tabs: Signal<readonly SignalDetailTab[]> (router-children driven for deep-linkable URLs per design-doc Q3) - loading / error: Signal<...> for page-level state Toolbar row auto-skips when no breadcrumbs / status / actions. Tab nav row auto-skips when no tabs. Loading replaces the body slot with a spinner; error replaces it with a banner that stringifies non-Error values. Otherwise <ng-content> renders so consumers can mount <router-outlet> (tabbed detail) or raw content (single-page detail) at their discretion. Tailwind-only — no SCSS. Uses semantic tokens (bg-content-bg, text-content-muted, bg-success-100/text-success-800, etc.) so dark mode and theme overrides flow through unchanged. Folds the design doc's Layer 5 (tab nav helper) into Layer 3 since they're not separable in practice. 16 vitest cases cover: empty-toolbar skip, breadcrumbs (link vs span, chevron count), status pill colors (success / warning / danger / neutral default), header actions (icon, primary class, visibility flip, disable flip, click invoke, disabled-no-invoke), tab rendering, hidden-tab flip, empty-tabs skip, loading-replaces-body, error-replaces-body, body-when-clean, non-Error stringification.
FWT-956 framework primitive #3: extend StepComponent with an additive signal-native step contract alongside the existing StepOnNextFunction lifecycle. New SignalStepHandle interface: { valid: Signal<boolean>; submit?: () => Promise<void>; skipIf?: Signal<boolean>; } When a step sets the new `signalHandle` input the StepComponent's effective valid / skip / submission delegate to the handle: - valid getter returns signalHandle.valid() when set, else legacy _valid storage - skip getter returns signalHandle.skipIf() when set, else legacy _skip storage - new invokeNext(index) method dispatches signalHandle.submit() (or auto-success when handle is set without submit), falls back to legacy onNext(index, this) when no signalHandle SteppersComponent's only change is goNext() calling step.invokeNext(idx) instead of step.onNext(idx, step). canGoto / canGoNext / findValidStep already use the now-effective getters. Existing 100+ legacy consumers keep working unchanged. @ngrx/store integration retained on SteppersComponent — drives RouterNav / getPreviousRoutingState back-navigation that 100+ consumers depend on. Wider consumer migration sweep + ngrx removal tracked at FWT-957. 14 new vitest cases on StepComponent: legacy valid/skip storage (getter returns _valid/_skip), signal-handle valid (precedence over legacy boolean), signal-handle skipIf (with fallback to legacy when skipIf is absent on the handle), invokeNext dispatch (legacy onNext path / signal+submit Promise resolution / signal- no-submit auto-success / Promise rejection mapped to {success:false, message} / non-Error rejection stringification).
FWT-956 sample migrations validating the no-submit auto-success
path of the new signal-native step contract (FWT-956 #3a).
Each consumer is a single-step tile/picker page with
hideNextButton — the step is a confirmation surface, navigation
happens via tile selection (RouterNav dispatch on selectedTile
setter). Migrated:
- backup-restore-endpoints (named explicitly in FWT-956 AC)
- create-endpoint-base-step
- setup-welcome
Per consumer: import signal + SignalStepHandle, declare
`signalHandle: SignalStepHandle = { valid: signal(true).asReadonly() }`,
bind `[signalHandle]="signalHandle"` on the <app-step>.
Behavior is unchanged — the legacy default `_valid = true` had
the same effect — but the consumers now declaratively participate
in the new contract, establishing the migration template that
FWT-957 will follow for the remaining ~105 consumers.
No legacy features dropped; this is purely additive adoption of
the new input on the StepComponent. The legacy onNext / valid
inputs continue to work for any consumer that hasn't yet migrated.
FWT-956 framework primitive #4: port state-driven SCSS coloring on <app-form-field> to template-bound Tailwind class getters. SCSS file shrinks from 394 to 213 lines. Three new getter methods on CustomFormFieldComponent replace SCSS descendant-combinator state rules: - prefixSuffixColorClasses (replaces .form-field-prefix / .form-field-suffix with .focused / .invalid / .valid parents) - underlineColorClasses (replaces .form-field-underline) - rippleColorClasses (replaces .form-field-ripple, including color-accent / color-warn / invalid / valid overrides + the scale-x focus expansion) Static color rules moved inline to the template as Tailwind utilities: - .error-icon → text-danger - .success-icon → text-success - .form-field-error wrapper → text-danger - .form-field-hint wrapper → text-content-muted SCSS retained: - Appearance variants (fill / outline / legacy) — use color-mix() and dynamic CSS-var-driven backgrounds that don't map to fixed Tailwind utilities - compact float-label-never variant for filter contexts - media-query rules (responsive / print / reduced-motion / high-contrast) The FormControl + toSignal bridge pattern is documented as a top-of-file comment block on custom-form-field.component.ts with a full usage example showing composition into headerActions / SignalStepHandle. The bridge is intentionally one-way (form → signal); FormControl remains the source of truth for two-way binding. No new SignalFormHandle interface — forms compose into existing primitives via toSignal + computed expressions. 14 new vitest cases on the getter transitions: default state (text-content-muted / bg-input-border / bg-input-focus-border + scale-x-0), focused (text-input-focus-border / bg-transparent / scale-x-100), invalid > focused / invalid > color=accent precedence (text-danger / bg-danger), valid (text-success / bg-success), color=accent ripple (bg-accent), color=warn ripple (bg-danger). No visual regressions expected — the same state→color mapping is preserved, just expressed as template-bound class strings instead of SCSS descendant rules. Existing consumers (Connect endpoint dialog, every form across the codebase) continue to render identically.
FWT-957. Migrates 5 endpoint admin wizards to the per-step SignalStepHandle contract introduced in FWT-956: - backup-endpoints: select + password steps; submit wraps existing backup-download flow as Promise, navigates to /endpoints on success - restore-endpoints: file + password steps; submit wraps restore flow as Promise, navigates to /endpoints on success - edit-endpoint: child EditEndpointStepComponent exposes signalHandle wrapping legacy onNext (legacy validate/onNext kept for IStepperStep interface compliance) - local-account-wizard: signal wraps form-status validity; submit awaits existing next() (legacy hard window reload preserved) - helm-hub-registration: single-step Shape 2; valid=true, submit wraps Artifact Hub register + Router.navigate(/endpoints) local-account-wizard requires runInInjectionContext for toSignal because its setup runs from ngOnInit (not constructor).
FWT-957. Migrates 8 CF admin wizards (add/edit org, space, quota, space-quota) to the per-step SignalStepHandle contract. Each child step component exposes signalHandle: SignalStepHandle and a @input() redirectUrl: string. Parent template binds [signalHandle]="step1.signalHandle" and [redirectUrl] to the same URL previously passed as <app-steppers cancel> — matching the legacy cancel$ fallback behavior. For ViewChild-backed forms (quota, space-quota, edit-organization, edit-space) where the form is conditional (@if (quota)), validity mirroring is wired via either ngAfterViewInit (static ViewChild) or a ViewChild setter (conditional). Edit-space preserves its two-stage update (space attrs then optional quota change). Legacy validate()/submit methods on org/space step components left in place to minimize diff (dead code, harmless). All replaced redirect: true paths now use Router.navigateByUrl.
FWT-957. invite-users-create exposes signalHandle: SignalStepHandle with submit calling a refactored runInvite() so the new signal-handle path and the legacy onNext share the same side effects. Cancel-button-text madeChanges binding preserved.
FWT-957. Migrates 5 app/service consumers to SignalStepHandle: - new-application-base-step: Shape 1 tile selector (signal+html only) - edit-application: Shape 2 single-step CRUD; form valid+dirty as signal, explicit Router.navigate to app detail - application-delete: 2 confirmation steps (Routes, Service Instances) as valid:signal(true); Confirm step Shape 2 destructive submit delegating to existing redirectToAppWall flow. Legacy startDelete arrow retained for spec compatibility. - add-route-stepper / add-routes: Shape 2; signalHandle exposed on child AddRoutesComponent. addRouteMode made signal-backed via getter/setter so step valid recomputes on radio toggle. - detach-service-instance / detach-apps: child exposes signalHandle for Unbind Apps step; parent Confirm step Shape 2 with submit- driven detachServiceBinding + Router.navigate to /services.
FWT-957. edit-profile-info exposes signalHandle: SignalStepHandle backed by toSignal of editProfileForm.statusChanges + dirty tracking, mirroring the legacy [valid]="editProfileForm.valid && dirty" binding. Submit awaits the existing save flow + Router.navigate(/user-profile).
FWT-957. 12 wizards mark inline with // FWT-957 DEFERRED: comments — these are multi-step flows with cross-step shared state that the per-step SignalStepHandle contract from FWT-956 cannot express: - Endpoint family: create-endpoint, console-uaa-wizard, git-registration (two-step Register + optional Connect with cross-component connect.doConnect/onEnter/onNext coordination) - App / service families: create-application, deploy-application, add-service-instance (cross-step ngrx state — selected service ID flows step1 -> step2 -> step3) - CF user family: manage-users (3-step + onLeave/onEnter + applyStarted toggle), remove-user (applyStarted two-click + ignoreSuccess semantic) - Autoscaler / K8s family: edit-autoscaler-policy, kube-config-registration, create-release, upgrade-release (multi-step with shared mutable state) FWT-959 introduces the cross-step coordinator primitive (SignalStepperContext<T>) and sweeps all 12, then completes the ngrx removal from SteppersComponent / StepComponent that this ticket leaves open.
The endpoint-data.service.ts load() path was calling
/pp/v1/cf/routes/{cnsi} without ?return=counts, falling through to
the full route list + ListDestinations path on the backend. That
delayed the home card's route count behind the apps fetch instead of
arriving in parallel with the orgs and apps counts.
Adding ?return=counts hits the backend per_page=1 fast path
(native_handlers.go:588) — same pattern the orgs and apps requests
already use.
Visible effect: home cards display all three counts (orgs, apps,
routes) together, then update the recently-deployed apps list when
that fetch completes.
FWT-959 Part 1. Adds 12 optional fields to SignalStepHandle so the 12 deferred Shape 3 stepper consumers (manage-users, deploy-application, edit-autoscaler-policy, kube-config-registration, etc.) can express their full @input surface as signals: blocked, hidden, onEnter, onLeave, destructiveStep, canClose, disablePrevious, finishButtonText, nextButtonText, cancelButtonText, hideCloseButton, showBusy Each existing @input on StepComponent gains a setter+getter pair — the getter prefers signalHandle.<field>?.() over legacy storage. Signal reads inside getters are tracked by Angular CD so the parent re-evaluates without explicit EventEmitter wiring. submit() resolved-value now threads { ignoreSuccess?: boolean } through StepOnNextResult — preserves the legacy applyStarted / ignoreSuccess two-click semantic that manage-users and remove-user rely on. New invokeLeave() mirrors invokeNext() for onLeave delegation; SteppersComponent.goNext() updated to call it. pOnEnter now prefers signalHandle.onEnter over legacy onEnter. Adds 17 spec cases covering the new field paths (win-vs-fall-through, state-driven toggling, onEnter/onLeave/ignoreSuccess delegation).
Follow-up to dc8a71a — the spec's ROUTES_URL constant also needs the ?return=counts query param so the HttpTestingController matches the new request shape.
Production build caught a TS2339 that vitest gate missed: the
Promise<void | { ignoreSuccess?: boolean }> resolved value can't
be safely accessed via ?.ignoreSuccess because optional chain
doesn't narrow void out of the union. Add a typeof check before
reading the property.
FWT-959 Part 2 (Partition D). Parent-owned handles for the 2-step selection→import flow. KubeConfigImportComponent.applyStarted promoted to a signal-backed getter/setter so the review step's canClose/destructiveStep/finishButtonText can be computed(). Selection step's submit is omitted (auto-advance); cross-step cluster data is read from the selector's KubeConfigHelper inside the review step's onEnter rather than the legacy onNext-data-return path. Review step's submit delegates to the importer's existing onNext to preserve the two-click "Import then Close" semantic and translates the legacy redirect: true into an explicit Router.navigate to /endpoints.
FWT-959 Part 2 (Partition D). 2-step Install Chart wizard. Details step
handle mirrors validate$ via toSignal so the form's validity gates the
Next button. Overrides step handle resizes the editor on enter and
runs the existing createNamespace + installChart pipeline on submit;
legacy { redirect: true, redirectPayload: { path } } becomes an
explicit Router.navigate to the workload summary page. Dropped the
endpointChanged toObservable indirection — read endpoint.valueChanges
directly inside the namespaces$ stream — and removed the now-unused
endpointChangedSignal field plus its feeder subscription.
FWT-959 Part 2 (Partition D). 2-step Upgrade Workload wizard. Version
step handle's valid signal is fed by the existing validate$
subscription inside the helper.hasUpgrade() callback (validate$ is
constructed lazily once the upgrade chart resolves). Overrides step
handle delegates to doUpgrade and on success navigates to cancelUrl —
the legacy { redirect: true, redirectPayload: { path: cancelUrl } }
plumbing becomes an explicit Router.navigate. showAdvancedOptions
parity branch preserved for when the advanced step is reinstated.
AsyncPipe import removed — no remaining | async in the template.
FWT-959 Part 2 (Partition A). 2-step UAA setup wizard. Step 1's handle mirrors uaaForm.valueChanges into a signal so the Next button gates on form validity, and submit() dispatches SetupConsoleGetScopes then awaits the uaaSetup store, populating uaaScopes/selectedScope on success. Step 2's submit dispatches SetupSaveConfig and awaits the combined uaa+auth state with the legacy delay/retry/VerifySession chain, hard-reloading the app on success. Cross-step state stays as plain fields read by step 2's template since step 1 populates them inside its submit before advance. Legacy uaaFormNext/uaaScopeNext StepOnNextFunctions and the validateUAAForm Observable removed in the same diff.
FWT-959 Part 2 (Partition A). 2-step inline CF/metrics fallback flow rendered when no custom registrationComponent is wired. step1's handle mirrors the step's validate Observable into a signal (subscribed in queueMicrotask because validate is constructed lazily in ngAfterContentInit), and submit delegates to the existing onNext to preserve the snackbar + dup-endpoint warning side-effects, then hands the ConnectEndpointConfig to connect.onEnter before advance — replacing the legacy onNext-data-return → pOnEnter path. Legacy redirect:true from step1 translates to Router.navigate(['/endpoints']). CreateEndpointConnectComponent grows additive validSignal / doConnectSignal alongside getter/setter shims for the legacy valid / doConnect fields, so the connect step handle's computed() reads stay reactive without polling. Public API unchanged — both create-endpoint and git-registration consumers keep working with the same template- ref bindings.
FWT-959 Part 2 (Partition A). 2-step Github/Gitlab registration wizard mirroring create-endpoint's shape. registerStepHandle mirrors the form's validate Observable into a signal subscribed inside init() (which runs after endpoints$ resolves), with an immediate initial- value set so the Next button reflects the pre-selected default type. submit calls a new runRegistration() private method (extracted from the old onNext field) and hands the ConnectEndpointConfig to connect.onEnter before advance. connectStepHandle shares the create-endpoint shape exactly, reading the connect child's signal- backed valid/doConnect via computed().
FWT-959 Part 2 (Partition C). Single Confirm step that uses the
applyStarted / ignoreSuccess two-click semantic. applyStarted
promoted to a signal-backed getter/setter so the handle's canClose /
disablePrevious / destructiveStep / finishButtonText fields can be
computed() over it. submit() returns { ignoreSuccess: true } on the
first click (apply dispatched, no auto-advance, no success snackbar)
and navigates back to defaultCancelUrl on the second click — the
legacy { redirect: true } becomes an explicit Router.navigateByUrl.
isBlocked$ bridged to a signal in ngAfterViewInit so the handle's
blocked field reacts when the store data settles. Legacy startApply
StepOnNextFunction removed in the same diff.
The Jetstream CAPI proxy wraps responses in {<cnsiGuid>: body} when
x-cap-cnsi-list is set without x-cap-passthrough — the multi-endpoint
fan-out shape. AppDetailDataService was assigning the wrapped object
directly to APIResource<IApp>, so app.entity ended up undefined and the
build-tab template threw 'Cannot read properties of undefined (reading
\\'staging_failed_description\\')' on every render tick.
Adding x-cap-passthrough:true asks the proxy to return the raw CAPI body
unwrapped, matching the typed signals.
Bump dev.64.
…on complete The CDK Overlay surface for the lifecycle snackbar failed to mount with NG0201 because ComponentPortal was constructed without an injector, so Angular looked up AppApplicationActionsService in the environment injector chain instead of the component-scoped chain that actually provides it. Capture the parent Injector at service-construction time and pass it as the third argument to ComponentPortal so the standalone overlay component resolves its providers. Visibility was also too short — the prior wiring relied on TailwindSnackBar defaults (~4s), not enough to read a multi-line "verb + cf/org/space" message. Replace that with an explicit 10s linger window driven by a showProgress signal: true on op begin, cleared 10s after terminal state, cancellable so back-to-back ops don't inherit a previous timer's tail. Total visible duration = op_duration + 10s with a 10s floor. Drop the redundant terminal MatSnackBar.open() calls — the persistent overlay already carries the outcome (success checkmark / failure × + error text), and keeping two surfaces was double feedback. Add a fan-out refresh of AppDetailDataService at the end of every lifecycle op (success and failure paths) so the Status card and other signal-native consumers update immediately instead of waiting for the 45s idle-poll tick.
CF returns 400 from /v3/apps/:guid/processes/web/stats when the app is
STOPPED, but our refresh('all') was firing all four fetches in parallel —
so on a freshly-loaded STOPPED app we'd always log a 400 in the console
and the stats signal would settle into an error state.
Split refresh('all') into two phases:
1a) parallel: app + summary + env (cheap, always safe)
1b) await 1a, then conditionally fetch stats only when state is
STARTED or unknown
Add shouldFetchStats() helper and update the spec to await tick() between
phases. The Status card path is unchanged — it still calls fetchStats()
directly when needed.
The Instances card was rendering <app-running-instances>, which reads the
running count from a legacy ngrx paginator. That paginator isn't refreshed
by the signal-native fetchStats() path, so after a stop/start the Status
card would correctly show 1/1 RUNNING while the Instances card sat at 0/1
until a write operation incidentally dispatched dispatchAppStats() (or the
45s idle-poll fired).
Switch to two computed signals reading from AppDetailDataService.stats():
runningCount = stats().filter(s => s.state === 'RUNNING').length
desiredCount = app()?.entity?.instances ?? 0
Inline the "{{ running }} / {{ desired }}" rendering and drop the
<app-running-instances> import. Same source of truth as the Status card,
so the two cards now move together.
Two pages-load races were producing console noise and a TypeError:
1. getBreadcrumbs() crashed on .name when endpoints[cfGuid], org.entity,
or space.entity were still undefined during the first emission of
the combineLatest. Add a filter that requires all four to be present
before mapping.
2. The favourite$ stream emitted with info.entity.entity.cfGuid undefined
during the same window, causing a flood of "endpointId is required"
warnings from the favorites helper. Tighten the filter to require
cfGuid populated before subscribing downstream.
Both fixes are filters, not error handlers — the right shape was already
arriving on subsequent emissions; we just needed to wait for it.
The app-detail page currently reads through the v2 proxy because the
Jetstream-native handlers for single-app reads weren't built. To close
that gap, define the Stratos-shape composed envelopes the new handlers
will return:
StAppDetail — /pp/v1/cf/apps/{cnsi}/{appGuid}
StAppSummary — /pp/v1/cf/apps/{cnsi}/{appGuid}/summary
StEnvVars — /pp/v1/cf/apps/{cnsi}/{appGuid}/env
StAppStat — /pp/v1/cf/apps/{cnsi}/{appGuid}/stats
Plus the v3 sub-resource shapes the composed envelopes embed:
StProcess — /v3/apps/:guid/processes/web
StDroplet — /v3/apps/:guid/droplets/current (+ StDropletBuildpack)
StPackage — /v3/apps/:guid/packages (most recent)
StBuild — /v3/apps/:guid/builds (most recent)
Type-only commit; no runtime change. The handlers, data-service switch,
and template migration land in subsequent commits per the slice 1
commit-4 delta plan.
These shapes also become the template for slices 2..N: any future detail
page (service-instance, org, space) follows the same composed-envelope
pattern with sub-resources embedded into one Stratos response.
Two new Stratos-native handlers in native_apps_detail.go close the gap
the slice 1 commit-4-incomplete state left:
GET /pp/v1/cf/apps/{cnsi}/{appGuid} — basic StApp passthrough
GET /pp/v1/cf/apps/{cnsi}/{appGuid}?return=summary — list-summary row
GET /pp/v1/cf/apps/{cnsi}/{appGuid}?return=details — full StAppDetail
GET /pp/v1/cf/apps/{cnsi}/{appGuid}/env — StEnvVars
The detail endpoint follows the same ?return= mode convention list
endpoints already use (counts/summary/recent on /cf/apps/:cnsi).
?return=details composes the full envelope from /v3/apps/:guid +
/processes/web + /droplets/current + /packages + /builds + /features/ssh
with concurrent fan-out via errgroup; per-source failures surface in
_meta.unavailable rather than failing the envelope.
This is a wire contract for slices 2..N — every detail endpoint added
by a future migration (service-instance, org, space) must expose the
same default / summary / details modes. Sub-resources that carry
qualitatively different content (env, routes, bindings) keep their
own paths.
Tests cover all three return modes, the per-source-failure tristate
path (missing droplet surfaces droplet/stack in _meta.unavailable
while the rest of the envelope returns 200), and the env handler.
…pEnvVarsState) Stratos-shape (v3-composed) is the canonical wire contract; this file manufactures the v2-flavored APIResource<IApp> / IAppSummary / AppEnvVarsState / AppStat shapes that unmigrated ngrx consumers still depend on. The data service emits StAppDetail signals; ApplicationService (facade shim) routes each legacy *$ accessor through the relevant stToLegacy.X converter so unmigrated tabs see no API change. Four converters land in this commit, each independent: stToLegacy.appDetail StAppDetail → APIResource<IApp> stToLegacy.appSummary StAppDetail → IAppSummary (derived; no /summary fetch) stToLegacy.envVars StEnvVars → AppEnvVarsState stToLegacy.appStats StAppStatsInstance[] → AppStat[] Each entry is intentionally one function — slices 2..N add their own (stToLegacy.serviceInstance, stToLegacy.organization, etc.) following the same shape. The file is the v3-shim graveyard: as each migration lands, that consumer's converter loses its last caller and gets deleted with the migration. The file dies when the last consumer moves to direct v3 reads. 17 spec cases cover the field-level mappings, including the tristate paths (droplet null → buildpack/detected_buildpack stay undefined, pkg null → package_state empty, build error → staging_failed_description).
The data service primary signals are now Stratos-shape composed envelopes
(StAppDetail + StEnvVars + StAppStat[]) sourced from Jetstream-native
handlers — /pp/v1/cf/apps/{cnsi}/{appGuid}?return=details,
/pp/v1/cf/apps/{cnsi}/{appGuid}/env, /pp/v1/cf/app-stats/{cnsi}/{appGuid}.
This closes the slice 1 commit-4 gap that left the Summary tab reading
v2-shape inline-relation fields the new handler didn't populate.
Three structural changes:
1. AppDetailDataService holds V3 signals (appDetail, envVars, stats) as
the canonical state. Legacy app() and summary() are computed views
that call stToLegacy.appDetail / stToLegacy.appSummary so unmigrated
cards reading the legacy APIResource<IApp> shape keep working.
Space / org / domains stay V2-shaped — those migrate when the
org/space detail pages do.
2. ApplicationService facade routes appStats$ through stToLegacy.appStats
so consumers (auto-scaler, app-monitor) see the legacy AppStat shape
they expect, populated with real per-instance metrics, not zero-fill.
3. /cf/app-stats wire shape widened end-to-end. The original trimmed
{index, state} wasn't enough for the auto-scaler / app-monitor /
Instances-tab consumers that read uptime / cpu / memory / disk. Both
the Go StAppStatsInstance struct and the frontend StAppStat type now
carry the full v3 stats payload (uptime + quotas + usage{cpu,mem,
disk,time}). cf-apps-signal-config.service still reads only state for
its running-count, ignoring the extra fields.
Also fixes three pre-existing test breakages exposed by the shape change:
AppApplicationActionsService (Bug 2 commit added the AppDetailDataService
inject for refresh-on-op-complete; spec was missing the provider stub),
CardAppInstancesComponent (Bug 14 commit switched to read dataService
directly; spec was missing the provider), and ApplicationTabsBaseComponent
(transitively needed AppDetailDataService through actions service).
Stratos data model is the canonical wire contract: the adapter
manufactures legacy V2 shapes from V3 signals; the V3 signals never
manufacture V2 in reverse. When a slice's templates migrate to read
appDetail() directly, that consumer's adapter callsite goes with them.
build-tab.component.html now reads from data.appDetail() and the
nested process / droplet / pkg / build / app sub-objects instead of
going through the legacy APIResource<IApp> adapter view. Each
binding maps directly to the V3 wire shape:
app.entity.memory → detail.process?.memoryMb
app.entity.disk_quota → detail.process?.diskMb
app.entity.command → detail.process?.command
app.entity.ports → detail.process?.ports
app.entity.health_check_* → detail.process?.healthCheck*
app.entity.package_state → detail.pkg?.state
app.entity.package_updated_at → detail.pkg?.updatedAt
app.entity.staging_failed_… → detail.build?.error
app.entity.buildpack → detail.droplet?.buildpacks?.[0]?.name
app.entity.stack?.entity.name → detail.droplet?.stack || detail.app.stackName
card-app-uptime drops the ApplicationMonitorService dependency and
computes max/min/avg uptime directly from data.stats() on the V3 shape
(per-instance uptime arrives on every StAppStat now that the wire shape
was widened in the previous commit). Renders the not-running placeholder
when data.running() is false, mirroring the previous behavior.
card-cf-info migrates to toSignal() bridges over the existing
cfEndpointService observables. The data path was already V3-native
(the cfInfo entity catalog effect hits /pp/v1/cf/info/{cnsi} per
cloud-foundry.effects.ts:30-35) — this commit moves the consuming card
off `| async` pipes onto signal reads. fetchAutoscalerInfo stays
deferred to ngOnInit so test setups that don't register the autoscaler
catalog can still construct the component.
The v3-to-legacy adapter gains two field lifts to close coverage gaps
the existing template was reading through the adapter:
detected_start_command ← process.command (V3 collapses both)
docker_image ← droplet.image (docker-lifecycle apps)
After this commit, the Summary tab + the three cards (status,
instances, uptime) read appDetail() directly. The legacy app() and
summary() adapter views on AppDetailDataService are now consumed only
by ApplicationService's facade bridges — when those bridges retire as
unmigrated tabs migrate, the adapter views can be deleted.
Splits the application-delete flow into two halves so the actual delete
runs on the app detail page (not from inside the wizard):
1. Action bar Delete button → routes to the wizard
2. Wizard walks through Routes + Service Instances + Confirm
- Routes/Bindings steps still capture selections via setSelected*
- Confirm step's button text is "Confirm" (was "Delete")
- Confirm submit STASHES selections in AppDeleteSelectionService and
navigates back to /applications/{cf}/{appGuid} — no delete fires
from the wizard
3. ApplicationBaseComponent watches selection.requested() via effect
4. On flip → app page calls actions.deleteWithCleanup(routes, bindings)
5. deleteWithCleanup opens the "Are you sure?" ConfirmationDialog
6. On Yes → orchestrated cleanup + delete runs as one DELETING
lifecycle event with three progress stages:
CLEANUP_ROUTES → Promise.all(deleteRoute)
CLEANUP_BINDINGS → Promise.all(deleteServiceBinding)
DELETE_APP → deleteApp
The progress overlay shows the whole sequence under one verb so the
operator sees route removal as part of the delete, not a separate op.
7. On success → router.navigate(['/applications']) (app wall)
Why this matters:
- Old flow called deleteApp BEFORE deleting routes/bindings (cascading
was fire-and-forget). CF v3 stalls /v3/apps/:guid delete jobs while
routes are still mapped — the writeWithJob await never terminated and
the Confirm step hung indefinitely. Reordering so cleanup runs first
removes the stall condition.
- Wizard no longer carries the deletion logic — its only job is
selection. The app page owns the actual lifecycle event.
runLifecycleAction gains a 'delete' verb that maps to the DELETING
LifecycleVerb. Post-success refreshes (entity GET, stats fetch, data
service refresh) are skipped for delete because the app is gone — the
onAfter callback handles navigation instead. Other verbs are unchanged.
The legacy startDelete arrow + its spec test are removed. They were
"retained for spec compatibility" but the field never had a non-test
caller after the FWT-957 stepper migration switched to signalHandle.submit.
Per the no-skipping-defined-symbols rule (feedback_no_remove_unused),
removing only because the user explicitly asked to fix delete and the
arrow no longer matches the redesigned shape.
Coverage:
- New AppDeleteSelectionService spec (4 cases)
- Updated application-delete.component spec (4 cases verifying wizard
stashes selections + does NOT call deleteApp)
- Updated application-base.component spec to stub the new dependencies
via TestBed.overrideComponent
The dev.65 deploy build failed at the angular-compiler step with three TS errors that vitest's transform pipeline didn't catch: 1. v3-to-legacy-adapter.ts read env.SystemProvided / Environment / ApplicationProvided / RunningProvided / StagingProvided (PascalCase) but StEnvVars declares those fields as camelCase. Aligned the reads to camelCase to match the type. 2. The same adapter's appStats() emitted `uris: []` inside an AppStat literal whose `uris` is `string[]`. Implicit any[] inference was accepted by vitest, rejected by the prod build (TS7018). Cast to `[] as string[]`. 3. application-actions.service.ts deleteWithCleanup's onProgress callback emitted a partial StratosJob via `as StratosJob` cast. StratosJob requires id, kind, startedAt, updatedAt — vitest let the cast through, prod build rejected (TS2352). Populated every required field on the literal and dropped the cast. 4. application.service.ts appSummary$ read detail.loading().summary and detail.errors().summary — but EntityKind dropped 'summary' in the V3 cutover (the data service no longer fetches /summary separately; StAppDetail carries every summary field). Re-routed to the 'app' kind since that's the fetch summary now piggybacks on. 5. app-detail-data.service.ts stratosProject computed read env.Environment (PascalCase) instead of env.environment. Lesson recorded in feedback_vitest_misses_tsc_strict.md: vitest passes do not guarantee a clean prod build. For typed cutover work, run `bunx tsc --noEmit -p packages/cloud-foundry/tsconfig.lib.json` before declaring done — the angular-compiler strict-mode catches what vitest swallows in ~5s instead of the ~10-min deploy round-trip.
…tial load
ApplicationStateService.get() returns the Unknown fallback shape
{label:'Unknown', indicator:ERROR, actions:null} when called with
null app + null stats — which is the steady-state during the first
~100ms of an app-detail page load before AppDetailDataService's
appDetail / stats signals settle. The action-bar template was reading
appState.actions.restart / .stop / .start / .restage unconditionally
inside the @if (applicationState$ | async; as appState) block, so
every change-detection tick during that window threw
"Cannot read properties of null (reading 'restart')".
The error didn't break functionality (the buttons render and become
disabled correctly once the real state lands) but it flooded the
DevTools console and stack-traced through every lifecycle effect tick.
Wrap the actions sub-block in @if (appState.actions; as appActions)
and read appActions.X. The block simply doesn't render until the
Unknown fallback gives way to a real state — same UX, no console noise.
Surfaced during dev.66 local validation on adepttech connection.
ApplicationStateService.stateMetadata dispatches on V2 package_state
strings (STAGED / PENDING / FAILED) — those are the only keys it
recognises. The adapter was pulling V3's pkg.state directly into the
legacy package_state slot, which surfaces V3 vocabulary
(READY / PROCESSING_UPLOAD / AWAITING_UPLOAD / etc.) that the state
table doesn't know about.
For a STARTED app with V3 pkg.state="READY", the lookup
stateMetadata['STARTED']['READY'] missed → fell through to the Unknown
fallback {label:'Unknown', actions:null} → action-bar's @if guard
hid the Start/Stop/Restart/Restage buttons entirely.
Map V3 inputs onto V2 vocabulary the state table understands:
- droplet.state === 'STAGED' → 'STAGED' (running app)
- build.state === 'FAILED' → 'FAILED'
- pkg.state === 'FAILED' | 'EXPIRED' → 'FAILED'
- everything else (in-flight / no droplet) → 'PENDING'
Apply in both stToLegacy.appDetail (IApp.package_state) and
stToLegacy.appSummary (IAppSummary.package_state) so both consumers
see the same translation.
Surfaced during dev.66 local validation — operator buttons were
missing on the Summary tab. Documented in feedback_vitest_misses_tsc_strict
that vitest also doesn't catch this kind of vocabulary-mismatch bug
because the adapter spec was asserting the (incorrectly) passed-through
V3 string and assertion logic was internally consistent. Updated the
spec to assert the V2 vocabulary translation explicitly.
Migrate app-detail Summary tab to V3-shape signal reads. Status card cluster (state, memory, disk, instances) plus in-flight stage-row sourced from action service. Lifecycle snackbar with stage-row + linger; settling poll on instances/stats post-action; envVars refresh on start / restart / restage. Delete stepper survives parameterized-route reuse strategy (AppDeleteSelectionService root-provided); confirm step labels resolve from seed at trigger time, sync signal fallback, no GUID rendering. Cloud-foundry endpoint service exposes a sync signal mirror alongside endpoint$ to kill the firstWithFallback race on parent recreate. Backend: lookupWebProcessGUID wraps capi.ErrNotFound so deleted-app stats polling returns 404 not 502. See 2026-04-21-frontend-signal-migration-pattern.md for slice 1 retrospective and pre-flight checklist for subsequent slices.
Three remaining endpoint$ async-pipe consumers migrated to the sync mirror exposed by CloudFoundryEndpointService.endpoint(). Drops the local toSignal bridge in card-cf-info and the async-pipe reads in card-cf-user-info + cloud-foundry-tabs-base. Test mock for card-cf-user-info updated from of(...) observable to signal(...) to match the template's sync read. Item #1 of pre-slice-2 sweep doc 2026-05-03-pre-slice2-sweep.md.
Specs lagged behind slice-1 implementation changes: - card-app-status spec asserted text-content-secondary for STOPPED; slice 1 changed the implementation to text-warning (the visibility fix) — assertion updated. - application-actions spec mocked AppDetailDataService with only refresh; slice 1 added sync signal reads (app/org/space/stats/ summary/lastPolledAt). Added makeDataServiceStub() with full signal surface, plus endpoint signal mirror on the CF endpoint stub. - app-detail-data spec MOCK_ENV used legacy V2 capitalised Environment / SystemProvided; the V3 wire shape (StEnvVars) is lowercase environment / systemProvided. - app-delete-selection spec called setPending with two args; slice 1 expanded the signature to (appGuid, target, routes, bindings). Updated all call sites + added a seed() coverage case. All 782 cloud-foundry vitest tests pass locally.
Two layers of fix:
1. Defensive aliases (tailwind.config.js)
- Extract shade scales (brand, success, warning, danger, info, accent)
as named constants.
- Add the matching shaded scale into each semantic singleton: warning,
danger, success, info, accent, primary, error. Tailwind no longer
silently drops the shaded-singleton form (text-warning-600 etc.).
primary aliases onto brand (no primary-shade). error aliases onto
danger (project canonical).
- New tentative semantic token (gray) for status icons that mean
"uncertain / initialising" (autoscaler events, kube analysis report
severity, application state when runtime hasn't reported yet).
2. Canonical 1B sweep across templates and code
- text/bg/border/ring-{warning,success,danger,info,accent}-{n} →
text/bg/border/ring-{name}-shade-{n}
- text-primary-{n} → text-brand-{n}
- text/bg/border-error-{n} → text/bg/border-danger-shade-{n}
- 8 one-offs: bg-card-header-bg → bg-content-secondary,
bg-surface-hover → bg-content-secondary, bg-warning-soft →
bg-warning-shade-50, border-content-border-strong →
border-content-border, text-content-primary → text-content-text,
text-accent-dark → text-accent-shade-700, text-nav-muted →
text-nav-text-muted, text-theme-text → text-app-text.
One signal-detail spec asserted bg-success-100; updated to
bg-success-shade-100 to match the canonical class string.
DEVELOPER_GUIDE.md examples updated to teach the canonical *-shade-{n}
form so the guide stops modelling the old drift.
custom-form-field.component.ts bg-input-border / bg-input-focus-border
/ text-input-focus-border deliberately not changed — input.border /
input.focus-border tokens DO resolve, and the agent flagged them as
likely-intentional underline strokes.
Item #5 of pre-slice-2 sweep doc 2026-05-03-pre-slice2-sweep.md.
Mirrors slice 1's structure for the per-app Instances tab. Framework primitives (reusable across remaining slices): - removeRow(cnsi, guid) on MergeOrchestrator + removeItem on CnsiEntitySource — synchronous, idempotent, no HTTP. Drives post-delete row eviction without re-fetching. - raiseFocusPriority(kind) on AppDetailDataService — refcount-aware Set so multi-consumer release stays correct; bumps stats poll to 5s while a consumer holds priority, falls back to settling cadence when released. Instances tab surfaces: - AppInstanceActionsService — per-instance verbs (killInstance) with transitioningIndex signal + inFlight computed; mirrors slice-1 AppApplicationActionsService shape. - card-app-usage rebuilt signal-native (computed aggregates over data.stats(); dropped legacy ApplicationMonitorService dep; SCSS deleted). - card-app-instances reskinned (signal-driven counts + scale/edit buttons; SCSS deleted; legacy ngrx subscriptions removed). - CfAppInstancesSignalConfigService configures the signal-list framework with index/state/since/cpu/memory/disk/host columns + per-row Kill row-action. - instances-tab template wires <app-signal-list>, kill confirm dialog, and focus-priority raise/lower on ngOnInit/ngOnDestroy. ApplicationMonitorService deletion deferred — non-Instances consumers (build-tab, cf-autoscaler) still import it. Sweep ticket to track.
Two downstream consumers of the wave-1 removeRow primitive land together; both target the App Wall surface. #3 Post-delete row eviction: AppApplicationActionsService.runLifecycleAction now calls this.apps.orchestrator?.removeRow(cfGuid, appGuid) on delete success. Eliminates the 1-2 transient 404 console-noise entries from the stats poller racing against the gone-app's row. #4 Priority-ordered org-batched space-name resolver: CfAppsSignalConfigService.loadNames replaces the bulk per_page=500 spaces fetch (which hit the catalog cap on multi- thousand-space CFs and rendered "—" for spaces beyond page 1) with org-batched chunks of 20 orgs per request via ?organization_guids=g1,g2,... Priority order: orgs of apps on the first ~2 list pages first (so visible "—"s flip on first paint, awaited), remaining orgs in any order through a worker pool of 3 concurrent chunks per CNSI (fire-and-forget). Generation counter aborts stale drains on initialize() re-call. Jetstream native_handlers.go gains an organization_guids query- param passthrough on getNativeSpaces (mirrors the existing guids filter; ~5 lines + Go test). V3 supports the filter natively; capi.WithFilter(...) variadic was already proven in getNativeOrgSpaces. Cap rationale: ORGS_PER_SPACES_CHUNK=20 keeps URLs ~740 chars, SPACES_CHUNK_CONCURRENCY=3 per CNSI bounds gorouter pressure.
Two surface-level guards to stop the per-app Routes tab from crashing while it waits for slice-3 signal migration. cf-app-routes-list-config-base.ts: switch the canEditAppsInSpace switchMap from app$ to waitForAppEntity$ — the slice-1 V3 swap changed app$'s emission timing so its first emission has entity: undefined, and the legacy switchMap dereferenced app.entity.entity.space_guid on it. cf-routes-data-source-base.ts: defensive guard around the V2- nested domain.entity.name read. V3 routes carry only relationships.domain.data.guid — the embedded domain object is gone. Bail on the legacy enrichment path when it's missing rather than crashing the whole list. Result: Routes tab loads without crash. List body still renders empty for V3 routes (the entity.url filter at line 80 drops rows whose url isn't computed by the legacy enrichment). Slice 3 fixes this properly by reading StRoute.url directly — the V3 wire already carries a CF-rendered url field, so no domain resolution is needed for display. The legacy gymnastics in this file were V2-specific.
Add a guid-batch app-name resolver to support the Apps-Attached column on the per-app Routes tab and any other surface that needs name lookup without a full apps-list drain. Backend: extend getNativeApps with ?guids=g1,g2,... passthrough, mirroring the existing getNativeSpaces pattern. When the guids filter is present, skip per-app processes/spaces/routes enrichment — name resolution doesn't need the fan-out. Frontend: signal-backed AppNameResolverService with batch coalescing (one HTTP request per microtask flush per CNSI), in-flight dedup, multi-CNSI cache isolation, and resolveMany() partial-cache-hit handling. Mirrors slice-2 framework primitives in shape. Specs: 6 vitest cases covering cache hit/miss, batch coalesce, dedup, multi-CNSI isolation, and partial bulk hits. Go test TestGetNativeApps_GuidsFilter asserts the upstream forward + the enrichment-skip path.
Extend AppDetailDataService with a per-app routes signal and a removeRoute(guid) cache-eviction hook. Symmetrical with the existing stats/envVars/domains read-side surface; routes joins as a new EntityKind in the fetchers/_loading/_errors records. removeRoute(guid) is the slice-3 echo of slice-2's orchestrator-level removeRow primitive — single-CNSI, in-memory, idempotent. The action service's verb success path (next commit) will call it; the data service stays focused on read state plus the eviction hook. Specs: 7 vitest cases covering null-before-load, populated-after-fetch, HTTP error surfacing, sync row removal, no-op on absent guid, no-op before any load, and refetch-after-remove repopulation.
Per-row action service for the Routes tab — sibling to slice-2's AppInstanceActionsService. unmapRoute uses the synchronous DELETE path; deleteRoute routes through writeWithJob so the transitioning signal holds across the 202+job settle window and the row spinner stays put until the route is truly gone. Cache eviction is intentionally NOT inside this service — the component layer wires verb-success to dataService.removeRoute(guid), mirroring slice-2 where killInstance left the orchestrator update to its consumer. Tab-scoped (not providedIn:'root') so transitioningRouteGuid resets cleanly on tab navigation. Specs: 9 vitest cases — idle state, mid-flight signal, URL shape, HTTP 500 cleanup, error surfacing, reentrancy guard, deleteRoute 200 fast-path, 202+job COMPLETE settle, FAILED job rejection.
Signal-list configuration for the Routes tab — mirrors CfAppInstancesSignalConfigService from slice 2. Columns: Host, Domain (parsed from server-rendered StRoute.URL — no domain catalog needed), Path, Port (TCP routes), Apps-Attached (via the resolver primitive from #1), Actions kebab. Domain is parsed inline from URL minus Host minus :Port minus Path because StRoute.URL is CF-rendered server-side. Apps-Attached defaults to the current appGuid when StRoute.AppGUIDs is empty (per-app routes endpoint doesn't populate it; future backend work will fill it from r.Relationships.Destinations). Per-row Unmap + Delete buttons; both disabled when actionsService.inFlight() is true. Sort defaults to createdAt desc matching legacy. No header actions — attach/create defer to a later slice. Specs: vitest coverage of column shape, row-action wiring, in-flight disable, sort/filter handlers, and Apps-Attached defaulting.
Replace the legacy <app-list> with <app-signal-list> on the per-app Routes tab. Providers swap from the ListConfig factory to [AppRouteActionsService, CfAppRoutesSignalConfigService] (both tab-scoped). Drop the orgDomains$ pre-fetch — URL is server-rendered so domain enrichment is no longer needed. Verb-success cache eviction wires at the per-row callback level: the component opens a confirm dialog, awaits the action service verb, then calls dataService.removeRoute(guid) on success. Mirrors slice-2 where the kill-instance success callback updated the orchestrator from the component layer rather than from inside the action service. Spec rewritten to mirror the slice-2 instances-tab harness: confirm dialog gating, verb invocation, post-success removeRoute call.
Delete the legacy CfAppRoutesListConfigService + spec; remove its public_api export. Routes tab is the only consumer (verified by grep) and slice 3 #5 swapped it for the signal-native config. The shared base CfAppRoutesListConfigBase, cf-app-routes-data-source, and cf-app-map-routes-list-config.service stay in place — they're still extended/imported by the map-routes (attach existing) and delete-app-routes flows, which migrate in a later slice.
The bail-out path on the V3-shape guard added in 066a4da returned APIResource<IRoute> instead of APIResource<ListCfRoute>, breaking the production Angular build's strict type-check. (Vitest typecheck was lax enough to let it through; gate parity is a separate follow-up.) Same runtime behavior — just match the existing cast pattern used a few lines above where already-enriched rows are passed through.
The gate (bun run gate-check) was lint + vitest, which let strict-mode TS errors caught only by ng build's Angular compiler slip through to make build (and CI). Slice 3 #5 surfaced this when a V2-shape guard's bail-out path returned the wrong narrowed type — green gate, red production build. Append `bun run build` to gate-check so the gate now matches what the deploy pipeline actually rejects. Adds ~60-90s per gate run; worth it to fail fast before push. Update Makefile help text to reflect the new step.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
End-to-end V2 → V3 cutover for cf-stratos. The Stratos backend's CF interactions on the user-traffic path are now V3-only; the frontend reads from V3-native Jetstream handlers (
/pp/v1/cf/...) returning Stratos-shape DTOs (StApp,StOrg,StSpace,StServiceInstance,StServiceBroker,StStack,StBuildpack,StRoute,StRole,StUser, …) over single-resource and bounded-pagination endpoints.Supersedes #5307. Every commit on #5307 is contained in this branch — ancestry confirmed. Once #5308 merges, #5307 will be closed. Dev iterations v5.0.0-dev.10 → dev.46.
What's in here
Signal-native infrastructure
EndpointDataService/OrgDataService/EndpointDataRegistry— per-endpoint and per-org signal stores with parallel fetchEndpointDataShim— write-through bridge from Stratos-shape to NgRx for incremental migrationPage migrations to signal-native list
ListStateStore.bind()SignalListConfig framework additions
headerActionsslot for page-level actions (Manage Roles, Invite User…)maxVisiblefor cells with hundreds of segmentsApp wall improvements
getNativeAppsenriches the default page with web-process memoryRead-side V3-native sweep (A0 → A6)
Read paths for ~14 entities migrated to V3-native Jetstream handlers:
?return=countsfor bounded countsSingle-resource endpoints alongside list endpoints (no auto-drain),
?guids=first-class, uniform paginated envelope. Wire contract follows the six principles forced by the spaces-504 incident (no unbounded list responses, single-resource alongside list,?guids=first-class, uniform envelope, no auto-drain, filters narrow at CAPI).Write-side V3 cutover
stratosjobs.Tracker+JobTranslatorinterface,RunFastPathhybrid sync/handoff wrapper (/pp/v1/stratosjobs/{id}for client polling)CFJobTranslatorfor single CF v3 jobs (/v3/jobs/{guid})/v2/apps/{guid}→/v3/apps/{guid}(last user-traffic v2 callsite removed)A8 — Restage V3 orchestration (downtime path)
Restage previously routed through a
/v2/apps/{guid}/restagepassthrough — the only atomic restage CF ever shipped. CF v3 has no atomic restage; cf-cli v8.18.3 implements it as a 7-step composition: package lookup → build create → build poll → set droplet → stop → start → instance poll. This branch implements that composition behind the Stratos async-job contract:RestageJobTranslatoradvances the state machine by one stage per Tracker.Refresh poll (so HTTP handlers stay short while staging takes minutes)advanceRestage(ctx, client, ref)— orchestrator state machine, downtime pathgetNewestReadyPackage,createBuildForPackage,pollBuildUntilTerminal,setCurrentDroplet,stopApp,startApp,getWebProcessGUID+pollInstancesUntilRunningcommand/v7/restage_command.go+command/v7/shared/app_stager.go. When upstream changes, the maintainer follows the numbered procedure at the top ofnative_apps_restage_v3.go.Wire shape preserved — frontend's existing
writeWithJob(restage)call incf-apps-signal-config.service.tsstill works with the new V3 backend. Rolling/canary strategy is accepted at the wire but not yet honored by the orchestrator (deferred — see "Out of scope").A9 — userinvite v2 → v3
Four CF API calls in the userinvite plugin migrated:
POST /v2/usersPOST /v3/usersPUT /v2/organizations/{org}/users/{user}POST /v3/roles {type:"organization_user"}PUT /v2/spaces/{space}/{role}/{user}POST /v3/roles {type:"space_<role>"}GET /v2/organizations/{org}/user_roles?q=user_guid:XGET /v3/roles?organization_guids&user_guidsV2
CF-UaaIdTakenidempotency check replaced with V3UniquenessError(code 10016 + title/detail substring fallback). Bug fix:processUserInviteno longer callsAssociateUserWithOrgtwice (:=shadowing in the legacy guardedif).Backend V3 cutover for the frontend's
/v2/infoNew native handler
GET /pp/v1/cf/info/:cnsiGuidcomposing/v3/info+ the unversioned API root/(withLink.Metaextracted from the latter). Returns a Stratos-shape JSON whosesnake_casefields mirror the legacyICfV2Infoso the frontend cutover is a pure URL swap —cloud-foundry.effects.ts:40now calls/pp/v1/cf/info/{cnsi}instead of/pp/v1/proxy/v2/info. No frontend consumer (CF Summary card,hasSSHAccess$) changes.The dual-probe of
/v2/infoand/v3/infoincloudfoundry/main.gostays — it's intentional capability detection (V2-only vs V3-only vs both), not an unmigrated callsite. Documented inline.A7 — cf-cli v8 bump
code.cloudfoundry.org/cli/v8 v8.18.2 → v8.18.3. Indirect dependency tracking — cf-cli v8 itself uses capi v3 internally, so updating it tightens our V3 alignment without direct API changes.FWT-18 — User Role + Users count
CF Summary user-role and users-count derived from V3
StUsersnapshot rather than ngrx pagination drain.Bug fixes
endpointDataServiceheld in a plain field;orgs/spacescomputeds tracked only the first endpoint's signal forever. Fix wraps the field inWritableSignalso the computeds re-track on CNSI swap. Affectscf-orgs-signal-configandcf-spaces-signal-config.signal-list.component.html— favorite/actions columns share an empty header, so tracking bycol.headerproduced duplicate""keys on every CD pass. Switched all six call sites tocol.key. Console quieter, rendering noticeably faster.[favorite]race with[entityConfig]— converted favorite to a setter that records whether the parent bound it, and moved the entity-monitor fallback subscription tongOnInitso the race no longer overwrites a parent-supplied favorite via the entity-stamped cfGuid.pageSizeOptionslist (table-mode 25 was leaving the dropdown showing 6 while 25 cards rendered).authTypesdefault toUsernamePassword + Noneso plugin-package endpoints (metrics, autoscaler) render their Connect step instead of crashing onObject.keys(undefined).transformutility on modal content was creating a containing block and breakingposition:fixedmath).markForCheck'd after busy resets so retry works under OnPush + zoneless.Dependency bumps
@stratosui/devkit→ Angular 21 toolchaingolang.org/x/cryptoacross three plugins (clears 6 moderates)code.cloudfoundry.org/cli/v8 v8.18.2 → v8.18.3(A7)github.com/norman-abramovitz/fw-capi/v3 v3.216.4-fix-apps-delete.6 → .7(Link.Meta support)fw-capi dependency
This branch consumes
norman-abramovitz/fw-capi/v3 v3.216.4-fix-apps-delete.7. Two upstream PRs againstfivetwenty-io/capicarry the fork's changes:*Jobfrom Location header on Delete/Scale/Start/Stop/Restart)Link.Meta map[string]interface{}(used by/pp/v1/cf/infoto readapp_ssh.meta.host_key_fingerprintetc.)Once those merge, a follow-up Stratos PR will drop the
replacedirective ingo.modand reference upstream directly.Test plan
make check gate) — frontend lint + vitest (1442 tests across 588 files), backendgo vet+go test ./...dev.46deployed): connection, navigation, list readsOut of scope (queued for follow-up tickets)
deployment_create+deployment_pollorchestrator stages. Wire acceptsstrategy: rolling|canarybut orchestrator only drives the downtime path today.POST /v3/apps/{guid}/actions/rollbackhandler.CFEndpointMetadata.SupportsV3._meta.unavailableemission policy — deferred until ≥3 concrete tristate cases force a shape decision (today onlyStServiceBroker.authUsername).make check gate, contract fixtures, optionalmake check gate-extraintegration target. Tracked inKS docs/2026-04-30-gate-improvements-assessment.md.Notes for review
git logto find what's gone..7tag from the fork; upstream merges land via follow-up.fw-capi version pinning
The
.7tag is onnorman-abramovitz/fw-capi:fix-apps-delete-returns-job. After the upstream PRs merge, the plan is:replaceto the main-derived tag (eliminates "Stratos points at a feature branch" fragility).fivetwenty-io/capihas the changes, drop thereplacedirective entirely.Tracked separately so this PR's review isn't gated on upstream PR acceptance.