Skip to content

V2 → V3 frontend cutover (signal-native migration) - #5308

Merged
norman-abramovitz merged 466 commits into
developfrom
feature/cf-v2-frontend-cutover
May 4, 2026
Merged

norman-abramovitz merged 466 commits into
developfrom
feature/cf-v2-frontend-cutover

Conversation

@norman-abramovitz

@norman-abramovitz norman-abramovitz commented Apr 28, 2026 •

Copy link
Copy Markdown
Contributor

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 fetch
  • EndpointDataShim — write-through bridge from Stratos-shape to NgRx for incremental migration
  • Stratos-shaped TypeScript DTOs and tolerant entity reader for forward compatibility
  • Session-level endpoint concurrency config exposed to frontend
  • Field-rename codemod for V3 sweep batches

Page migrations to signal-native list

  • Endpoints, marketplace, services wall (multi-CNSI)
  • CF organizations tab, organization spaces tab
  • CF space routes/apps/service-instances/users tabs
  • CF users page
  • CF-level routes page (new)
  • 15 paginated lists migrated from legacy ngrx paginators to ListStateStore.bind()

SignalListConfig framework additions

  • headerActions slot for page-level actions (Manage Roles, Invite User…)
  • Compound maxVisible for cells with hundreds of segments
  • Per-row kebab action menu
  • Per-mode tuple state (card vs table) so each view has its own pagination
  • Page-size snap on view-mode toggle
  • Scroll restoration on tab switch

App wall improvements

  • Favorite-star column, running/desired instances column
  • Text filter targets name/state/CF-Org-Space
  • Cross-tab navigation from summary cards
  • Compound CF/Org/Space cell with stacked links
  • App-stats refresh after scale to unstick Instances tab
  • getNativeApps enriches the default page with web-process memory

Read-side V3-native sweep (A0 → A6)

Read paths for ~14 entities migrated to V3-native Jetstream handlers:

  • A0 — V3-native pipeline framework substrate, single-resource adapter, V3-aware org/space/quota service layer
  • A2–A5 — V3-native wire swap across 12 read entities (with one revert + retry to land cleanly)
  • A6 — Deep-relations batch for Apps, Spaces, ServiceInstances; AppSpace + ServicePlanName joins; org private domains via V3 handler
  • A6.3 — Spaces drain filtered by all-orgs guids for cold-cache predictability
  • All read handlers converted to V3 paging-only with ?return=counts for bounded counts

Single-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

  • Stratos async-job contract: stratosjobs.Tracker + JobTranslator interface, RunFastPath hybrid sync/handoff wrapper (/pp/v1/stratosjobs/{id} for client polling)
  • CFJobTranslator for single CF v3 jobs (/v3/jobs/{guid})
  • Lifecycle write handlers: delete app/org/space/route/role/service-binding/service-instance, app start/stop/restart, scale, set-droplet, plan visibility, manage-org/space, user-provided service instances, audit events (per-org + per-space)
  • Metrics permission probe migrated /v2/apps/{guid} → /v3/apps/{guid} (last user-traffic v2 callsite removed)
  • Phase 1C completion of missing V3 write handlers
  • Action URLs aligned with V3-native Jetstream handlers

A8 — Restage V3 orchestration (downtime path)

Restage previously routed through a /v2/apps/{guid}/restage passthrough — 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:

  • RestageJobTranslator advances 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 path
  • 7 unit-tested helpers: getNewestReadyPackage, createBuildForPackage, pollBuildUntilTerminal, setCurrentDroplet, stopApp, startApp, getWebProcessGUID + pollInstancesUntilRunning
  • Reference + drift-detection procedure pinned in source: cf-cli v8.18.3 command/v7/restage_command.go + command/v7/shared/app_stager.go. When upstream changes, the maintainer follows the numbered procedure at the top of native_apps_restage_v3.go.

Wire shape preserved — frontend's existing writeWithJob(restage) call in cf-apps-signal-config.service.ts still 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:

Old (v2) New (v3)
POST /v2/users POST /v3/users
PUT /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:X GET /v3/roles?organization_guids&user_guids

V2 CF-UaaIdTaken idempotency check replaced with V3 UniquenessError (code 10016 + title/detail substring fallback). Bug fix: processUserInvite no longer calls AssociateUserWithOrg twice (:= shadowing in the legacy guarded if).

Backend V3 cutover for the frontend's /v2/info

New native handler GET /pp/v1/cf/info/:cnsiGuid composing /v3/info + the unversioned API root / (with Link.Meta extracted from the latter). Returns a Stratos-shape JSON whose snake_case fields mirror the legacy ICfV2Info so the frontend cutover is a pure URL swap — cloud-foundry.effects.ts:40 now 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/info and /v3/info in cloudfoundry/main.go stays — 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 StUser snapshot rather than ngrx pagination drain.

Bug fixes

  • CF org/space favorite stars leaking across CF endpoints sharing one CAPI — root cause was endpointDataService held in a plain field; orgs / spaces computeds tracked only the first endpoint's signal forever. Fix wraps the field in WritableSignal so the computeds re-track on CNSI swap. Affects cf-orgs-signal-config and cf-spaces-signal-config.
  • NG0955 track-by spam in signal-list.component.html — favorite/actions columns share an empty header, so tracking by col.header produced duplicate "" keys on every CD pass. Switched all six call sites to col.key. Console quieter, rendering noticeably faster.
  • Meta-card [favorite] race with [entityConfig] — converted favorite to a setter that records whether the parent bound it, and moved the entity-monitor fallback subscription to ngOnInit so the race no longer overwrites a parent-supplied favorite via the entity-stamped cfGuid.
  • Orgs page-size default 25 → 24 so the card-mode default lands in the card pageSizeOptions list (table-mode 25 was leaving the dropdown showing 6 while 25 cards rendered).
  • Connect-endpoint dialog stuck after failed connect — Previous now re-enabled with auto-unregister so the form can re-submit cleanly.
  • Empty endpoint authTypes default to UsernamePassword + None so plugin-package endpoints (metrics, autoscaler) render their Connect step instead of crashing on Object.keys(undefined).
  • Auth-type dropdown panel anchored to its trigger inside the register modal (Tailwind transform utility on modal content was creating a containing block and breaking position:fixed math).
  • Stepper markForCheck'd after busy resets so retry works under OnPush + zoneless.
  • TailwindDialog panel uses opacity-only open animation (transform-based animation also created a containing block on every dialog).
  • CF SSH using process GUID not app GUID.
  • App instances duplicate rows + button displacement.
  • PostCSS nesting warning, deploy-step2 test crash.
  • CNSI GUID missing from CF pagination cache keys.
  • Services-wall multi-CNSI fan-out — dropped unused methods replaced by signal-native pipeline.

Dependency bumps

  • @stratosui/devkit → Angular 21 toolchain
  • golang.org/x/crypto across 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 against fivetwenty-io/capi carry the fork's changes:

Once those merge, a follow-up Stratos PR will drop the replace directive in go.mod and reference upstream directly.

Test plan

  • Full local gate (make check gate) — frontend lint + vitest (1442 tests across 588 files), backend go vet + go test ./...
  • Backend unit coverage: 18 tests for restage orchestrator, 7 for JobTranslator, 3 for CF info handler, full handler-shape coverage on every native read/write
  • Adepttech smoke (dev.46 deployed): connection, navigation, list reads
  • Adepttech full smoke: app wall, marketplace, services wall, routes, CF users, per-space tabs (apps, routes, service-instances, users)
  • App actions: scale, restart, restage, delete (async-job path)
  • Endpoint connect/disconnect flow incl. failed-connect recovery
  • Cross-endpoint favorite walk-through: navigate between three CF endpoints sharing one CAPI; org-stars must reflect each endpoint's own DB state, not whichever endpoint loaded first
  • Restage end-to-end (Playwright on local Stratos against adepttech)
  • User-invite end-to-end (Playwright)
  • E2E suite (local + adepttech)
  • E2E coverage for newly migrated signal-native pages — follow-up tickets per migration

Out of scope (queued for follow-up tickets)

  • A8 rolling/canary path — deployment_create + deployment_poll orchestrator stages. Wire accepts strategy: rolling|canary but orchestrator only drives the downtime path today.
  • A8 rollback — POST /v3/apps/{guid}/actions/rollback handler.
  • A8 capability gating — V3-only operations (rolling/canary/rollback) should reject cleanly on V2-only CFs based on CFEndpointMetadata.SupportsV3.
  • A10 Revisions UI — frontend revisions panel + rollback button.
  • A11 V3 audit — sweep of remaining write paths (env-var groups, health-check, isolation segments, events filtering corner cases).
  • Metrics dual-mode V2/V3 — first dual-mode handler, separate workstream.
  • Backend _meta.unavailable emission policy — deferred until ≥3 concrete tristate cases force a shape decision (today only StServiceBroker.authUsername).
  • Gate improvements — capability-gate tests in make check gate, contract fixtures, optional make check gate-extra integration target. Tracked in KS docs/2026-04-30-gate-improvements-assessment.md.

Notes for review

  • 300+ commits intentionally preserved — each migration is a discrete reviewable unit. The branch retains its fine-grained history rather than being squashed into a monolith.
  • Commits follow the convention of listing dropped legacy features in the body so a reader can git log to find what's gone.
  • This PR is not gated on the upstream fw-capi PRs (Integrating helion-ui-framework #2, Integrating helion-ui-theme #3) being accepted. Stratos consumes the .7 tag from the fork; upstream merges land via follow-up.

fw-capi version pinning

src/jetstream/go.mod:
  github.com/fivetwenty-io/capi/v3 v3.216.4-fix-apps-delete.3 (declared)
  replace ... => github.com/norman-abramovitz/fw-capi/v3 v3.216.4-fix-apps-delete.7

The .7 tag is on norman-abramovitz/fw-capi:fix-apps-delete-returns-job. After the upstream PRs merge, the plan is:

  1. Merge fw-capi feature branch → fw-capi/main, retag from main.
  2. Bump Stratos replace to the main-derived tag (eliminates "Stratos points at a feature branch" fragility).
  3. Once upstream fivetwenty-io/capi has the changes, drop the replace directive entirely.

Tracked separately so this PR's review isn't gated on upstream PR acceptance.

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.
FWT-959 Part 2 (Partition C). 3-step Manage Roles wizard with the
optional first Usernames step kept conditional via the existing
@if (setUsernames) template guard. Each step's handle delegates to
the corresponding child via @ViewChild — setUsernamesStepHandle and
modifyStepHandle bridge the children's valid$/blocked$ observables
into local signals (ngAfterViewInit subscriptions + markForCheck) so
the stepper re-evaluates reactively. confirmStepHandle preserves the
applyStarted / ignoreSuccess two-click apply semantic via submit()
returning { ignoreSuccess: true } on the first click and explicit
Router.navigateByUrl on the second — same pattern as remove-user.
applyStarted promoted to a signal-backed getter/setter so the handle's
canClose / disablePrevious / destructiveStep / finishButtonText
fields are computed() over it. Legacy startApply StepOnNextFunction
removed in the same diff.
FWT-959 Part 2 (Partition C). Final 4-step wizard in this partition.
Cross-step state stays in EditAutoscalerPolicyService — children read/
write the policy via setState/getState as before. Each step's handle
delegates to the corresponding child via @ViewChild — step1Handle
bridges editLimitForm validity through a new valid$ observable, and
step{2,3,4}Handle's valid/disablePrevious are computed() over local
signals fed by each child's new editIndex$ BehaviorSubject (the
private editIndex field is now a getter/setter wrapping the subject so
templates and call-sites stay unchanged). Bridge subscriptions are
wired inside each handle's onEnter (deferred via setTimeout(0) so the
freshly-stamped child is queryable) and torn down in onLeave +
ngOnDestroy because the steppers component instantiates only the
active step's child at a time. Step 4's submit awaits step4.updatePolicy
and translates the legacy redirect result into an explicit
Router.navigateByUrl(parentUrl).
FWT-959 framework fix. SteppersComponent.setActive() invoked
step.onEnter() directly — bypassing pOnEnter, the helper added in
Part 1 (commit 0204495) that prefers signalHandle.onEnter over
the legacy @input default no-op. The result: Shape 3 wizards that
wired bridge subscriptions or child onEnter delegation through
signalHandle.onEnter never received the entry callback, leaving
their bridges dead and child onEnter side-effects unfired. Bug was
masked by spec smoke-tests which don't navigate between steps.

Replace the two raw step.onEnter(this.enterData) call-sites in
setActive() with step.pOnEnter(this.enterData). pOnEnter's internal
null-check supersedes the now-redundant if (step.onEnter) guards.

Side-effect: legacy wizards with [destructiveStep]="true" now run
the 1-second busy-flag visual delay on step entry (the long-
documented intent of pOnEnter). Affects two pre-migrated consumers
(application-delete, detach-service-instance); behaviour matches
the design comment in step.component.ts:299-318.

invokeLeave (line 265) and invokeNext (line 148) were already
wired correctly — only onEnter needed routing.
Production ng build surfaced three TS strict errors that vitest's
permissive TS pass missed (per feedback_gate_ts_strict_gap.md):

- manage-users:116 — drop result.message access; modify.onNext() returns
  a narrower union without message field, and the throw doesn't need it.
- kube-config-registration:92 — pass (0, null as any) to importer.onNext;
  StepOnNextFunction signature requires (index, step) arguments even
  though the importer's implementation ignores them.
- upgrade-release:184 — annotate map((): void => undefined) so the
  function expression has an explicit return type.

All three live in already-shipped FWT-959 Part 2 commits. Catching
them now before queuing more migrations on top.
FWT-959 Part 2 (Partition B). 3-step CF/org/space → app-name → route
flow. Cross-step state stays in CfOrgSpaceDataService + ngrx
(SetCFDetails / SetNewAppName) — children read/write through the
existing observable surface. Each step's handle delegates to the
matching child via ViewChild setters that wire bridge subscriptions
the moment the lazily-stamped child becomes available and tear them
down when the user navigates away. step1's blocked field mirrors
cfOrgSpaceService.isLoading$ via toSignal(); step2's setter calls
v.onEnter() to replicate the legacy [onEnter] binding so the
appName control re-validates on entry; step3's setter seeds and
mirrors setDomainHost.statusChanges into a local valid signal.
step3.onNext keeps dispatching its own RouterNav on success — no
explicit Router.navigate replacement needed.
FWT-959 Part 2 (Partition B). Up-to-6-step source-select → source-
config → upload → options → deploy flow. Step 1 (Cloud Foundry)
stays gated by the existing @if (!appGuid) template guard; steps
2_1 and 2_2 keep their legacy [hidden] / [skip] bindings (the
binding for step2_2.skip$ silently passes undefined today and is
preserved as-is). Each step uses a ViewChild setter so bridge
subscriptions wire when the lazily-stamped child appears; step3's
showBusy is derived from the same disablePrevious signal that
mirrors status.deploying — no extra child API needed.

The signal-handle submit() contract drops the legacy enterData
data channel, so the parent now captures step2_2's deployer into
pendingDeployer and forwards it into step3.onEnter(deployer) on
the next ViewChild fire — preserving the file-upload deployer
hand-off the legacy stepper performed framework-side.
FWT-959 Part 2 (Partition B, final). 5-step service flow
(CF → service → plan → bind → instance) plus user-provided 3-step
variant. Cross-step state stays in CsiGuidsService / CsiModeService
+ SetCreateServiceInstance* ngrx actions — children unchanged.
Mode branching (showSelectCf / showSelectService / showBindApp)
remains gated by @if blocks so no skipIf needed; bind-app's
skipApps$ is mirrored into the handle's skipIf signal.

ViewChild setters wire bridge subs on lazy step activation:
selectCF.validate, selectService.isFetching$, specifyDetails.
validate / serviceInstancesInit$. selectService / selectPlan /
bindApp / supd already expose validate as a Signal so handles
just computed-wrap the optional ViewChild reference. The legacy
[onEnter]="resetStoreData" on the CF step is replicated inside
the selectCF setter. The select-plan → bind-app / specify-details
plan hand-off (which the legacy stepper carried via enterData) is
captured parent-side in selectedPlan and forwarded by the
downstream setters — gated by pending*Enter flags so the eager
initial ViewChild fire doesn't call onEnter with stale state.
FWT-959 Part 2 follow-up. The migration of deploy-application dropped
the data channel between step2's source-select submit and step2_2's
source-upload onEnter. Legacy stepper relayed step2.onNext.data
(a FileScannerInfo) into step2_2.onEnter via the framework enterData
field; signal-handle submit() has no return-value channel, and the
agent assumed step2_2.onEnter could ignore the data — it cannot.
The unprimed deployer.fsFileInfo throws at deployer.fsFileInfo.root
during file-upload deploys (deploy-application-deployer.ts:486).

Capture step2.onNext.data into a parent-side pendingFsFileInfo and
forward via the step2_2 ViewChild setter — same pattern already in
place for the step2_2 → step3 deployer hand-off (pendingDeployer).
git-url and docker-image source paths bypass step2_2 so leaving
the field undefined in those flows is correct.
FWT-959 Part 2 follow-up. The original migration wired bridge
subscriptions inside signalHandle.onEnter, deferring the ViewChild
read via setTimeout(0) so the freshly-stamped child became readable.
Macrotasks aren't tracked by zoneless Angular CD — works in practice
because cdr.markForCheck inside the subscriber compensates, but the
shape is fragile under future CD changes.

Replace with @ViewChild setters (same pattern used by Partition B's
deploy-application / add-service-instance / create-application). The
setter fires whenever the ref becomes available — synchronously
right after the *ngTemplateOutlet swap — and again with undefined
on step exit, so bridge subscription lifecycle is automatic. Drops
the setTimeout indirection and the duplicate teardown logic between
onLeave and ngOnDestroy.
FWT-959 Part 2 follow-up. add-service-instance declared two boolean
fields that are never assigned or read anywhere in the codebase —
inherited from a pre-migration state. The template's step gating
reads modeService.viewDetail.show* directly, not these fields.
FWT-959 Part 2 follow-up. validateSub (subscribed inside the
helper.hasUpgrade() callback to mirror validate$ into the version-
step handle's signal) was never torn down — listConfig.versionsData
Source.selectedRows$ is a hot stream that would keep emitting after
component destroy until GC. Implement OnDestroy + unsubscribe.
FWT-959 Part 2 follow-up. The migration hardcoded /endpoints inside
the review step's submit() Router.navigate call to mirror the
template's <app-steppers cancel="/endpoints"> attribute. Drift risk
if the cancel attr is ever changed. Extract a readonly cancelUrl
field and bind both the stepper attr and the navigate call to it
— matches manage-users / remove-user's defaultCancelUrl pattern.
FWT-959 Part 2 follow-up. The block comment claimed the conditional
first step was surfaced as a hidden field on the SignalStepHandle.
Actually it's gated by the @if (setUsernames) template guard;
setUsernames is set in the constructor and never flips, so a static
template gate suffices and no signal-handle hidden field exists.
Comment-only fix.
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.
Adds a 'radio' kind to SignalListColumn with SignalListRadioBinding
exposing selectedKey (WritableSignal<string|null>) plus optional
isDisabled. Renders a single-select radio in both card and table
modes. AddRoutesComponent's map-existing flow needs a tab-scoped
single-select picker; the stepper-local cf-map-routes config wires
into this slot in commit #4.
Mirrors slice 3's removeRoute: appends a freshly-attached route
without re-fetching. Mutates BOTH route signals so each consumer
view updates synchronously — _routes drives the Routes tab list
(slice 3 read path), _appDetail.app.routes drives the build-tab
Routes count metadata-item. Idempotent on guid match per signal;
no-op on _routes when null so the in-flight slice 3 drain isn't
raced. Specs cover both signals plus partial-state convergence.
Adds three sync verbs to the slice-3 service: attachRoute (PUT
/pp/v1/cf/apps/{cnsi}/{app}/routes/{route}, empty 200),
createRoute (POST /pp/v1/cf/routes/{cnsi}, returns StRoute),
and createAndAttachRoute which composes them. Orphan-on-attach
fail surfaces a thrown Error with the orphan StRoute attached;
caller (AddRoutesComponent commit #5) re-throws verbatim so the
stepper snackbar displays the orphan GUID + URL.
Tab/page-scoped @Injectable() service that builds the SignalList
config for the map-existing-route picker. Drains via the new
/pp/v1/cf/routes/{cnsi}?space_guids=... wire (commit #4.5).
Columns: radio + host + domain + path + port + apps. Disables
rows already mapped to the current app by reading
AppDetailDataService.routes() lazily inside isDisabled.
listAllRoutes now forwards a space_guids filter (full-list branch
only; counts stays cnsi-wide as the deliberate fast path).
Verified live against /v3/routes (3.180.0) — adepttech served 1
route filtered vs 47 unfiltered. toStRoute reads inline
r.Destinations from the list response and populates AppGUIDs
directly; deletes the populateRouteDestinations helper which was
N+1 against CAPI for no reason. Tests cover counts ignoring the
filter, full-list forwarding the URL-encoded comma-delimited
guids, and zero /destinations path hits in the route DTO mapper.
Replaces ngrx cfEntityCatalog.route.api.create +
.application.api.assignRoute with AppRouteActionsService verbs.
Map-existing flow swaps MapRoutesComponent for <app-signal-list>
driven by CfMapRoutesSignalConfigService (commit #4). Mode toggle
refreshes the picker on entry to 'map'. Orphan-on-attach errors
re-throw verbatim so the stepper snackbar shows GUID + URL.
Stepper provider list adds AppRouteActionsService and
CfMapRoutesSignalConfigService; the legacy ListConfig provider
is dropped (legacy guard test asserts its absence). Legacy
MapRoutesComponent retired in commit #6.
Three small adjacent fixes folded together:

- application.service.ts hydrates legacy data.cf via withLatestFrom
  on endpointEntitiesSelector so consumers reading cf?.name (e.g.
  build-tab CF endpoint row) display the name not the GUID.
- build-tab.component.html now renders the resolved endpoint name
  with the GUID as fallback.
- cf-entity-generator getGuid falls back to entity.guid when the
  V2 metadata wrapper is absent, matching the V3 flat shape that
  arrives via the native routes wire.
- native_apps_detail.composeAppDetails now fetches the per-app
  routes in the same errgroup as droplet/processes/package/build/
  ssh and stamps them onto StAppDetail.App.Routes, so the build
  tab's Routes count metadata-item populates without an extra
  client roundtrip.
- VERSION + package.json roll to dev.67 to cover the bundle.
Removes 11 files now orphaned by the signal-native flow:

  Map-existing flow (replaced by <app-signal-list> in #5):
    map-routes/map-routes.component.{ts,html,spec.ts}
    cf-app-map-routes-list-config.service.{ts,spec.ts}

  Dead delete-app routes picker (replaced by AppRoutesPickerComponent
  in slice 3, never deleted):
    delete-app-routes/delete-app-routes.component.{ts,html,spec.ts}
    delete-app-routes/app-delete-routes-list-config.service.ts

  Shared base classes whose last consumers retired above:
    cf-app-routes-list-config-base.ts
    cf-app-routes-data-source.ts

The two leaf folders (map-routes/, delete-app-routes/) are removed
along with their contents. Migration-history comments in
add-routes.component.ts and add-route-stepper.component.{ts,spec.ts}
are kept — they document the V2 → signal-native swap.
Stamps slice 3.5 (commits #1–#6 + Bucket B) for adepttech deploy.
CF v3's rendered route URL is bare (host.domain[/path] or domain:port
for TCP). Bucket B started populating StAppDetail.app.routes from the
v3 list response — before that, the app-detail signal had no routes
at all and AppDetailDataService.url() returned null, leaving the
action-bar Visit button greyed out. Now it returns the bare URL,
which the <a [href]> on application-action-bar.component.html:61
interprets as a relative path — clicking opens
localhost:5440/<bare-host>/ in a new tab and 404s.

Prepend https:// at AppDetailDataService.url() unless the URL
already carries a scheme. Modern CF deployments serve all HTTP
routes over TLS, matching the legacy getRoute(... browsable=true,
secure=true ...) behaviour. Specs updated to assert the prefix and
the no-double-prepend case.
After CardAppInstancesComponent.scaleApp() resolves, refresh both
the app-detail signal (carries the new desired count → denominator)
and the stats signal (carries the per-container state list → running
count + total) in parallel. The previous code dispatched a legacy
ngrx action (cfEntityCatalog.appStats.actions.getMultiple) which
doesn't feed the signal-native _appDetail / _stats sources, so both
the Status card "M/N running" line and the Instances card "M/N"
display lagged the focus-poll cadence by up to 5 seconds and could
disagree (e.g. status shows 1/2 while instances shows 1/1) until the
poll caught up. Explicit signal refreshes restore the legacy "matches
quickly" behaviour.

cfEntityCatalog import dropped — the component no longer touches
ngrx for any verb.
Adds a new SignalList column kind 'gauge' that renders an
<app-usage-gauge> per cell — a horizontal bar that fills with the
0..1 fraction and turns yellow at warningAt / red at errorAt.
SignalListGaugeBinding<T> exposes a value extractor, optional
valueText override (defaults to percentage), and threshold
constants. Wired into both card-mode and table-mode renders;
truncate guards updated to keep the gauge cell from being clipped.

CfAppInstancesSignalConfigService switches the Memory, Disk, and
CPU columns from plain text to kind: 'gauge' with the legacy
warningAt=0.8 / errorAt=0.9 thresholds. Memory and Disk derive a
0..1 fraction from used/quota via a small helper that returns 0
when quota is missing; CPU usage from CF arrives as a fraction
already and is capped at 1 to absorb the occasional >100% spike.
The render() callback still emits the human-readable "14 / 128 MB"
string for the filter/title fallback, so list filtering by
substring keeps working unchanged.

Restores the V2 instances list visual that the slice-2 migration
lost when it dropped <app-table-cell-usage>.

@krutten krutten left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merging for next DEV build

@norman-abramovitz
norman-abramovitz marked this pull request as ready for review May 4, 2026 22:08
@norman-abramovitz
norman-abramovitz merged commit fff9dc8 into develop May 4, 2026
12 checks passed
nabramovitz added a commit to nabramovitz/stratos that referenced this pull request Jul 1, 2026
getNativeOrgDomains filtered CF v3's organization_guids
result down to OwningOrgGUID==orgGuid, discarding every shared
and global domain. That query mixes owned + shared domains
(V2->V3 cutover, PR cloudfoundry#5308), and most orgs have no private
domains of their own, so the Add Route domain dropdown ended
up empty.

Only exclude domains privately owned by a different org; keep
global shared domains (no owner) and domains explicitly shared
with this org via SharedOrgGUIDs.

Adds TestGetNativeOrgDomains_KeepsSharedAndGlobalDomains, the
first coverage for this handler.
norman-abramovitz pushed a commit that referenced this pull request Jul 1, 2026
getNativeOrgDomains filtered CF v3's organization_guids
result down to OwningOrgGUID==orgGuid, discarding every shared
and global domain. That query mixes owned + shared domains
(V2->V3 cutover, PR #5308), and most orgs have no private
domains of their own, so the Add Route domain dropdown ended
up empty.

Only exclude domains privately owned by a different org; keep
global shared domains (no owner) and domains explicitly shared
with this org via SharedOrgGUIDs.

Adds TestGetNativeOrgDomains_KeepsSharedAndGlobalDomains, the
first coverage for this handler.
Bartan89 pushed a commit to Bartan89/stratos that referenced this pull request Jul 3, 2026
getNativeOrgDomains filtered CF v3's organization_guids
result down to OwningOrgGUID==orgGuid, discarding every shared
and global domain. That query mixes owned + shared domains
(V2->V3 cutover, PR cloudfoundry#5308), and most orgs have no private
domains of their own, so the Add Route domain dropdown ended
up empty.

Only exclude domains privately owned by a different org; keep
global shared domains (no owner) and domains explicitly shared
with this org via SharedOrgGUIDs.

Adds TestGetNativeOrgDomains_KeepsSharedAndGlobalDomains, the
first coverage for this handler.
norman-abramovitz pushed a commit that referenced this pull request Nov 12, 2026
The move to the signal list (#5308) made the Name column of the
Organization Quotas and Space Quotas lists plain text, so neither the
table name nor the card title opened the quota's page. Both columns
link again, to the paths the old quota cell used.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants