Skip to content

Commit 97d3f33

Browse files
Angular 21 upgrade (Angular 20 cleanup + Angular 21 + vitest 4) (#5286)
* Remove ComponentFactoryResolver (FWT-888) Replace deprecated Compiler/ngModuleFactory/ ComponentFactoryResolver pipeline with direct dynamic imports for home card components. Both CF and K8s entity generators now return the component class via import().then() instead of compiling module factories at runtime. * Replace first() with take(1) codebase-wide (FWT-907) Replace 295 bare first() calls with take(1) across 157 files to eliminate EmptyError exceptions in test environments. Add defaultIfEmpty guards on 15 critical paths (steppers, backup/restore, home page, user profile, deploy steps). Fix two NG0203 errors where toObservable() was called outside injection context in create-release and chart-values-editor. * Add ApplicationDeploySourceTypes provider (FWT-888) The CF home card component injects this service but it is not providedIn root. Previously supplied by CFHomeCardModule which is now bypassed by the direct dynamic import. Add to component providers and update spec to use overrideComponent. * Detect platform mismatch in dev backend (FWT-840) Check binary architecture before starting the dev backend server. Rebuild for the host platform if the existing binary was cross-compiled for deploy. * Remove dead wrapper NgModules (FWT-872) Delete CFHomeCardModule, KubernetesHomeCardModule, and MonocularModule. All wrapped components that are already standalone. No consumers remain after the ComponentFactoryResolver migration. * Guard against panic in K8s cert auth (FWT-872) Add bounds checking in extractCerts to prevent panic when request body has no colon delimiter. The body is consumed by BindOnce before reaching extractCerts — a pre-existing bug that needs a deeper fix to pass cert data as form fields. * Fix K8s auth body consumption bug (FWT-872) BindOnce in loginToCNSI consumed the request body before K8s auth handlers could read it. All handlers now read auth data from form fields instead of raw request body. Also strips embedded whitespace from token paste. * Add editable path and clear to file input (FWT-872) File input now supports typing a path, clearing a selected file, and browsing via file picker. * Convert K8s SCSS to Tailwind, fix chart colors (FWT-872) Convert 5 K8s component SCSS files to Tailwind utility classes. Restore missing chart color swatches lost during Tailwind migration — fixes invisible gauges in light mode. * Fix list filter, icon font, and side panel bugs Add Material Symbols Outlined font for icon rendering. Fix list filter not restoring all items by simplifying splitCurrentPage and removing switchMap re-subscription. Fix table expander TypeError when config is pre-evaluated. Fix resource viewer side panel with markForCheck on setProps. * Remove unused imports from AboutPageComponent * Convert 28 small K8s SCSS files to Tailwind (FWT-872) Batch convert remaining small SCSS files in the K8s package to Tailwind utility classes. Residual SCSS kept only for child component selectors and mat-icon sizing that Tailwind cannot express. * Fix resource viewer missing pipe and component imports Add TitleCasePipe, DatePipe, RouterModule, MetadataItem, and JsonViewer imports to KubernetesResourceViewerComponent. Missing imports caused template rendering to fail silently, producing empty side panel content. * Fix RouterLink import and null ConfigMap data guard Use RouterLink directive instead of RouterModule in standalone component imports. Guard against null data in ConfigMap/Secret entity column definitions. * Fix K8s home card counts not loading (OnPush) Add markForCheck after setting count observables in load() — OnPush change detection needs explicit trigger when observables are assigned after initial render. * Simplify service ports display, top-align table rows Replace nested sub-table with compact inline format showing port/protocol (name) → targetPort :nodePort. Change table row alignment from center to top for better readability with multi-line cells. * Fix missing cellDefinition binding in table rows Wire up [cellDefinition] on app-table-cell so columns with valuePath (clusterIP, type, replicas, etc.) render their values. Simplify service ports to inline format. * Migrate e2e suite to Playwright Port Protractor tests to Playwright with new page objects, fixtures, and helpers. Tests run against local Stratos (local auth) or remote (SSO); all core tests pass or skip gracefully when environment lacks CF data. - Add auth helpers: detectAuthType, browserLogin (local+SSO) - Add page objects: APIKeysListPage, EndpointsPage, etc. - Add list/table component helpers with findRow, getRowCount - Fix view-toggle detection: isEnabled() not isVisible() - Fix gate-check script: test-headless -> test * Fix autoscaler specs missing HttpClient provider StratosBrandingService now injects HttpClient; add provideHttpClient() to autoscaler component test setups. * Remove dead list helper, add type=text to filter input getCurrentPageStartIndex was unused; remove it with its tests. Add explicit type="text" to filter <input> so Tailwind @tailwindcss/forms class strategy styles it. * Use Tailwind forms class strategy, remove input shadows Switch @tailwindcss/forms to class strategy so it only styles elements with .input/.select, preventing accidental global input resets. Replace shadow-sm with shadow-none. * Migrate K8s/Helm package SCSS to Tailwind Delete empty SCSS files and remove their styleUrls/styleUrl references. Migrate SCSS content to Tailwind @apply directives or inline classes. Delete unused _mixins.scss and theme.scss. * Fix E2E test timing for CF-backed list tests Wait for isLoadingPage$ to clear (filter enabled) after goToAppsPage returns, so view toggles and filter input are interactable. Increase getItemCount timeout in list-filter and test timeouts for CF-latency-sensitive session memory tests. * Upgrade Angular 20.3.9→20.3.18, lodash-es, x/crypto Update Angular across all 8 frontend sub-packages from 20.3.9 to 20.3.18, fixing 5 XSS advisories (GHSA-58c5, GHSA-g93w, GHSA-jrmj, GHSA-prjf, GHSA-v4hv). Remove redundant Angular overrides from root package.json (sub-packages now declare correct version). Upgrade lodash-es to 4.18.0 (fixes GHSA-f23m, GHSA-r5fr). Bump golang.org/x/crypto to v0.49.0 in jetstream modules (fixes CVE-2025-47914, CVE-2025-58181). * Fix Firefox E2E failures and proxy /api-keys routing bug proxy.conf.cjs: change "/api/" key to "/api/v1" — Angular CLI strips trailing slashes from proxy keys, causing "/api/" to match "/api-keys" and forward it to the backend (404). Using "/api/v1" matches only actual API routes. auth.helper.ts: catch Firefox NS_BINDING_ABORTED on login redirect by retrying waitForURL — Firefox aborts the old binding mid-redirect but the navigation succeeds; the retry lets it complete. api-keys.spec.ts: wait for list or no-content element before branching — prevents false "not found" when the page is still loading after networkidle timeout. * ESLint config: vitest globals pattern + auto-fix (1826→788 warnings) Add varsIgnorePattern for vitest lifecycle imports in test files, eliminating ~974 false-positive no-unused-vars warnings. Run eslint --fix for auto-fixable rules (boolean cast, useless escape). Phase 1 of FWT-876 ESLint cleanup. * Fix ESLint warnings: eqeqeq, case-declarations, negated-async, types Mechanical fixes across 32 files (788→723 warnings): - Template == to === and != to !== (autoscaler, k8s) - Wrap switch case bodies with lexical declarations in { } - Replace !(obs$ | async) with === false/null as appropriate - Replace {} type annotations with Record<string, unknown> - Replace .hasOwnProperty() with Object.hasOwn() Phase 2-3 of FWT-876 ESLint cleanup. * Add check verb, FINAL and DRYRUN variables, remove bump release New features: - make check (lint|gate|tests|coverage|e2e) — quality gate verb - FINAL=strip — strip prerelease from version, persisted to package.json - DRYRUN=yes — cross-cutting dry-run variable (bump supported) - Remove bump release modifier (replaced by FINAL=strip on release) * Migrate remaining constructor injection to inject() function Convert 8 files from constructor parameter DI to inject() pattern. Handles super() calls, non-injectable string params (eslint-disable), and Effects classes. Completes FWT-874 inject migration (24→0 warnings). * Fix accessibility and remaining template ESLint warnings Add alt text to images, tabindex + keydown handlers for interactive non-button elements, associate labels with form controls. Fix stray eqeqeq in autoscaler step3. Completes FWT-875 accessibility fixes. 1826→681 warnings total (63% reduction across FWT-876). * Remove dead imports and expand ESLint unused-vars ignore patterns Remove 59 dead RxJS operator and Angular type imports across 50 files. Expand varsIgnorePattern for test files (fixture, component, etc.) and add argsIgnorePattern/varsIgnorePattern for underscore-prefixed params. 1826→572 warnings (69% reduction). Remaining 492 unused-vars across 244 files need per-file review in a follow-up session. * ESLint: ignore _-prefixed caught errors and spec vars Add caughtErrorsIgnorePattern: "^_" to both source and spec file rule blocks. Update spec varsIgnorePattern from literal "_" to "_\w*" to match underscore-prefixed names. Enables _-prefix convention for intentionally unused catch clause variables and test setup variables. * Fix TS build errors from inject() migration Subclasses still passed constructor args to base classes that were migrated to inject(). Service list configs passed 5 args where the base now takes only a URL string. Two value-position Record<string, unknown> typos crashed the compiler. - super(): drop now-empty args in autoscaler step 1-4 components, github-commits configs, cf-space-permission-cell - service-instances list configs: pass only the URL string arg - bind-apps-step: type bindingParams as Record<string, unknown> - helm-release-resource-graph, variables-tab: replace literal Record<string, unknown> in value position with {} - setup.actions SetupSuccess: payload type any (HTTP response object is not assignable to Record<string, unknown>) - table-cell.component: Type<Record<...>> -> Type<any> for ViewContainerRef.createComponent compatibility Build now succeeds; E2E core suite passes (71/0). * FWT-876: Reduce ESLint unused-vars warnings 532 to 5 Mechanical cleanup pass across 260 files. Five remaining warnings are intentional T type parameters on public API interfaces (CfAPIResource, IBaseListAction, TailwindSnackBarRef, UniquenessValidatorConfig, KubernetesPodTagsComponent). - Prefix unused function args, catch clause vars and assigned locals with _ (script-applied; destructured props use rename syntax: { name: _name } not { _name }) - Remove unused imports left over from inject() and take(1) migrations (Store, CFAppState, ChangeDetectionStrategy, first, ChangeDetectorRef, signals imports never wired up, etc.) - Remove dead local interfaces and helper functions (DomainFormModel, IValueLabels, getUniqueKeys, etc.) - Remove unused 'T' generics from non-public methods - Block-disable unused-vars on test framework barrel re-exports in core-test.helper.ts All unit tests pass; build clean; E2E core suite green. * Pin patched hono and vite via overrides GitHub Dependabot flagged 13 new alerts since last push: - hono < 4.12.12 (5 medium): cookie name handling, IP matching, serveStatic middleware bypass, toSSG path traversal - @hono/node-server < 1.19.13 (1 medium): same serveStatic bypass - vite <= 6.4.1 (7 high+medium): arbitrary file read via dev server WebSocket, server.fs.deny bypass, .map path traversal All come in transitively (hono via @angular/cli -> @modelcontextprotocol/sdk; vite via build tooling). Pin patched versions through package.json overrides so the bumps stay in semver-patch range without making them direct dependencies. vite stays in 6.x (no major bump). Build and gate check (1104 tests) pass. * Fix Helm chart browsing broken by standalone migration Two regressions surfaced when smoke-testing the helm/monocular pages against a real Helm repo: 1. createMonocularProviders required HTTP_INTERCEPTORS but the root app uses provideHttpClient(withInterceptors([...])) — the functional API does not register the legacy HTTP_INTERCEPTORS multi-provider. Result: NullInjectorError when navigating to /monocular/charts and chart-details. Fixed by marking the dependency Optional() and falling back to an empty array. 2. ChartDetailsComponent uses ChangeDetectionStrategy.OnPush but sets this.chart, this.currentVersion etc. inside an imperative subscribe(). With OnPush those assignments do not trigger a re-render and the chart-details page stayed blank. Inject ChangeDetectorRef and call markForCheck() in finalize(). Verified end-to-end against k3d + prometheus-community Helm repo: catalog renders, chart-details renders with README, version sidebar, dependencies, install instructions. * FWT-872: Convert helm/monocular SCSS to Tailwind, remove dead code Reduce helm/monocular SCSS from 477 lines to 54 lines (89%) while removing 651 lines of orphaned code from an older Helm chart browsing implementation that has been replaced by CatalogTabComponent + ListComponent + MonocularChartCard. Dead code removed (entire directories, no external refs): - monocular/charts/ (ChartsComponent, 372 lines) - monocular/chart-index/ (ChartIndexComponent, 110 lines) - monocular/chart-list/ (ChartListComponent, 68 lines) - monocular/list-filters/ (ListFiltersComponent, 57 lines) - monocular/app.component.scss (orphan, 44 lines) SCSS converted to Tailwind utilities in templates: - chart-details.component.scss (91 -> 0): header BEM block was dead code (header is rendered by entity-summary-title); remaining classes flattened to template - chart-details-usage.component.scss (46 -> 0): all classes except __install were dead; __install moved to template - chart-details-versions.component.scss (41 -> 0): internal table BEM classes inlined as Tailwind in template - chart-details-info.component.scss (23 -> 0): chartInfo BEM inlined; dead app-panel descendant rule removed - chart-item.component.scss (18 -> 0): entirely dead (.chart-item-info classes never used in template) - loader.component.scss (1 -> 0): empty placeholder SCSS kept but flattened (BEM removed, SCSS vars inlined): - list-item.component.scss (43 -> 35): content projection consumers (chart-item) reference these classes externally, so styles must apply via CSS rather than template Tailwind - panel.component.scss (19 -> 19): ngClass dynamic bindings reference --container/--background/--border modifiers Build clean. Smoke tested end-to-end against k3d + prometheus-community Helm repo: catalog (47 charts), filter, sort, pagination, and chart-details (README, version sidebar, maintainers, related links) all render identically. * FWT-872: Remove 34 empty K8s placeholder SCSS files Clean up empty .scss files left over from prior Tailwind migration steps. Each file contained only the comment "Styles moved to Tailwind classes in template" or was fully empty. Removed both the .scss file and the matching styleUrls entry from each component decorator. No template or class changes — purely removes references to files that contributed nothing to the bundle. * Remove 125 empty placeholder SCSS files across packages Same cleanup as the K8s pass, extended to the rest of the frontend. Each removed .scss file contained only a comment or was completely empty, with the styles long since moved into Tailwind utilities in the templates. Removed both the file and the matching styleUrls entry. By package: - cloud-foundry: 92 files - core: 26 files - example-extensions: 3 files - cf-autoscaler: 2 files - git, shared: 1 file each For two components (quota-definition, space-quota-definition) the styleUrls had a second array entry pointing at a shared quota-definition-base.component.scss — kept that entry and removed only the empty self-stylesheet. No template or class changes; bundle is unaffected. * WIP: ng update @angular/core@21 @angular/cli@21 * Angular 21 upgrade WIP * WIP: vite 7 + vitest 4 migration, peer deps bumped * Document Angular 21 upgrade WIP state Comprehensive status doc explaining what works (build, E2E, dev server) and what blocks completion (NG0203 in vitest 4 unit tests). Includes: - All version bumps applied - All code changes for Angular 21 breaking changes - The exact NG0203 failure mode and debugging hypotheses - Five things tried that didn't fix the test infra issue - Three resumption strategies (wait, invest a focused day, or abandon and revisit) - Resume-from-cold checklist with the relevant files This branch should be kept as a backup only — ship work continues on feature/Angular-21 (still at Angular 20.3.18) until @analogjs/vite-plugin-angular and vitest 4 catch up to Angular 21 + zoneless compatibility. * cf-stratos: fix NG0203 root cause + vitest 4 pool migration Under vitest 4 + Angular 21, two instances of @angular/core/fesm2022/_not_found-chunk.mjs were being loaded in the same process — one via Node's native ESM loader (triggered by externalized @ngrx/store) and one via VitestModuleEvaluator's transform pipeline. Each instance had its own module-level _currentInjector state, so NgRx Store's factory called getCurrentInjector() on the wrong instance and threw NG0203: StateObservable token injection failed. Force all Angular-dependent libraries through vitest's transform pipeline via server.deps.inline + ssr.noExternal so there's a single module graph: - /^@angular\// - /^@ngrx\// - /^@analogjs\// - 'ng2-charts' Also migrate the deprecated test.poolOptions block to vitest 4 syntax: - poolOptions.forks.singleFork: true -> maxWorkers: 3 (at test: level) - poolOptions.forks.isolate: false -> isolate: true (at test: level) isolate:true gives each test file a fresh VM context, preventing the cross-file state pollution that was masking real bugs under the old singleFork+isolate:false layout. maxWorkers:3 keeps wall time reasonable (~1.8x speedup vs single worker). Applied identically to all 8 per-package vitest configs. Effect on full suite: Before: 234 failed / 866 passed / 9 skipped (1109 total) After: 0 failed / 1100 passed / 9 skipped (1109 total) * cf-stratos: break circular @stratosui/core barrel imports in core/ When a file inside core/src/ imports from the @stratosui/core barrel (public-api.ts), loading that file triggers public-api.ts to evaluate, which re-exports shared.module.ts, which in turn imports the original file — before public-api.ts has finished initializing. The result is an undefined slot somewhere in SharedModule's imports array, which Angular 21 later trips over with: TypeError: Cannot read properties of undefined (reading 'ngModule') TypeError: Cannot read properties of undefined (reading 'ɵcmp') at isModuleWithProviders / isStandaloneComponent at SharedModule2.get / CreateEndpointModule2.get The `2` suffix is Angular's JIT compiler deconflicting what looks like two copies of the same module (really: one full copy + one partial copy from the cycle). Under Angular 20 + vitest 3 the NG0203 error fired first so these never surfaced; with that fixed, they became visible as 3 of the 5 remaining core failures. Fix: inside core/src/, import from direct relative paths instead of the @stratosui/core barrel. Seven production files affected: table-cell-endpoint-name.component.ts (CustomTooltipDirective) logout-page.component.ts (CardWrapperComponent) eula-page.component.ts (CustomTooltipDirective) events-page.component.ts (CustomTooltipDirective) backup-connection-cell.component.ts (CustomTooltipDirective) card-number-metric.component.ts (UtilsService) create-endpoint-helper.ts (CurrentUserPermissionsService, StratosCurrentUserPermissions, UserProfileService, SessionService) The @stratosui/core barrel is still the correct import path from other packages — only in-package imports need to be direct. * cf-stratos: initialize EntityCatalogHelper in 4 entity-touching specs Four component specs instantiate components whose constructors or input setters call stratosEntityCatalog.<entity>.store.*, which requires the EntityCatalogHelper singleton to have been set via EntityCatalogHelpers.SetEntityCatalogHelper(helper). Under the old isolate:false test layout these specs happened to inherit the helper from an earlier spec in the same fork; under the new isolate:true layout each file gets a fresh VM context and must initialize the helper itself. These were pre-existing test bugs masked by the NG0203 failures — three fail with 'EntityCatalogHelper not initialized' and one with 'Cannot read properties of undefined (reading \\'store\\')' once NG0203 is out of the way. Fixes: table-cell-endpoint-name.component.spec.ts - register stratos entities via entityCatalog.register() - inject EntityCatalogHelper and call SetEntityCatalogHelper() running-instances.component.spec.ts - add STORE_TEST_PROVIDERS (brings ENTITY_SERVICE_FACTORY_TOKEN and friends into the test providers) - inject EntityCatalogHelper and call SetEntityCatalogHelper() cloud-foundry-events.component.spec.ts - inject EntityCatalogHelper and call SetEntityCatalogHelper() (providers were already complete) helm-release-history-tab.component.spec.ts - mock HelmReleaseHelperService with useValue instead of providing the real one, avoiding its constructor's call into workloadsEntityCatalog.release.store. Mirrors the pattern already used by upgrade-release.component.spec.ts. With these in place the full vitest suite is green: 0 failed / 1100 passed / 9 skipped (1109 total) in 377s * cf-stratos: use strict equality in autoscaler policy templates @angular-eslint/template/eqeqeq flags loose `==` / `!=` in Angular templates and wants `===` / `!==`. The rule is correct — Angular templates should use strict equality to match TypeScript conventions. Under the angular-eslint 21.x + ESLint 9.x combo pulled in by the Angular 21 upgrade, the rule's auto-fix path also crashes with a TypeError when it encounters these operators: TypeError: Cannot destructure property 'source' of '(0, get_nearest_node_from_1.getNearestNodeFrom)(...)' as it is null. at getFix (@angular-eslint/eslint-plugin-template/dist/rules/eqeqeq.js:81) The crash aborts the entire lint phase, blocking `make check`. This commit fixes all 14 loose-equality instances across the 4 autoscaler edit-policy templates (step2, step3, step4, and the parent). No functional change — editIndex is always a number so `==`/`===` behave identically here. After this commit `bun run lint` completes with 115 warnings, 0 errors. * cf-stratos: fix nil res deref in metrics/main.go (go vet) The Angular 21 branch's new `make check` target runs `go fmt && go vet` as part of the lint gate (Makefile refresh from 2026-04-07). That surfaced a latent nil-pointer bug in plugins/metrics/main.go: res, err := httpClient.Do(req) defer res.Body.Close() // ← go vet flags this if err != nil || res.StatusCode != http.StatusOK { go vet's SA5011 check caught it: if httpClient.Do returns an error, `res` is nil and the deferred `res.Body.Close()` will panic when the function returns. The error branch never gets to execute. Reorder to check err first, then defer, then check status — mirroring the pattern already in use higher up in the same file (line 185-198): res, err := httpClient.Do(req) if err != nil { log.Errorf("Error performing http request: %v", err) return "", api.LogHTTPError(res, err) } defer res.Body.Close() if res.StatusCode != http.StatusOK { log.Errorf("Error performing http request - response: %v", res) return "", api.LogHTTPError(res, err) } Also includes a trailing-whitespace cleanup in datastore/20240818042100_RetryEndpointCACert.go picked up by `go fmt` during the same make check run. After this commit `go vet ./...` on the whole jetstream returns exit 0. * cf-stratos: bump version to v5.0.0-dev.4 for Angular 21 adepttech deploy Bumps package.json prerelease tag ahead of the CF deploy to console.run.adepttech.ca with the Angular 21 upgrade work. Ensures the About/Diagnostics page shows a unique version string so we can confirm the new build is live vs the previous dev.3 deploy. * cf-stratos: widen endpoint register dialog inputs The Name and Endpoint Address inputs in the Register Cloud Foundry Endpoint dialog were collapsing to ~180px regardless of the column width. The cause was a combination of: 1. The step-1 SCSS declared form.stepper-form .custom-form-field { max-width: 450px } — but `custom-form-field` is the root class inside <app-form-field>'s own template and Angular view encapsulation prevented this selector from piercing into the child component, so the rule was dead code. 2. <app-form-field>'s root <div> uses `inline-block w-full`. Without a block-level host, the `w-full` collapses to the host's intrinsic size (which is `inline` by default), and a hard `min-width: 180px` on .form-field-infix pinned the rendered width. Fix with Tailwind utilities directly on the affected tags — no ::ng-deep, no new SCSS (per the project's Tailwind-only policy): <app-form-field class="block w-full"> <input class="w-full" ...> </app-form-field> Remove the dead max-width rule from the step-1 SCSS to keep the file honest. After: Name and Endpoint Address fields fill their column (~450-500px at the default modal width). * cf-stratos: restore previous pageHeader portal on component destroy PageHeaderComponent is rendered via a TemplatePortal pushed into TabNavService.pageHeader (a signal, read by <app-show-page-header> in the app shell). Under the old implementation every instance: - ngAfterViewInit: setPageHeader(new portal) — overwrites - ngOnDestroy: tabNavService.clear() — unconditional wipe That assumes there's only ever one live page-header at a time. Violated when a component loaded inside the endpoint register modal (e.g. CreateEndpointComponent, K8s/Helm/Git registration components) has its own nested <app-page-header>. The inner header clobbers the outer's portal on mount, then on modal close its ngOnDestroy calls clear() — wiping the signal entirely. The outer endpoints-page's PageHeaderComponent is still alive but has no mechanism to re-register, so the whole page-header bar (title + Add button + theme/user menu) disappears until a hard browser refresh. Give each instance stack-like discipline without changing the service API: remember the previous portal on mount, restore it on destroy, and refuse to clear if someone else is the current owner. ngAfterViewInit() { this.previousPortal = this.tabNavService.pageHeader(); this.myPortal = new TemplatePortal(...); this.tabNavService.setPageHeader(this.myPortal); } ngOnDestroy() { if (this.myPortal && tabNavService.pageHeader() === this.myPortal) { if (this.previousPortal) { this.tabNavService.setPageHeader(this.previousPortal); } else { this.tabNavService.clear(); } } // Else: another component is the current owner; leave it alone. } This is the universal fix — protects against any nested page-header scenario regardless of which registration component is involved. * cf-stratos: defense-in-depth for endpoint register modal lifecycle Layered improvements around the register-endpoint modal so that the underlying Add button + page-header visibility bug can't resurface through a different code path. The universal fix is the prior commit (page-header portal restore); these are complementary hardening. Endpoints page (button visibility): - Replace `*appUserPermission="canRegisterEndpoint | async"` with a signal-backed `@if (canRegisterEndpoint())`. The directive + async pipe combo was sensitive to CD timing under zoneless Angular 21; a signal read is tied to the permission result and doesn't churn when unrelated state changes. - Drop UserPermissionDirective from the component's imports. UserPermissionDirective (defensive hardening for other callers): - Replace one-shot ngOnInit with ngOnChanges. Accept `null | undefined` for the input so it survives the first CD cycle when an async pipe hasn't emitted yet. Tear down the previous subscription and rendered template before re-subscribing. - Other templates that still use `*appUserPermission` now respect input changes instead of silently locking to the first value. Endpoint register modal (event handling + nested header): - Replace raw `document.addEventListener('keydown', ...)` with `@HostListener('document:keydown.escape')`. The raw listener bypassed Angular's event pipeline and did not trigger change detection under zoneless; the decorator is registered through Angular and fires CD correctly. Also fixes the memory leak from `.bind(this)` creating a fresh function that removeEventListener in ngOnDestroy couldn't remove. - When loading a registration component via ComponentRef (either the endpoint-specific path or the CreateEndpointComponent fallback), set `hideHeader = true` on the instance so its nested <app-page-header> is skipped. Prevents the visual duplication even on the happy path where portal restore would paper over it. CreateEndpointComponent: - Add `@Input() hideHeader = false`. - Wrap the template's <app-page-header> in `@if (!hideHeader)`. - Routed full-page usage keeps its header (default); modal usage opts out so there's no nested page-header inside the modal. * cf-stratos: e2e regression tests for register modal close paths Four Playwright tests covering every way a user can dismiss the endpoint register modal, pinning the invariant that the Add endpoint (+) button in the page header must remain visible across any modal open/close cycle: 1. Close via the footer Cancel button 2. Close via the header X icon 3. Close via the Escape key 4. Consecutive cycles (Cancel → X → Escape) to catch state pollution between cycles Each test opens the modal, verifies the overlay is visible, closes it via the target path, verifies the overlay is gone, and asserts the Add button is still visible. Regression guard for the whole class of bugs we just fixed: nested PageHeaderComponent clobbering the TabNavService signal, raw document.addEventListener bypassing zoneless change detection, and directive/async-pipe CD timing issues that caused the Add button to vanish after modal close. Implementation notes: - The `<app-endpoint-register-modal>` host element collapses to 0x0 because the actual modal content is fixed-positioned and taken out of flow. Use `toHaveCount(1)` for host presence, `toBeVisible()` on the inner fixed overlay div for visible-to-user assertions. - Uses the existing `adminPage` fixture — worker-scoped authenticated session, no per-test login cost. * cf-stratos: bump version to v5.0.0-dev.5 for adepttech redeploy Bumps the prerelease tag ahead of the next CF deploy to console.run.adepttech.ca, carrying the endpoint register modal lifecycle fixes (page-header portal restore, signal-backed Add button, HostListener for Escape, Tailwind-widened inputs). Unique version makes the About/Diagnostics page unambiguous about which build is live compared with the previous dev.4 deploy. * docs: update for shipped make bump lifecycle and check verb Cover prerelease stages (alpha/beta/prerelease), automatic build metadata, FINAL=strip, DRYRUN=yes cross-cutting variable, and the new make check verb with its five modifiers. * rename STRATOS_E2E_* env vars to drop redundant prefix Mechanical rename across 15 live files: STRATOS_E2E_BASE_URL → E2E_BASE_URL STRATOS_E2E_PROFILE → E2E_PROFILE STRATOS_E2E_ENV → E2E_ENV STRATOS_E2E_WORKERS → E2E_WORKERS The repo is named cf-stratos; the STRATOS_ prefix added no namespace value. Legacy Protractor-era CI scripts in deploy/ci/{travis,automation} are intentionally not renamed — they reference removed commands and do not run. Verified behavior-neutral: make check lint — clean make check gate — vitest 1100/9/0, go tests all pass make check e2e — 76/41/0 in 6.1 min (exact baseline match) * add E2E_BROWSERS, E2E_TRACE, E2E_VIDEO, E2E_SCREENSHOTS Make recipe variables for the e2e Playwright runs: E2E_BROWSERS=chromium,firefox,webkit pick projects (or "all") E2E_TRACE=on force trace capture E2E_VIDEO=on force video capture E2E_SCREENSHOTS=on force screenshot capture Also wire DRYRUN=yes (existing cross-cutting variable, previously only consumed by bump) to check.e2e and test.e2e — maps to playwright --list. Three helpers (_e2e_browsers, _e2e_flag, _e2e_toggle) cover all current and future e2e variables. Validation is delegated to Playwright (unknown projects surface "Project 'X' not found"). Defaults are unchanged: empty E2E_BROWSERS falls through to --project=chromium; empty trace/video/screenshot use the playwright.config.ts defaults. Verified: make -n check e2e [8 permutations] — helper expansions correct make check gate — vitest 1100/9/0, go tests all pass make check e2e — 76/41/0 in 6.1 min (exact baseline match) * docs: cover E2E_* recipe variables and DRYRUN extension Add "E2E recipe variables" subsection under Quality Gates documenting E2E_BROWSERS, E2E_TRACE, E2E_VIDEO, E2E_SCREENSHOTS with a values/example table and common-invocations block. Update the cross-cutting DRYRUN=yes entry to note it now applies to check e2e and test e2e as well as bump. * version-bump.sh: hide 'bump release' from user-facing usage text Word-conflict cleanup follow-through. The 2026-04-07 word-conflict resolution removed 'make bump release' from the Makefile's BUMP_MOD filter in favor of 'make release cf FINAL=strip'. But the underlying version-bump.sh script still advertised 'bump release' in its Commands list and Examples block, recreating the word-conflict signal at the script layer. Remove 'bump release' from the script's user-facing Commands list and Examples. The internal bump_release function and its case handler stay in place — they're called by the Makefile's FINAL=strip re-exec block at line 378. Added a short Note: pointing users to the canonical 'make release cf FINAL=strip' path. No logic change. No test surface affected. Usage-text-only edit. * resolve display name when options load after value custom-select showed the raw form-control value (often a GUID) when writeValue() ran before async options loaded. options.changes now re-resolves displayValue after options arrive. Fallback shows placeholder instead of the raw value to avoid a GUID flash during load. * suppress duplicate label on app-select in form-field custom-form-field rendered a floating label from the child app-select's placeholder while the select also rendered its own muted placeholder in the trigger — two overlapping texts on all 22 app-select usages. Suppress the form-field label entirely when the child is an app-select. Fix CSS specificity so the select placeholder renders in the muted color (move text color from static class to conditional ngClass to avoid same-specificity override). * add clearing, required marker, and color fixes to selects Add "None" option to create/edit organization quota dropdowns so users can clear a selection back to the placeholder state. Selecting a null-valued option now clears selectedValues rather than storing null, returning the trigger to its placeholder appearance. Show a red asterisk on required selects when empty — compensates for the form-field label suppression from the prior commit. Hide the success checkmark when no value is present. Use component CSS classes for placeholder/value text color instead of inline styles to match the form-field floating label and eliminate a dark→grey flash on initial render. * tighten form-field spacing and fix unlimited-input Remove reserved subscript padding (1.34375em) from form-field wrapper — errors/hints now take space only when present. Reduce infix padding from 0.75em to 0.25em and inter-field gap from mb-4 to mb-2 for denser forms. Adjust floating label position and underline to match. Fix unlimited-input: toggle was inverted (never toggled the unlimited flag on user click). Replace float/margin layout with flex. Drop required validation when unlimited is checked. * fix quota form labels, validation, and layout Make the floating-label system actually render on quota create/edit forms (org and space) by fixing the ContentChild selector, detecting required state correctly, and adding AppInputDirective to consumer imports. Also make the form visually match the console481 reference and remove several UX annoyances. Label rendering: - custom-form-field ContentChild(AppInputDirective, forwardRef) so that projected <input appInput> elements are discovered (string 'input' was treating it as a template ref that no consumer declares). - Read required state from input.required property (not hasAttribute) and re-read in ngAfterViewInit after projected bindings apply. - quota-definition-form, space-quota-definition-form, unlimited-input now import AppInputDirective so the directive is actually attached. Label UX: - floatLabel defaults to 'always' so labels stay above the input and never overlap with the input area. - Drop scale(0.75) on the floating transform (caused blurry text from transform-scaled 12px glyphs) and use text-xs directly. - Move required * to the beginning of the label for scannability. - Remove transition-all so color state changes (blue/green/red) are instant, no lingering previous color. Validation: - isValid / isInvalid consider only dirty, not touched, so tabbing or clicking through empty required fields does not trigger errors. Layout: - Widen .stepper-form max-width 450 -> 600 so long floating labels fit without wrapping into the input area. - Add w-full + placeholder:!text-transparent to inputs (inline Tailwind) so fields fill available space and @tailwindcss/forms can't override the transparent placeholder. Checkbox tab order: - Add tabindex=-1 to the hidden native <input type=checkbox> in the custom checkbox so keyboard tab skips it (the visible wrapper div already handles keyboard via keydown.enter/space). unlimited-input state handling: - Initialize unlimited=false (was !: boolean undefined) to avoid NG0100 ExpressionChangedAfterItHasBeenCheckedError. - Separate onCheckboxChange(event) (reads event.checked from the MatCheckboxChange event) from onChange() (applies state based on current unlimited). setInitialValues() now calls onChange without double-toggling. * require dirty form on quota edit steps The Update button on the edit org quota and edit space quota screens was enabled as soon as the existing quota data passed validation — which meant opening the page and tabbing through fields without changing anything left the button active. Change the step validate() to also require formGroup.dirty so the button only enables once the user has actually modified something. Create steps are unaffected: entering required values makes the form both valid and dirty at the same time. * convert custom-form-field label to tailwind, drop dead scss Move the floating-label color/size/position state into the template via a labelClasses getter that returns a Tailwind class string based on the component's current state (focused/valid/invalid/neutral). The SCSS no longer owns any label styling. Delete rules that never fired because of Angular ViewEncapsulation: all .form-field-infix input/textarea/select selectors (including the ones under :focus-visible, reduced-motion, dark-theme, float-label-never, and print @media blocks). Consumer templates already supply w-full, placeholder:!text-transparent, etc. directly on their own <input> elements. Move :host { display: block } out of custom-form-field.component.scss and unlimited-input.component.scss into the components' host metadata (host: { class: 'block' }). The unlimited-input SCSS file had nothing else in it and is now deleted, along with its styleUrls entry. Extend tailwind.config.js with an 'input' color namespace exposing --input-bg / --input-text / --input-border / --input-placeholder / --input-focus-border / --input-disabled-bg, so the label color states resolve via named utilities (text-primary, text-success, text-danger, text-input-focus-border, text-input-placeholder) rather than arbitrary var() values. Net result: -161 lines of dead/duplicated SCSS, one source of truth for label color in the TS getter. * wire floating labels for endpoint create/edit and connect auth forms Apply the same floating-label + input-stretch + placeholder-hide fix (already in place on the quota forms) to all endpoint screens that use app-form-field, so the custom-form-field ContentChild discovery works and the labels actually render above the inputs. Components that now import AppInputDirective so the directive attaches to their projected <input appInput> elements: - create-endpoint-cf-step-1 (endpoint create: name, url, client id/secret) - edit-endpoint-step (endpoint edit: name, url, client id/secret) - credentials-auth-form (connect dialog: username, password) - token-endpoint (connect dialog: token) Template inputs now carry class="w-full placeholder:!text-transparent" so they fill the available width and the browser's native placeholder does not bleed through the floating label (@tailwindcss/forms otherwise overrides the placeholder color on type=number inputs). SSO and none auth forms contain no inputs and are unchanged. * mirror quota form validity into a signal for cd propagation The step button state on the create/edit quota screens (org and space) only updated after an unrelated DOM event because the [valid]="step1.validate()" binding in AddQuotaComponent / AddSpaceQuotaComponent / EditQuotaComponent / EditSpaceQuotaComponent is in an OnPush host whose view-tree parent is not on the render chain from the form component. Content inside an <app-step> is wrapped in an <ng-template> and rendered via ngTemplateOutlet in SteppersComponent, so markForCheck from the form walks up through the steppers, not the page component that owns the [valid] binding. ApplicationRef.tick() from inside statusChanges subscription throws NG0101 (recursive tick). Fix: mirror formGroup.valid into a WritableSignal and read it from the valid() method that the binding calls. Reading a signal inside a template evaluation registers the consuming component as a dependent, so Angular automatically marks it dirty when the signal changes — works across ngTemplateOutlet and OnPush boundaries without any manual CD scheduling. Verified via Playwright: toggling Unlimited on required fields now immediately flips the Create button between enabled/disabled in both directions; also no recursive-tick errors in the console. * wire floating labels and button state for org/space create/edit Apply the same floating-label + signal-based validity propagation fix that the quota forms got to the four Cloud Foundry org/space CRUD steps: create-organization, edit-organization, create-space, edit-space. Template changes: - Input elements get class="w-full placeholder:!text-transparent" so they stretch to fill the form and the browser's native placeholder doesn't bleed through the floating label. - Edit screens' "name taken" error condition no longer calls validate() (which now returns form-level validity) — it reads the nameTaken / spaceNameTaken error from the orgName / spaceName form control directly. TS changes: - Each step imports AppInputDirective so the directive actually attaches to the projected <input appInput> elements and the custom-form-field can discover them via ContentChild. - Each step mirrors its form's validity into a WritableSignal so the [valid]="step1.validate()" binding on the parent page component (OnPush, off the ngTemplateOutlet render chain) auto-refreshes when the user edits the form. - Edit screens additionally require formGroup.dirty before the signal goes true, so the Update button only enables after an actual modification. Refactor in AddEditSpaceStepBase: the subclass-supplied name-uniqueness check is renamed from `validate` to `isNameUnique` so the subclass can define its own zero-argument `validate()` method for form-level validity without clashing with the base's validator signature. The base's spaceNameTakenValidator now calls isNameUnique. * convert service and cf-org-space step forms to floating-label pattern Make the service-instance creation flow consistent with the other form screens (quota, endpoint, org/space): - specify-details-step, specify-user-provided-details, schema-form: replace <app-label>X</app-label> + <input> pairs with <input placeholder="X" class="w-full placeholder:!text-transparent"> so the label floats above the input and the field stretches to fill the form. The Tags and User Provided Service Instance selects now also use the form-field's floating label (placeholder on app-select instead of a sibling app-label). - create-application-step1 (shared between Create Application and Create Service Instance flows): replace the hand-rolled <div class="mb-4"><label>X *</label><app-select class="border-b ..."> pattern with <app-form-field><app-select placeholder="X"> so the required asterisk, color states, and underline all come from the shared custom-form-field instead of ad-hoc classes. The "no orgs" and "no spaces" messages become <app-error> children. - All app-select elements in these screens now default to [autoSelectSingleOption]="false" and get an explicit <app-option [value]="null">None</app-option> so a single CF/org/space doesn't silently auto-select. - AppErrorComponent is now exported from @stratosui/core so consumers in the cloud-foundry package can import it. * cascade-clear and suppress required error on None in cf-org-space step The shared CF/Org/Space step (create-application-step1, also used by the create-service-instance flow) had two UX issues after the recent form-field conversion: 1. Clearing CF cleared Org but left Space stale. The existing service cascade uses withLatestFrom(org.list$) which can see pre-cleared data and skip the "clear space" branch. Handle the cascade explicitly in the template (onCfChange / onOrgChange) so clearing CF clears Org and Space, and clearing Org clears Space. 2. Selecting "None" on a required select immediately painted a red "This field is required" error, which reads as "you did something wrong" instead of "you're back to the starting state." When None is selected, reset the affected control(s) to pristine + untouched and force updateValueAndValidity() so the custom-form-field's statusChanges subscription fires and re-evaluates isInvalid (which is gated on `dirty`). The Next button stays disabled via the form's invalid state, but no error decoration is shown until the user actually picks something invalid. * remove focus outline double-line and widen floating-label gap Two visual polish fixes affecting all the screens that use app-form-field (quota, endpoint, org/space, service instance, schema form). focus:outline-none: - Browser default focus outlines were painting a blue ring around text-type inputs on focus, showing through as a "double line" against the form-field underline underneath. Number inputs already had outline-style: none from their own browser defaults, so only text inputs showed the extra border. Add focus:outline-none to every patched input so the form-field's own focus-state underline is the only focus indicator. floating-label translate: - The floating label was sitting 13.2px above its base, which left the label's bottom ~3px BELOW the input's top — they visibly overlapped. Change the labelClasses getter's translate from -translate-y-[1.1em] to -translate-y-[1.8em], giving a clean ~5.6px gap between the label and the input. * bump dev version to v5.0.0-dev.6 Version bump for today's deploy to adepttech (v5.0.0-dev.6+build.20260413.212fbae271). Also picks up a stray emoji unicode-escape → literal-emoji change in package.json scripts.bootstrap, harmless cosmetic diff that the version bump script re-formatted. * fix db URI parser to handle query parameters The fallback parsing of cf services was neglecting the fact that the database URI might contain query parameters. Update the regex to capture the optional ?key=value portion, parse it into a map, and add it to DatabaseConfig as a new QueryParams field. Tests cover URIs both with and without query params to confirm the regex handles both cases. Cherry-picked from upstream PR #5196 (author: Jan-Robin Aumann). Co-authored-by: Jan-Robin Aumann <jaumann@anynines.com> * add optional background refresh for cnsi tokens Introduces opt-in goroutines that proactively refresh CNSI (endpoint) tokens in the background using stored credentials, so endpoints that are not frequently used don't become unavailable once their refresh token has expired. The feature is disabled by default and must be explicitly enabled. When enabled, background workers use the stored credentials to re-authenticate before the tokens expire. - New fields on the CNSI/token structs to track backgroundable state - cnsi.go gains the scheduler + test coverage (cnsi_test.go) - pgsql_tokens.go gains store/load helpers for credentials + tests - main.go starts the refresh workers at startup when enabled - Desktop helm/kubernetes plugin token handlers updated to match the new signatures Cherry-picked from upstream PR #5203 (author: Jan-Robin Aumann). Co-authored-by: Jan-Robin Aumann <jaumann@anynines.com> * fix list header multi-filter / right-side overlap The list header's left container (.list-component__header__left) had `flex: 1; min-width: 0`, which told flexbox to allocate it the leftover space after the right container, AND to allow shrinking below its content width. When the multi-filters (cf/org/space selects) couldn't fit in the allocated width, they would visually overflow into the .list-component__header__right area, layering the right-side filter labels (e.g. "Service Type") on top of the Space select. Concretely on the services-wall view at ~1205px header width: - __left was allocated 254px (flex: 1, with __right's content ~905px) - __left's multi-filter content needed 385px (2 selects * 120px min-width + labels + gaps) - The 131px overflow put the Space select visually underneath the Service Type label from __right, rendering as e.g. "Se-doSpace" overlapping text Fix: - `.list-component__header__left`: `flex: 1; min-width: 0;` → `flex: 0 1 auto;`. Left now starts at content size (flex-basis: auto), can shrink under pressure (flex-shrink: 1), but does not grow beyond its content (flex-grow: 0). Min-width is no longer forced to 0, so flexbox respects content min-size. When the total content doesn't fit on one row, the parent .list-component__header- card (which already has flex-wrap: wrap) naturally drops __right to a second row. - `.list-component__header__left--multi-filters`: `flex-wrap: nowrap` → `flex-wrap: wrap`. When __left itself is still tight (very narrow viewports above the 767px mobile breakpoint), the internal filters can stack onto multiple rows rather than forcing __left to grow. Verified in Playwright at 600 / 800 / 900 / 1024 / 1280 / 1400 / 1800 viewport widths: labels and selects never overlap, layout gracefully drops rows as space shrinks. Below 768px the existing media query switches to a column layout, unchanged. * add copy UAA token button to page header Ports upstream PR #5169 (author: Jan-Robin Aumann) to modern Stratos patterns on feature/Angular-21. Backend (clean port): - api/structs.go — JSON tags on TokenRecord so it can be serialised - session.go — new AuthTokenEnvelope struct and retrieveToken handler. The handler reads user_id from the signed session cookie, verifies the session, then returns the UAA TokenRecord for that user only. No client-supplied user identifier anywhere — users can only ever retrieve their own token. - main.go — GET /api/v1/auth/token route Frontend (rewritten for Angular 21 patterns): - store/types/auth.types.ts — TokenData + AuthTokenEnvelope interfaces - store/public-api.ts — exported via `export type` - page-header.component.ts — uses inject(HttpClient) + inject(SnackBarService), firstValueFrom(), and navigator.clipboard.writeText(). No ngx-clipboard dependency. - page-header.component.html — new vpn_key button + dropdown in the Tailwind toggle pattern (isTokenMenuOpen), @if control flow, no mat-* Security hardening over the upstream PR: the original kept each token in a hidden <div>{{ token$ | async }}</div> buffer for the clipboard-copy library to read, which left the raw tokens live in the DOM where browser extensions could scrape them. This port skips the DOM buffer entirely — the token is fetched on-demand when the user clicks Copy and written straight to the clipboard API. The /auth/token envelope is also re-fetched every time the menu opens (via shareReplay(1) in a per-open observable) so users can't receive a stale or expired token from a process-lifetime cache. Co-authored-by: Jan-Robin Aumann <jaumann@anynines.com> * support GitHub Enterprise and private repo deploy via PAT Ports upstream PR #5195 (author: Jan-Robin Aumann) to modern Stratos patterns on feature/Angular-21. Before this port, Stratos' "deploy from GitHub" wizard only worked against public repositories on github.com. Customers on GitHub Enterprise or with private github.com repos had no path through the UI. This port adds two optional inputs to step 2 of the deploy wizard: - GitHub Enterprise URL — base URL of a GHE instance (validated) - GitHub Access Token — optional PAT for private repos Backend (clean port of the cfapppush plugin): - types.go — GitSCMSourceInfo gains AccessToken, CloneDetails gains AccessToken. Typo fix: upstream used `AcccessToken` (3 c's); fixed here to `AccessToken` (JSON tag was already correct). - deploy.go — threads AccessToken from the websocket message into CloneDetails and into GetVCS(withAccessToken(...)). - vcs.go — functional-options pattern on GetVCS. Access-token URL rewriting moved into vcsCmd.Create itself (upstream did it at the call site) so it can't be bypassed. GetVCS() also returns a fresh struct per call to avoid concurrent callers racing on a mutated package-level prototype. Frontend — modern Angular patterns: - deploy-application-step2.component.html — two new inputs in the existing form-field pattern (not mat-form-field). The access-token input uses type="password" + autocomplete="off" so browsers won't offer to save it. Error surfaces via Tailwind class, not mat-error. - deploy-application-step2.component.ts — new applyGithubEnterpriseAndToken() helper hooked into the existing valueChanges pipeline via tap(). URL validation uses `new URL(...)` in try/catch. No-op when the active SCM is not GitHub (GitLab path stays untouched). - github-project-exists.directive.ts — renamed helper to getTypeAndEndpointWithAuth(), now expects 3 comma-separated parts (type,endpoint,token). - scm-base.ts, github-scm.ts, scm.service.ts — plumbing to carry HttpOptions with an Authorization header through every GitHub API call (repos, branches, commits, search). - @stratosui/git public_api now re-exports BaseSCM and GitHubSCM so consumers can call setAccessToken/setPublicApi without deep imports. - deploy-application-deployer.ts, deploy-application.types.ts — thread accessToken through the GitSCMSourceInfo envelope so it reaches the jetstream backend. - github-commits-list-config-deploy.service.ts — passes access token through when instantiating the SCM for the commits list. Security notes: the PAT is a user secret of similar sensitivity to the UAA token. It's sent over TLS, never persisted server-side, and used only in-memory during the clone. The embedded-in-URL form (required by git's CLI) means it briefly appears in the git process arguments — visible to anyone with shell access inside the jetstream container. Upstream limitation, documented here for future readers. Co-authored-by: Jan-Robin Aumann <jaumann@anynines.com> * support async service binding and show last-binding state Ports upstream PR #5053 (author: Jan-Robin Aumann) to modern Stratos patterns on feature/Angular-21. Closes the "async provisioning" UX gap tracked in cloudfoundry/stratos#4498 and cloudfoundry-community/stratos#26. Background: many Cloud Foundry services (postgres-as-a-service, Blacksmith-broker-backed services) provision instances asynchronously — the broker returns 202 immediately but the instance isn't actually ready for minutes. The "Add Service Instance" wizard previously pushed the user straight into a "bind to app" step, which failed for async services because the instance wasn't ready yet. Users saw a confusing error with no hint that the create might just be in flight. This port makes two changes to the wizard flow and surfaces binding state in the service list/card. csi-mode.service.ts — when the wizard launches from the services wall (top-down flow, likely async), override showBindApp: false so the inline binding step is skipped. Users create the instance, see it progress on the services wall, then bind later once it's ready. New component ServiceInstanceLastServiceBindingComponent (standalone): - Reads the latest entry from IServiceBinding[] and displays its last_operation state via app-boolean-indicator (in-progress spinner vs yes/no icon) along with type + created_at. - Rendered both inline in the services-wall card tile and as a new "Last Service Binding" column in the cf-service-instances list. New component TableCellLastServiceBindingComponent — thin wrapper that renders "-" for user-provided service instances (no binding last_op) and delegates to ServiceInstanceLastServiceBindingComponent otherwise. cf-api-svc.types.ts — IServiceBinding now declares optional last_operation: ILastOperation. The field is present on async-broker responses today but wasn't declared in the TS type, which would have broken the new component under strict type-checking. Upstream changes skipped: - cf-shared.module.ts declarations — obsolete post-standalone migration. Each new component declares `standalone: true` with its own imports list instead. - add-service-instance.component.ts fixes — already present on our branch in cleaner form: we inject(ChangeDetectorRef) and call cdr.detectChanges() in ngOnInit, and apps$ uses shareReplay with refCount so the pipe is subscribed via async without the upstream's manual .subscribe() hack. - Karma/jasmine spec files — our branch uses vitest. Existing tests cover the surrounding code; the new components are small enough that coverage comes from the e2e wizard flow. - tsconfig.json / package.json reformat — purely cosmetic in upstream. - Debug console.log(this.serviceInstance) that upstream left in ngOnInit — removed. - Boxed Boolean type with // tslint:disable-next-line:ban-types — replaced with primitive boolean. Co-authored-by: Jan-Robin Aumann <jaumann@anynines.com> * add Go unit tests for retrieveToken and vcs URL rewriting Covers the two pieces of novel backend logic added in the recent port batch (PR 5169 + PR 5195). auth_test.go: - TestRetrieveToken — success case. Mirrors TestVerifySession's mock plumbing (setupHTTPTest, sqlmock, InitStratosAuthService) to stub a signed-session user_id/exp and verify the handler returns an AuthTokenEnvelope containing the decrypted TokenRecord. - TestRetrieveTokenNoSessionDate — error path. Omits "exp" from the session so GetSessionInt64Value fails, and verifies the handler writes an error envelope (status:"error", data:null) rather than propagating the error to echo. vcs.go: - Extracted the access-token URL rewrite logic from Create() into vcsCmd.repoWithToken(repo). Create() now delegates to it. Pure refactor — no behavioural change — done so the rewrite can be unit-tested without shelling out to a real git binary. vcs_test.go (new file, six tests): - TestRepoWithToken_NoTokenReturnsURLUnchanged — no-op when token empty - TestRepoWithToken_EmbedsTokenAsBasicAuth — PAT injected as x-access-token basic-auth userinfo - TestRepoWithToken_InvalidURLReturnsError — malformed URL surfaces a wrapped parse error - TestRepoWithToken_SpecialCharactersInTokenEscaped — tokens containing @, :, / must be percent-encoded so they don't prematurely terminate the userinfo section. This is the class of bug that would silently point git at the wrong host. - TestGetVCS_ReturnsFreshCopyNotPrototype — confirms GetVCS() returns a copy of the package-level vcsGit prototype rather than the prototype itself, so concurrent callers can't race on a mutated accessToken field. Directly exercises the concurrency fix that deviated from upstream PR #5195's GetVCS implementation. - TestGetVCS_WithAccessTokenOption — confirms the functional-options pattern actually applies the token to the returned struct and preserves the prototype's other fields. * FWT-917 instrument CfOrgSpaceDataService for diagnostic capture Adds a dev-build-only event log to CfOrgSpaceDataService so the CF→Org→Space pre-fill inconsistency tracked in FWT-917 can be diagnosed with a Playwright harness that reads events via window.__cfOsDebug.snapshot(). cf-org-space-debug.ts (new) — small CfOrgSpaceDebug class with a bounded ring buffer (500 events/instance), performance.now() timestamps, and a [CFOS #N] prefixed console.log for manual devtools use. createCfOrgSpaceDebug() factory registers each instance with a global aggregator on window.__cfOsDebug that returns a time-sorted merge across every instance — necessary because CfOrgSpaceDataService is component-scoped (each wizard gets its own instance), and H1 ("no cross-route handoff") is the leading hypothesis. Gated on !environment.production at every entry point so prod builds are no-ops and globalThis is never touched. cf-org-space-service.service.ts — log calls at the 10 event points needed to discriminate the six hypotheses: - service:construct (H1 — per-instance lifecycle) - cf:list-emit (upstream CF endpoint emissions) - cf:select-change (BehaviorSubject value changes) - org:list-emit (filtered org list emissions) - org:select-change - space:list-emit - space:select-change - allOrgs:entities-emit (H2/H5 — pagination race timing) - initialValues:resolved (H4 — stale filter inputs) - autoSelector:setup (H1 — confirms enableAutoSelectors was called) - autoSelector:cf-pre-filter (H3 — happy-path emission swallowed by filter) - autoSelector:cf-cascade-fire - autoSelector:org-pre-filter - autoSelector:org-cascade-fire The cf-pre-filter and org-pre-filter taps sit BEFORE their respective filter() operators so the diagnostic log captures emissions that the filter drops — that's the signature pattern for hypothesis H3 (filter(cf !== initialCf) swallows the first real emission when the persisted initial CF matches reality). allOrgs.entities$ is wrapped with a non-invasive tap via object spread — subscribers to this.allOrgs.entities$ see the tap, this.allOrgs.pagination$ bypasses it, and the original getPaginationObservables result is not mutated. Zero production impact verified: the debug class short-circuits log/snapshot to return early when environment.production is true, and the factory returns an instance without ever touching globalThis in prod. Existing cf-org-space-service.service.spec.ts (14 tests) passes unchanged — the instrumentation is purely additive. * FWT-917 extend instrumentation to cover quota and breadcrumb paths The original instrumentation in 2a670a921e covered CfOrgSpaceDataService, which powers the picker dropdowns in app/service wizards. But the user reports the same symptom on quota wizards (org quota create/edit, space quota), and a grep confirms quota wizards do NOT use CfOrgSpaceDataService at all — they pull cfGuid from the route via ActiveRouteCfOrgSpace and display CF/Org/Space context via CfOrgSpaceLabelService (the breadcrumb service). Three additional instrumentation surfaces are needed …
1 parent 56a06b7 commit 97d3f33

1,059 files changed

Lines changed: 7379 additions & 7785 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

ANGULAR-21-UPGRADE-STATUS.md

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
# Angular 21 Upgrade — Work-in-Progress Status
2+
3+
**Branch:** `try/angular-21-upgrade` (worktree)
4+
**Base:** `feature/Angular-21` at commit `2c15a7653a` (Remove 125 empty placeholder SCSS files)
5+
**Last updated:** 2026-04-09
6+
7+
## TL;DR
8+
9+
Angular 21 is **fundamentally working**. Build succeeds, E2E passes (72/0), the
10+
application runs correctly in a dev server against k3d. The blocker is the
11+
**unit test infrastructure** — 234 vitest specs fail with `NG0203` because the
12+
combination of Angular 21 + vitest 4 + zoneless change detection + NgRx Store
13+
factory injection no longer works the way it did under Angular 20 + vitest 3.
14+
15+
This branch should be **kept as backup**. The actual Angular 21 upgrade is
16+
deferred until the test infrastructure can be migrated (estimated 1–2 focused
17+
days, or wait for `@analogjs/vite-plugin-angular` to ship full Angular 21 +
18+
vitest 4 support).
19+
20+
---
21+
22+
## ✅ What Works
23+
24+
| Check | Result |
25+
|---|---|
26+
| `ng build stratos --configuration=development` | clean, 14.7s |
27+
| `make check e2e` (Playwright core) | **72 passed / 41 skipped / 0 failed** |
28+
| Dev server (vite 7 + ng serve) | runs on port 5540 |
29+
| Manual smoke test (login → endpoints → K8s + Helm) | works end-to-end |
30+
| Auto-migrations applied by `ng update` | 7 files migrated to control-flow syntax |
31+
32+
### Versions installed
33+
| Package | Version |
34+
|---|---|
35+
| `@angular/core` | 21.2.8 |
36+
| `@angular/cli` | 21.2.7 |
37+
| `@angular/cdk` | 21.2.6 |
38+
| `@angular/build` | 21.2.7 |
39+
| `@angular-devkit/build-angular` | 21.2.7 |
40+
| `@angular-builders/custom-webpack` | 21.0.3 |
41+
| `@angular-eslint/*` | 21.0.0 |
42+
| `@schematics/angular` | 21.2.7 |
43+
| `@ngrx/store` (and effects, router-store, store-devtools) | 21.1.0 |
44+
| `typescript` | 5.9.3 |
45+
| `vite` | 7.3.2 (security CVEs all addressed) |
46+
| `vitest` | 4.1.4 |
47+
| `@analogjs/vitest-angular` | 2.4.4 |
48+
| `@swimlane/ngx-graph` | 11.0.0 (peer wants CDK ≤20, works at runtime) |
49+
| `ngrx-store-localstorage` | 20.1.0 (peer says `>=20`, works with NgRx 21) |
50+
51+
---
52+
53+
## ❌ The Blocker — `NG0203` in unit tests
54+
55+
### Symptom
56+
57+
`bun run test` produces:
58+
```
59+
Test Files 202 failed | 363 passed | 3 skipped (568)
60+
Tests 234 failed | 866 passed | 9 skipped (1109)
61+
```
62+
63+
Every component-rendering test that touches NgRx Store fails with:
64+
```
65+
Error: NG0203: The `StateObservable` token injection failed.
66+
`inject()` function must be called from an injection context such as a
67+
constructor, a factory function, a field initializer, or a function used
68+
with `runInInjectionContext`.
69+
Find more at https://v21.angular.dev/errors/NG0203
70+
71+
❯ injectInjectorOnly node_modules/@angular/core/fesm2022/_effect-chunk2.mjs:667
72+
❯ ɵɵinject node_modules/@angular/core/fesm2022/_effect-chunk2.mjs:684
73+
❯ Object.factory ng:/Store/ɵfac.js:5:49
74+
❯ R3Injector.hydrate packages/core/src/di/r3_injector.ts:533
75+
```
76+
77+
The **NgRx Store factory itself** (generated via `ɵɵngDeclareFactory`) is
78+
running outside an active injection context. When it tries to `inject(StateObservable)`
79+
to construct the `Store`, `getCurrentInjector()` returns `undefined` and
80+
Angular 21 throws.
81+
82+
### What used to work (Angular 20 + vitest 3)
83+
84+
Pre-upgrade test files use this pattern:
85+
86+
```ts
87+
TestBed.configureTestingModule({
88+
imports: [SomeStandaloneComponent],
89+
providers: [
90+
importProvidersFrom(
91+
CfAutoscalerTestingModule, // declares EntityCatalogFeatureModule
92+
...generateBaseTestStoreModules(), // includes StoreModule.forRoot(appReducers)
93+
CoreModule,
94+
NoopAnimationsModule,
95+
),
96+
provideRouter([]),
97+
provideZonelessChangeDetection(),
98+
// ...
99+
],
100+
}).compileComponents();
101+
```
102+
103+
Under Angular 20, `importProvidersFrom` flattened the modules' providers in
104+
the right order: NgRx Store providers (including `StateObservable`) were in
105+
place before any factory functions ran. Under Angular 21 the ordering breaks
106+
— the `Store` factory is now invoked before the environment injector has
107+
finished bootstrapping the providers it depends on.
108+
109+
### What was tried (and didn't fix it)
110+
111+
1. **Bump `@analogjs/vite-plugin-angular` and `@analogjs/vitest-angular` to 2.4.4**
112+
— these are the latest, peer deps say they support `vite ^6 || ^7`,
113+
`vitest ^1 || ^2 || ^3 || ^4`, no Angular version pinning. Installing them
114+
did not change the failure mode.
115+
116+
2. **Migrate `vitest.workspace.ts``vitest.config.ts` with `projects:`**
117+
— required because `defineWorkspace` was removed in vitest 4. The
118+
migration succeeded mechanically (tests are now discovered) but didn't
119+
change the failure mode.
120+
121+
3. **Replace `StoreModule.forRoot` with `provideStore` in `createBasicStoreModule`**
122+
— converted the helper from `ModuleWithProviders<StoreRootModule>` to
123+
`EnvironmentProviders` via `makeEnvironmentProviders([provideStore(...)])`.
124+
This **shifted** the error from `Store` to `EntityCatalogFeatureModule`,
125+
confirming the issue is with `importProvidersFrom` of NgModules whose
126+
constructors call `inject(Store)`.
127+
128+
4. **Convert `EntityCatalogFeatureModule` to use `provideEnvironmentInitializer`**
129+
— moved the `inject(Store)` / `inject(ReducerManager)` calls out of the
130+
module constructor and into an environment initializer that runs after
131+
all providers are bootstrapped. **This did not fix the test failure**
132+
the same `NG0203` reappears, this time inside the Store factory itself
133+
when the test tries to inject Store directly.
134+
135+
5. **Inline `provideStore(appReducers, ...)` in a single test file's
136+
providers array** — removing the `importProvidersFrom` wrapping
137+
completely. Same `NG0203` failure. This rules out `importProvidersFrom`
138+
as the cause and points at a deeper interaction between the vitest 4
139+
test environment and Angular 21's injector-context tracking.
140+
141+
### Root cause hypothesis
142+
143+
The most likely root cause is that **`@analogjs/vite-plugin-angular`'s test
144+
environment doesn't yet establish an injection context that survives
145+
across NgRx 21's factory functions when running under vitest 4 + zoneless
146+
mode**. NgRx 21's Store is now defined via `ɵɵngDeclareFactory` with
147+
explicit `deps`, and Angular 21 enforces that factories run inside
148+
`runInInjectorProfilerContext`. Under vitest 3 + Angular 20 the older
149+
factory shape happened to bypass this requirement.
150+
151+
Notably, the **same code paths work fine in the production build and in
152+
Playwright E2E** — the issue is specific to how vitest 4 spins up the
153+
test environment.
154+
155+
---
156+
157+
## Files changed in this branch
158+
159+
### Required code changes for Angular 21 itself
160+
| File | Change |
161+
|---|---|
162+
| `package.json` | Bumped Angular packages to 21, NgRx to 21.1.0, TypeScript override to ^5.9.3, vite override to ^7.3.2 |
163+
| `tools/builders/prebuild-application/package.json` | Workspace builder bumped to 21.x |
164+
| `vitest.config.ts` | Migrated from `defineWorkspace` to `projects:` (vitest 4 syntax) |
165+
| `vitest.workspace.ts` | **Deleted** (removed in vitest 4) |
166+
| `src/.../tile/tile.component.ts` | `@HostBinding('class.app-tile-1-3') private isOneThirdFixed``public` |
167+
| `src/.../tile-group/tile-group.component.ts` | 7 `@HostBinding(...) private` fields → `public` |
168+
| `src/.../tile-grid/tile-grid.component.ts` | `@Input() private fit``public` |
169+
170+
### Files updated by `ng update` automation
171+
| File | Migration |
172+
|---|---|
173+
| `kubernetes-namespace-preview.component.html` | Block control flow syntax |
174+
| `kubernetes-namespace-preview.component.ts` | Block control flow syntax |
175+
| `stratos-title.component.ts` | Block control flow syntax |
176+
| `custom-tabs.component.ts` | Block control flow syntax |
177+
| `deploy-application-fs.component.ts` | Block control flow syntax |
178+
| `cf-role-checkbox.component.ts` | Block control flow syntax |
179+
| `add-api-key-dialog.component.ts` | Block control flow syntax |
180+
181+
---
182+
183+
## Pre-flight that confirmed the codebase was ready
184+
185+
Before attempting the upgrade, verified the following on `feature/Angular-21`:
186+
187+
| Risk area | Status |
188+
|---|---|
189+
| `provideZoneChangeDetection` already in `app.module.ts` ||
190+
| No `NgModuleFactory` usage ||
191+
| No `moduleId` on components ||
192+
| No `UpgradeAdapter` (legacy upgrade) ||
193+
| No custom interpolation delimiters ||
194+
| No `lastSuccessfulNavigation` consumers ||
195+
| 62 `[ngClass]` + 6 `[ngStyle]` bindings | left as-is — auto-migration available, low risk |
196+
| TypeScript 5.8 → 5.9 | bumped via override |
197+
| Gate check passed on base branch | 1104 tests, all green |
198+
| E2E passed on base branch | 72 passed, 41 skipped, 0 failed |
199+
200+
---
201+
202+
## What surfaced as Angular 21 breaking changes that hit our code
203+
204+
| # | Breaking change | Files affected | Fix applied |
205+
|---|---|---|---|
206+
| 1 | Host binding type checking enabled by default — can't bind to `private` fields | `tile/`, `tile-group/`, `tile-grid/` | Made fields `public` |
207+
| 2 | `@Input() private` similarly rejected | `tile-grid/` | Made `public` |
208+
| 3 | TypeScript 5.9 minimum | `package.json` overrides | Bumped override |
209+
| 4 | `@angular/build@^20` not compatible | resolved version | Forced override to `^21.2.7` |
210+
| 5 | NgRx Store factory injection context (test env) | All component specs touching Store | **NOT FIXED** — see blocker section |
211+
212+
---
213+
214+
## How to resume this work
215+
216+
### Option A — wait for tooling
217+
218+
Watch these for vitest 4 + Angular 21 + zoneless support:
219+
- `@analogjs/vite-plugin-angular` releases
220+
- `@analogjs/vitest-angular` releases
221+
- vitest 4 documentation on Angular testing
222+
223+
When the toolchain is ready, this branch can be rebased onto the latest
224+
`feature/Angular-21` and the test failures should resolve without further
225+
code changes.
226+
227+
### Option B — invest the focused day
228+
229+
The fix likely requires one of:
230+
231+
1. **Custom test bootstrap** that wraps every spec in `runInInjectionContext`
232+
— would need a vitest plugin or a global beforeEach that initializes
233+
TestBed's environment injector before any factory runs.
234+
235+
2. **Migrate every spec away from `importProvidersFrom`** of modules whose
236+
constructors `inject()`, replacing them with explicit `provide*` calls
237+
in the providers array. This is mechanical but touches ~200 spec files
238+
and the test framework helpers (`generateBaseTestStoreModules`,
239+
`BaseTestModules`, etc.).
240+
241+
3. **Stop using zoneless in tests** — switch back to `provideZoneChangeDetection`
242+
for tests only. May or may not avoid the issue depending on how vitest 4
243+
spins up the platform.
244+
245+
### Option C — abandon and revisit later
246+
247+
Drop this branch and stay on Angular 20 (`feature/Angular-21` branch — yes,
248+
the name is misleading; it has been the long-running work branch for the
249+
Angular 21 prep work but currently sits at Angular 20.3.18). All the
250+
Angular 21 readiness work landed on `feature/Angular-21` already and the
251+
codebase will continue to work fine on Angular 20 until the tooling
252+
catches up.
253+
254+
---
255+
256+
## Resuming step-by-step
257+
258+
To resume work in this worktree:
259+
260+
```bash
261+
cd /Users/norm/Projects/CloudFoundry/cf-stratos-ng21
262+
git status # should be clean on try/angular-21-upgrade
263+
git pull origin try/angular-21-upgrade
264+
265+
# Reinstall dependencies (worktree might have stale node_modules)
266+
rm -rf node_modules bun.lock
267+
bun install
268+
269+
# Verify build still works
270+
npx ng build stratos --configuration=development
271+
272+
# Verify E2E still passes
273+
make check e2e
274+
275+
# To work on the test infrastructure issue, start with one spec:
276+
npx vitest run \
277+
src/frontend/packages/cf-autoscaler/src/shared/list-types/app-autoscaler-metric-chart/app-autoscaler-metric-chart-card/combo-chart/combo-series-vertical.component.spec.ts \
278+
--project=cf-autoscaler
279+
```
280+
281+
### Files to look at first when resuming
282+
283+
| File | Why |
284+
|---|---|
285+
| `src/frontend/packages/store/testing/src/store-test-helper.ts` | `createBasicStoreModule` — currently uses `StoreModule.forRoot`, may need migration to `provideStore` |
286+
| `src/frontend/packages/store/src/entity-catalog.module.ts` | `EntityCatalogFeatureModule` constructor injects `Store` — candidate for `provideEnvironmentInitializer` migration |
287+
| `src/frontend/packages/core/test-framework/core-test.helper.ts` | `generateBaseTestStoreModules` — entry point for most test setups |
288+
| `src/frontend/packages/*/src/test-setup.ts` | Per-package vitest test setup files — initialize TestBed platform |
289+
| `vitest.config.ts` | Already migrated to vitest 4 `projects:` syntax |
290+
291+
---
292+
293+
## Pointers to the original Angular 20 baseline
294+
295+
If the upgrade is abandoned, the prep work is fully landed on
296+
`feature/Angular-21` (still at Angular 20.3.18) and is in excellent shape.
297+
Ship-ready commits since 2026-04-09:
298+
299+
| Commit | Description |
300+
|---|---|
301+
| `2c15a7653a` | Remove 125 empty placeholder SCSS files across packages |
302+
| `51040a07e7` | FWT-872: Remove 34 empty K8s placeholder SCSS files |
303+
| `3c94ee4e66` | FWT-872: Convert helm/monocular SCSS to Tailwind, remove dead code (-966 lines) |
304+
| `a0b3bfa8ad` | Fix Helm chart browsing broken by standalone migration |
305+
| `698aa8daaf` | Pin patched hono and vite via overrides (security CVEs) |
306+
| `217f9e24ac` | FWT-876: Reduce ESLint unused-vars warnings 532 to 5 |
307+
| `23cdbd2c97` | Fix TS build errors from inject() migration |
308+
| `0085038667` | ESLint: ignore _-prefixed caught errors and spec vars |

0 commit comments

Comments
 (0)