EPIC: Angular 22 + Nx 23 full-leverage modernization (signal forms, resource APIs, affected CI, remote cache) #722

Open
opened 2026-07-19 15:09:32 +00:00 by spikerj · 28 comments
Owner

Adopt everything Angular 22 and Nx 23 offer across spikersoft-angular. The runtime upgrade is done (22.0.6 / 23.1.0); this epic is adoption. Inventory confirmed we're already zoneless + 100% OnPush + control-flow-migrated + inject()-everywhere — the real gaps and the effort series:

Track Angular

  • E0 Housekeeping: stale files, strip vestigial SSR targets, pin paramsInheritanceStrategy: 'always', ngClass/ngStyle schematics, drop unused zone.js dep, re-include 5 lint-excluded projects
  • E3 Signal queries/IO: @ViewChild×73 / @Input×14 / @Output×14 / constructor-DI×6 via official v22 schematics
  • E4 resource()/httpResource(): 17 hand-rolled fetch-into-signal services (pilot: status.service; largest: art-studio.service). Rule: reads→resources, mutations imperative + reload()
  • E5 Signal Forms (flagship): pilot contact.component proves [formField] + Material 22 error tracking + shared Transloco error helper; wave 1 = 9 Reactive surfaces small→large (profile last); wave 2 = ALL 63 ngModel screens (decision: migrate everything). MatStepper: drop [stepControl] for completed/editable computeds. ngx-mask files (3) stay Reactive until supported
  • E6 @defer/injectAsync: three.js, Monaco, maps, emulators clusters (~178 heavy components; only 4 files use @defer today)
  • E7 Router+HTTP: withComponentInputBinding; functional interceptors (order-preserving); withXhr stays (#230 decision)
  • E8 optional: Angular Aria spike; @Service for new services

Track Nx (fully parallel)

  • E1a nx affected for CI test/lint (PRs; keep full run-many on master pushes; keep single full prod build per #549)
  • E1b self-hosted remote cache: Nx 23 built-in HTTP cache protocol (NX_SELF_HOSTED_REMOTE_CACHE_SERVER) + MinIO-backed cache server on the swarm, token in OpenBao. NOT nx-remotecache-* (dead API on 23), NOT @nx/s3-cache (activation key), never Nx Cloud
  • E1c e2e becomes an Nx project via @nx/playwright/plugin inference (wraps in place, cache:false — API-connected suite)
  • E2a local workspace generator for the 7-file lib pattern (test target + tags always emitted) + cached per-project typecheck targets (retires the unusable root tsc, fixes #664 pain)
  • E2b test-target backfill on the 30 libs without one

Conflict rules: keep @angular/build:unit-test (never @nx/vite:test); @nx/playwright is the ONLY inference plugin; never mix formControlName and [formField] in one form; per-directory schematic runs only.

~35–38 PRs total. Plan of record: ~/.claude/plans/bright-dazzling-ritchie.md (approved 2026-07-19).

Adopt everything Angular 22 and Nx 23 offer across spikersoft-angular. The runtime upgrade is done (22.0.6 / 23.1.0); this epic is adoption. Inventory confirmed we're already zoneless + 100% OnPush + control-flow-migrated + inject()-everywhere — the real gaps and the effort series: **Track Angular** - E0 Housekeeping: stale files, strip vestigial SSR targets, pin `paramsInheritanceStrategy: 'always'`, ngClass/ngStyle schematics, drop unused zone.js dep, re-include 5 lint-excluded projects - E3 Signal queries/IO: `@ViewChild`×73 / `@Input`×14 / `@Output`×14 / constructor-DI×6 via official v22 schematics - E4 resource()/httpResource(): 17 hand-rolled fetch-into-signal services (pilot: status.service; largest: art-studio.service). Rule: reads→resources, mutations imperative + reload() - E5 Signal Forms (flagship): pilot contact.component proves `[formField]` + Material 22 error tracking + shared Transloco error helper; wave 1 = 9 Reactive surfaces small→large (profile last); wave 2 = ALL 63 ngModel screens (decision: migrate everything). MatStepper: drop [stepControl] for completed/editable computeds. ngx-mask files (3) stay Reactive until supported - E6 @defer/injectAsync: three.js, Monaco, maps, emulators clusters (~178 heavy components; only 4 files use @defer today) - E7 Router+HTTP: withComponentInputBinding; functional interceptors (order-preserving); withXhr stays (#230 decision) - E8 optional: Angular Aria spike; @Service for new services **Track Nx (fully parallel)** - E1a `nx affected` for CI test/lint (PRs; keep full run-many on master pushes; keep single full prod build per #549) - E1b self-hosted remote cache: Nx 23 built-in HTTP cache protocol (`NX_SELF_HOSTED_REMOTE_CACHE_SERVER`) + MinIO-backed cache server on the swarm, token in OpenBao. NOT nx-remotecache-* (dead API on 23), NOT @nx/s3-cache (activation key), never Nx Cloud - E1c e2e becomes an Nx project via `@nx/playwright/plugin` inference (wraps in place, cache:false — API-connected suite) - E2a local workspace generator for the 7-file lib pattern (test target + tags always emitted) + cached per-project `typecheck` targets (retires the unusable root tsc, fixes #664 pain) - E2b test-target backfill on the 30 libs without one **Conflict rules:** keep `@angular/build:unit-test` (never @nx/vite:test); @nx/playwright is the ONLY inference plugin; never mix formControlName and [formField] in one form; per-directory schematic runs only. ~35–38 PRs total. Plan of record: `~/.claude/plans/bright-dazzling-ritchie.md` (approved 2026-07-19).
Author
Owner

E4 status — completion PR open

E4 (resource() / httpResource()) is finished in code. The finishing wave is open as spikersoft-angular#458 (refactor(a22): E4 PR4 — finish resource()/httpResource adoption).

Prior E4 PRs already landed: PR1 pilot (#437), PR2 pure-reads (#440), PR3a blog (#441), PR3b art-studio sticky config (#442). PR4 covers the rest of the inventory plus the utilization gaps (reactive params, ticking-signal polling, parse, error opacity, defaultValue/hasValue documentation).

E4 will be done when #458 merges. The epic itself stays open — Angular track still has E5 (signal forms), E6 (@defer/injectAsync), E7 (router+HTTP), E8 (Aria spike); Nx track has E1b/E1c/E2a/E2b as planned.

## E4 status — completion PR open E4 (`resource()` / `httpResource()`) is finished in code. The finishing wave is open as [spikersoft-angular#458](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/458) (`refactor(a22): E4 PR4 — finish resource()/httpResource adoption`). Prior E4 PRs already landed: PR1 pilot (#437), PR2 pure-reads (#440), PR3a blog (#441), PR3b art-studio sticky config (#442). PR4 covers the rest of the inventory plus the utilization gaps (reactive params, ticking-signal polling, `parse`, error opacity, `defaultValue`/`hasValue` documentation). **E4 will be done when #458 merges.** The epic itself stays open — Angular track still has E5 (signal forms), E6 (`@defer`/injectAsync), E7 (router+HTTP), E8 (Aria spike); Nx track has E1b/E1c/E2a/E2b as planned.
Author
Owner

Progress update (2026-07-20)

E4 completespikersoft-angular#458 merged.

Epic status inventory (what's already on master):

  • E0, E1a/b/c, E2a/b, E3, E4, E5 pilot (contact), E6 PR1 (three.js @defer), E7
  • 🔄 E5 wave 1 started: spikersoft-angular#459edit-health-info dialog → Signal Forms (applyEach for dynamic meds/contacts)
  • Remaining: E5 wave 1 rest (user-create → org-registration → registration-stepper → child-account → employment → profile), E5 wave 2 (ngModel screens), E6 PR2–4 (Monaco/maps/emulators), E8 optional

Next up after #459: keycloak-admin user-create.

## Progress update (2026-07-20) **E4 complete** — [spikersoft-angular#458](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/458) merged. **Epic status inventory (what's already on master):** - ✅ E0, E1a/b/c, E2a/b, E3, E4, E5 pilot (contact), E6 PR1 (three.js `@defer`), E7 - 🔄 **E5 wave 1** started: [spikersoft-angular#459](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/459) — `edit-health-info` dialog → Signal Forms (`applyEach` for dynamic meds/contacts) - ⏳ Remaining: E5 wave 1 rest (user-create → org-registration → registration-stepper → child-account → employment → profile), E5 wave 2 (ngModel screens), E6 PR2–4 (Monaco/maps/emulators), E8 optional Next up after #459: keycloak-admin `user-create`.
Author
Owner

E5 wave 1 — user-create PR open

#459 (edit-health-info) is merged.

Next Reactive → Signal Forms surface is open as spikersoft-angular#460 (feat(a22): E5 wave1 — user-create on Signal Forms).

Still remaining in E5 wave 1: organization-registration → registration-stepper (COPPA) → child-account-dialog → employment/application-dialog → profile.

## E5 wave 1 — user-create PR open [#459](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/459) (edit-health-info) is merged. Next Reactive → Signal Forms surface is open as [spikersoft-angular#460](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/460) (`feat(a22): E5 wave1 — user-create on Signal Forms`). **Still remaining in E5 wave 1:** organization-registration → registration-stepper (COPPA) → child-account-dialog → employment/application-dialog → profile.
Author
Owner

E5 wave 1 — organization-registration PR open

spikersoft-angular#461 — organization-registration on Signal Forms.

Also still open: #460 (user-create).

Still remaining in E5 wave 1: registration-stepper (COPPA) → child-account-dialog → employment/application-dialog → profile.

## E5 wave 1 — organization-registration PR open [spikersoft-angular#461](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/461) — organization-registration on Signal Forms. Also still open: [#460](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/460) (user-create). **Still remaining in E5 wave 1:** registration-stepper (COPPA) → child-account-dialog → employment/application-dialog → profile.
Author
Owner

E5 wave 1 — registration-stepper PR open (COPPA)

spikersoft-angular#462 — registration-stepper Signal Forms (validateAsync availability + MatStepper [completed]).

Also open: #460 user-create, #461 org-registration.

Merge gate for #462: e2e registration walk + manual QA. SMS consent subtree left Reactive for a follow-up.

Still remaining in E5 wave 1 after this: child-account-dialog → employment/application-dialog → profile (+ SMS consent cleanup).

## E5 wave 1 — registration-stepper PR open (COPPA) [spikersoft-angular#462](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/462) — registration-stepper Signal Forms (`validateAsync` availability + MatStepper `[completed]`). Also open: [#460](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/460) user-create, [#461](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/461) org-registration. **Merge gate for #462:** e2e registration walk + manual QA. SMS consent subtree left Reactive for a follow-up. **Still remaining in E5 wave 1 after this:** child-account-dialog → employment/application-dialog → profile (+ SMS consent cleanup).
Author
Owner

E5 wave 1 — child-account-dialog PR open (COPPA)

Verified nothing else had claimed it (no open PRs / remote branches), then migrated the six-step create/edit child-account dialog: spikersoft-angular#474.

Notable firsts for the wave: applyWhen for conditional validation (passport fields only when international; credentials only in create mode) and the cross-field validate + valueOf password check. Username availability reuses the #462 validateAsync resource shape.

Merge gate: manual QA of create + edit paths (auth-gated dialog — the anonymous e2e walk doesn't reach it).

Still remaining in E5 wave 1: employment/application-dialog → profile (+ SMS-consent subtree cleanup from #462). E6 PR2–4 and E5 wave 2 unchanged.

## E5 wave 1 — child-account-dialog PR open (COPPA) Verified nothing else had claimed it (no open PRs / remote branches), then migrated the six-step create/edit child-account dialog: [spikersoft-angular#474](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/474). Notable firsts for the wave: `applyWhen` for conditional validation (passport fields only when international; credentials only in create mode) and the cross-field `validate` + `valueOf` password check. Username availability reuses the #462 `validateAsync` resource shape. **Merge gate:** manual QA of create + edit paths (auth-gated dialog — the anonymous e2e walk doesn't reach it). **Still remaining in E5 wave 1:** employment/application-dialog → profile (+ SMS-consent subtree cleanup from #462). E6 PR2–4 and E5 wave 2 unchanged.
Author
Owner

E5 wave 1 — employment application-dialog PR open

spikersoft-angular#476 — five FormGroups + four FormArrays migrated; the merge(valueChanges) progress tracker became a plain computed over the models, and Validators.requiredTrue maps to required() (Signal Forms treats false as empty). Small noted UX change: postal-code auto-fill now fires on blur instead of debounced keystrokes.

Gates run locally in order: full app build → full app test suite (2062 green) → lint.

(#474 child-account-dialog merged earlier today.)

E5 wave 1 remaining: profile (last, by design) + SMS-consent subtree cleanup from #462.

## E5 wave 1 — employment application-dialog PR open [spikersoft-angular#476](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/476) — five FormGroups + four FormArrays migrated; the `merge(valueChanges)` progress tracker became a plain `computed` over the models, and `Validators.requiredTrue` maps to `required()` (Signal Forms treats `false` as empty). Small noted UX change: postal-code auto-fill now fires on blur instead of debounced keystrokes. Gates run locally in order: full app build → full app test suite (2062 green) → lint. (#474 child-account-dialog merged earlier today.) **E5 wave 1 remaining: profile** (last, by design) + SMS-consent subtree cleanup from #462.
Author
Owner

E5 — SMS-consent carve-out closed (COPPA)

spikersoft-angular#478 — the #462 leftover. SmsConsentStateService's FormGroup-in-a-signal (with the _formValueChanged counter hack) became a model signal + form() FieldTree; host public API unchanged so the stepper was untouched apart from a stale comment. Spec now tests the real schema.

Merge gate: e2e-anonymous green on the PR + manual QA of the SMS step (registration = COPPA).

Gates run locally per step: full app build → full app suite (2059 green) → lint.

E5 wave 1 remaining: profile only. Recommend it as its own sitting — it's the largest Reactive surface in the app and was sequenced last deliberately.

## E5 — SMS-consent carve-out closed (COPPA) [spikersoft-angular#478](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/478) — the #462 leftover. `SmsConsentStateService`'s FormGroup-in-a-signal (with the `_formValueChanged` counter hack) became a model signal + `form()` FieldTree; host public API unchanged so the stepper was untouched apart from a stale comment. Spec now tests the real schema. **Merge gate:** `e2e-anonymous` green on the PR + manual QA of the SMS step (registration = COPPA). Gates run locally per step: full app build → full app suite (2059 green) → lint. **E5 wave 1 remaining: profile only.** Recommend it as its own sitting — it's the largest Reactive surface in the app and was sequenced last deliberately.
Author
Owner

E5 wave 1 COMPLETE — profile PR open

spikersoft-angular#479 migrates the last wave-1 surface: all six profile FormGroups (incl. the dynamic curated-palette group as a Record-shaped model), the three child components now take FieldTree inputs (first use of that pattern), the unsaved-changes guard rides aggregated root dirty(), and every valueChanges subscription became a template event.

When #479 merges, E5 wave 1 is done: 7/7 surfaces + pilot + SMS-consent carve-out — no Reactive Forms left in wave-1 scope.

Gates run locally per step: full app build → full app suite (2059 green) → lint. Suggested manual pass in the PR body (personal save, address verify/postal autofill, theme live preview, unsaved-changes dialog).

Angular track remaining: E5 wave 2 (~63 ngModel screens, ngx-mask trio excluded) and E6 PR2–4 (Monaco/maps/emulators @defer). E8 optional.

## E5 wave 1 COMPLETE — profile PR open [spikersoft-angular#479](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/479) migrates the last wave-1 surface: all six profile FormGroups (incl. the dynamic curated-palette group as a Record-shaped model), the three child components now take `FieldTree` inputs (first use of that pattern), the unsaved-changes guard rides aggregated root `dirty()`, and every valueChanges subscription became a template event. **When #479 merges, E5 wave 1 is done: 7/7 surfaces + pilot + SMS-consent carve-out — no Reactive Forms left in wave-1 scope.** Gates run locally per step: full app build → full app suite (2059 green) → lint. Suggested manual pass in the PR body (personal save, address verify/postal autofill, theme live preview, unsaved-changes dialog). **Angular track remaining:** E5 wave 2 (~63 ngModel screens, ngx-mask trio excluded) and E6 PR2–4 (Monaco/maps/emulators @defer). E8 optional.
Author
Owner

E6 PR2 (Monaco/dev-tools cluster) open — measured, not assumed

spikersoft-angular#481. Chunk fingerprinting showed the cluster's presumed target (Monaco) is already lazy via MonacoLoaderService — the real offender was quicktype-core riding the /tools/json route. One-file fix (loader-cached dynamic import): navigation payload 3370 → 2293 KB raw (−32%), measured by static-closure walk over the dist chunk graph. Blockly stays as-is (deliberate static share with the voxel robot panel); duckdb needs nothing.

Also open: #479 (profile — E5 wave 1 finale, awaiting manual QA).

E6 remaining: PR3 maps cluster (map-tracker chunk is 1.24 MB — promising), PR4 emulators. Then E5 wave 2.

## E6 PR2 (Monaco/dev-tools cluster) open — measured, not assumed [spikersoft-angular#481](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/481). Chunk fingerprinting showed the cluster's presumed target (Monaco) is **already lazy** via `MonacoLoaderService` — the real offender was `quicktype-core` riding the `/tools/json` route. One-file fix (loader-cached dynamic import): **navigation payload 3370 → 2293 KB raw (−32%)**, measured by static-closure walk over the dist chunk graph. Blockly stays as-is (deliberate static share with the voxel robot panel); duckdb needs nothing. Also open: [#479](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/479) (profile — E5 wave 1 finale, awaiting manual QA). **E6 remaining: PR3 maps cluster (map-tracker chunk is 1.24 MB — promising), PR4 emulators.** Then E5 wave 2.
Author
Owner

E6 PR3 (maps cluster) open

spikersoft-angular#482 — defer-wrapper for /tracker: navigation payload 1437 KB → 11 KB (maplibre-gl streams in behind an on-viewport @defer). Leaflet measured at ~150 KB per copy and left alone (country-map is profile-adjacent while #479 is in review). Gates: build → i18n:scan (one pre-existing finding, verified on clean master) → 2061 tests green → lint.

Next: E6 PR4, emulators cluster (v86 / alpine-linux / mac-plus) — same measure-first approach.

## E6 PR3 (maps cluster) open [spikersoft-angular#482](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/482) — defer-wrapper for `/tracker`: navigation payload **1437 KB → 11 KB** (maplibre-gl streams in behind an on-viewport `@defer`). Leaflet measured at ~150 KB per copy and left alone (country-map is profile-adjacent while #479 is in review). Gates: build → i18n:scan (one pre-existing finding, verified on clean master) → 2061 tests green → lint. Next: **E6 PR4, emulators cluster (v86 / alpine-linux / mac-plus)** — same measure-first approach.
Author
Owner

E6 PR4 (emulators) open — E6 track complete

spikersoft-angular#483. Measurement said: v86 already lazy by construction (~220 KB route closure, engine runtime-fetched); the real find was alpine-linux + mac-plus dead since the v86 catalogue, orphaning 158 MB of ROM/disk-image assets — 232 MB in every dist/image/deploy. Deleted (ticket #751).

E6 = 4/4 clusters done (three.js #449, dev-tools #481 −32% /tools/json, maps #482 1437→11 KB /tracker, emulators #483 −232 MB dist). Every cluster's action came from chunk-graph measurement, not the plan's assumptions.

Open PRs awaiting merge: #479 (profile, manual QA), #482 (tracker defer), #483 (emulators cleanup).

Angular track remaining: E5 wave 2 only (~63 ngModel screens, ngx-mask trio excluded) + optional E8.

## E6 PR4 (emulators) open — E6 track complete [spikersoft-angular#483](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/483). Measurement said: v86 already lazy by construction (~220 KB route closure, engine runtime-fetched); the real find was **alpine-linux + mac-plus dead since the v86 catalogue, orphaning 158 MB of ROM/disk-image assets — 232 MB in every dist/image/deploy**. Deleted (ticket #751). **E6 = 4/4 clusters done** (three.js #449, dev-tools #481 −32% /tools/json, maps #482 1437→11 KB /tracker, emulators #483 −232 MB dist). Every cluster's action came from chunk-graph measurement, not the plan's assumptions. Open PRs awaiting merge: #479 (profile, manual QA), #482 (tracker defer), #483 (emulators cleanup). **Angular track remaining: E5 wave 2 only** (~63 ngModel screens, ngx-mask trio excluded) + optional E8.
Author
Owner

E5 wave 2 started — batch 1 open

Wave-1 profile (#479) and both E6 PRs (#482/#483) merged — thanks. #751 closed after #483.

spikersoft-angular#484 — wave-2 batch 1: six admin screens (activity-coverage, child-account-review, contact-messages, platform-adoption, hiring-management, fact-review) off ngModel. Pattern: form(signal) + [formField] (signals stay the models, FormsModule and setter-handlers deleted); one NG8022 (min attr) moved into schema.

Wave-2 ledger: 6/63 templates migrated. Next batches: admin heavy trio (skill-management 32 bindings, location-management 12, geography-facts 8), then keycloak-admin lib (5 templates), dev-tools libs (~16), art-studio (7), marks-site (4), rest.

## E5 wave 2 started — batch 1 open Wave-1 profile (#479) and both E6 PRs (#482/#483) merged — thanks. #751 closed after #483. [spikersoft-angular#484](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/484) — wave-2 batch 1: six admin screens (activity-coverage, child-account-review, contact-messages, platform-adoption, hiring-management, fact-review) off ngModel. Pattern: `form(signal)` + `[formField]` (signals stay the models, FormsModule and setter-handlers deleted); one NG8022 (min attr) moved into schema. **Wave-2 ledger: 6/63 templates migrated.** Next batches: admin heavy trio (skill-management 32 bindings, location-management 12, geography-facts 8), then keycloak-admin lib (5 templates), dev-tools libs (~16), art-studio (7), marks-site (4), rest.
Author
Owner

E5 wave 2 batch 2 open — admin cluster complete

spikersoft-angular#486 — skill-management (16 mechanical signal bridges), location-management (11 loose props → three object models, side effects via native (change)), geography-facts-management (bonus find: a full Reactive fb.group + FormArray the wave-1 inventory missed — now a proper model + form() schema; FormBuilder deleted from the file). Five NG8022 attr→schema moves.

Wave-2 ledger: 9/63 templates; admin cluster done. Gates per screen: intermediate builds (caught every NG8022) → full suite (2059) → lint.

Next batch: keycloak-admin lib (5 templates).

## E5 wave 2 batch 2 open — admin cluster complete [spikersoft-angular#486](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/486) — skill-management (16 mechanical signal bridges), location-management (11 loose props → three object models, side effects via native `(change)`), geography-facts-management (bonus find: a full Reactive `fb.group` + FormArray the wave-1 inventory missed — now a proper model + `form()` schema; FormBuilder deleted from the file). Five NG8022 attr→schema moves. **Wave-2 ledger: 9/63 templates; admin cluster done.** Gates per screen: intermediate builds (caught every NG8022) → full suite (2059) → lint. Next batch: keycloak-admin lib (5 templates).
Author
Owner

E5 wave 2 batch 3 open — keycloak-admin done

spikersoft-angular#487 — all five keycloak-admin templates; ngModel count in the lib is zero. group-create-dialog was a hybrid (compat ReactiveFormField + FormBuilder + in-place array mutation via ngModel) — now a clean model + form() schema with the array-index FieldTree idiom.

⚠️ Inventory drift, second sighting: the lib holds five MORE Reactive Forms components no wave inventory listed (role-create-dialog, user-roles, user-federated-identity, user-list, user-password-reset) — like geography-facts in batch 2. Wave 2 will need an addendum batch for stray Reactive surfaces; I'll do a repo-wide FormBuilder|formControlName census when the ngModel ledger closes so the epic ends at true zero.

Wave-2 ledger: 14/63. Next: dev-tools libs (~16 templates).

## E5 wave 2 batch 3 open — keycloak-admin done [spikersoft-angular#487](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/487) — all five keycloak-admin templates; ngModel count in the lib is zero. group-create-dialog was a hybrid (compat `ReactiveFormField` + FormBuilder + in-place array mutation via ngModel) — now a clean model + `form()` schema with the array-index FieldTree idiom. **⚠️ Inventory drift, second sighting:** the lib holds five MORE Reactive Forms components no wave inventory listed (role-create-dialog, user-roles, user-federated-identity, user-list, user-password-reset) — like geography-facts in batch 2. Wave 2 will need an **addendum batch for stray Reactive surfaces**; I'll do a repo-wide `FormBuilder|formControlName` census when the ngModel ledger closes so the epic ends at true zero. **Wave-2 ledger: 14/63.** Next: dev-tools libs (~16 templates).
Author
Owner

E5 wave 2 batch 4 open — dev-tools (small nine) + Monaco [(value)]

spikersoft-angular#488. Infrastructure piece: MonacoEditorComponent now has a signal-native value model() beside its CVA — editor panes shouldn't need FormsModule; the ngModel path stays for unmigrated callsites. Nine templates migrated (blockly, diagram, duckdb, base64, both x86 panels, ipv4, quick-type main, save-name dialog); four new NG8022 variants for the playbook ([disabled] bindings → schema disabled(), name attr, maxlength).

Wave-2 ledger: 23/63. Remaining: dev-tools heavy seven (batch 5: obfuscation modal 48 bindings, qr-code 18, csv/json conversions 28, options modal 14, reg-ex 8, image-to-avif 8) → art-studio (7) → marks-site (4) → misc app screens → stray-Reactive addendum census.

## E5 wave 2 batch 4 open — dev-tools (small nine) + Monaco `[(value)]` [spikersoft-angular#488](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/488). Infrastructure piece: **MonacoEditorComponent now has a signal-native `value` model()** beside its CVA — editor panes shouldn't need FormsModule; the ngModel path stays for unmigrated callsites. Nine templates migrated (blockly, diagram, duckdb, base64, both x86 panels, ipv4, quick-type main, save-name dialog); four new NG8022 variants for the playbook (`[disabled]` bindings → schema `disabled()`, `name` attr, `maxlength`). **Wave-2 ledger: 23/63.** Remaining: dev-tools heavy seven (batch 5: obfuscation modal 48 bindings, qr-code 18, csv/json conversions 28, options modal 14, reg-ex 8, image-to-avif 8) → art-studio (7) → marks-site (4) → misc app screens → stray-Reactive addendum census.
Author
Owner

E5 wave 2 batch 5 open — dev-tools cluster COMPLETE (stacked)

spikersoft-angular#489 — the heavy seven (~124 bindings). Highlight: the two csv/json converters replaced fifteen per-field handlers with one tracking effect(() => validateAndConvert()). Both quick-type settings modals became model+FieldTree with numeric bounds in schema.

⛓️ Merge order: #488 first, then #489 (it's stacked on #488's Monaco [(value)] wrapper; base is the batch-4 branch). Reminder: #488's earlier CI red was the verified runner-OOM — rerun on the same commit went green.

Wave-2 ledger: 30/63. Remaining: art-studio (7) → marks-site (4) → misc app screens (~14: voxel block-editor ×2, chat, book-upload, epub annotation, git-playground, cpu-dashboard, map-tracker, ai-assistant, language-runner, clang videos pane, save-content-dialog, js-formatting-options, fundraiser ×3, sponsor ×3, blog dialog, games-clue-for-sql, trellis-3d) → stray-Reactive census addendum.

## E5 wave 2 batch 5 open — dev-tools cluster COMPLETE (stacked) [spikersoft-angular#489](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/489) — the heavy seven (~124 bindings). Highlight: the two csv/json converters replaced **fifteen** per-field handlers with one tracking `effect(() => validateAndConvert())`. Both quick-type settings modals became model+FieldTree with numeric bounds in schema. ⛓️ **Merge order: #488 first, then #489** (it's stacked on #488's Monaco `[(value)]` wrapper; base is the batch-4 branch). Reminder: #488's earlier CI red was the verified runner-OOM — rerun on the same commit went green. **Wave-2 ledger: 30/63.** Remaining: art-studio (7) → marks-site (4) → misc app screens (~14: voxel block-editor ×2, chat, book-upload, epub annotation, git-playground, cpu-dashboard, map-tracker, ai-assistant, language-runner, clang videos pane, save-content-dialog, js-formatting-options, fundraiser ×3, sponsor ×3, blog dialog, games-clue-for-sql, trellis-3d) → stray-Reactive census addendum.
Author
Owner

E5 wave-2 batch 6 up: art-studio — PR spikersoft-angular#490.

Six templates migrated; the seventh inventory entry (art-asset-viewer) was a grep false positive (isLoadingModel contains the ngModel substring), so the true wave-2 denominator is 62, and the ledger now stands at 36/62 with the admin, keycloak-admin, dev-tools, and art-studio clusters all at zero ngModel.

Notables from this batch:

  • prompt-composer / photo-touch-up / photo-stack: straight form(signal) conversions with the min/max/maxlength attrs moved into schemas (the recurring NG8022 class).
  • preset-manager (nullable open-editor model) and review-queue / gallery-moderation (per-asset keyed note map) keep native [value] + (input) — documented [formField] exceptions, same category as batch 5's CSV-list fields.
  • New gotcha for the remaining batches: [maxlength] property bindings only compile via FormsModule's validator directive; once FormsModule leaves the imports they go NG8002 → convert to [attr.maxlength].

All local gates green (full build, full app suite 2065 tests, lib test+lint, fixers). Next: marks-site (4 templates), then the misc app screens, then the repo-wide stray-Reactive census to close the wave at true zero.

**E5 wave-2 batch 6 up: art-studio — PR [spikersoft-angular#490](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/490).** Six templates migrated; the seventh inventory entry (art-asset-viewer) was a grep false positive (`isLoadingModel` contains the `ngModel` substring), so the true wave-2 denominator is **62**, and the ledger now stands at **36/62** with the admin, keycloak-admin, dev-tools, and art-studio clusters all at zero ngModel. Notables from this batch: - prompt-composer / photo-touch-up / photo-stack: straight `form(signal)` conversions with the min/max/maxlength attrs moved into schemas (the recurring NG8022 class). - preset-manager (nullable open-editor model) and review-queue / gallery-moderation (per-asset keyed note map) keep native `[value]` + `(input)` — documented `[formField]` exceptions, same category as batch 5's CSV-list fields. - New gotcha for the remaining batches: `[maxlength]` *property* bindings only compile via FormsModule's validator directive; once FormsModule leaves the imports they go NG8002 → convert to `[attr.maxlength]`. All local gates green (full build, full app suite 2065 tests, lib test+lint, fixers). Next: marks-site (4 templates), then the misc app screens, then the repo-wide stray-Reactive census to close the wave at true zero.
Author
Owner

E5 wave-2 batch 7 up: marks-site — PR spikersoft-angular#492. Ledger: 40/62.

marks-site is now FormsModule-free. The three entity-array editors (address / generator / propane-tank) are documented native-binding conversions rather than formField: they mutate DTO entries in place on the parent-owned customer object, and the parent's Save sends that whole object relying on those mutations — Signal Forms' immutable, component-local writes would fork that data flow and drop sibling-tab edits on save. A numberFromInput helper preserves ngModel's empty→null semantics on numeric inputs, and nullable numerics bind via value="{{ x }}" interpolation (renders null as empty). marks-customer keeps its Reactive form per the locked ngx-mask decision, but its standalone customerType radio ngModel is gone too.

All local gates green (build first-pass, 2065-test app suite, lint, fixers). Remaining: the misc app-screens batch(es) (~22 templates), then the repo-wide stray-Reactive census.

**E5 wave-2 batch 7 up: marks-site — PR [spikersoft-angular#492](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/492). Ledger: 40/62.** marks-site is now FormsModule-free. The three entity-array editors (address / generator / propane-tank) are documented native-binding conversions rather than formField: they mutate DTO entries in place on the parent-owned customer object, and the parent's Save sends that whole object relying on those mutations — Signal Forms' immutable, component-local writes would fork that data flow and drop sibling-tab edits on save. A `numberFromInput` helper preserves ngModel's empty→null semantics on numeric inputs, and nullable numerics bind via `value="{{ x }}"` interpolation (renders null as empty). marks-customer keeps its Reactive form per the locked ngx-mask decision, but its standalone customerType radio ngModel is gone too. All local gates green (build first-pass, 2065-test app suite, lint, fixers). Remaining: the misc app-screens batch(es) (~22 templates), then the repo-wide stray-Reactive census.
Author
Owner

E5 wave-2 batch 8 up: five feature libs — PR spikersoft-angular#493. Ledger: 49/62.

blog moderation-dialog, the fundraiser trio, clue-for-sql, the sponsor trio, and trellis-3d-generator are all FormsModule-free. Mostly straight form(signal) conversions; handler-driven controls (sanitizing inputs, slider thumbs, native selects with number ids) documented as native-binding exceptions. Two bonus finds: a latent double-write race on trellis's wireframe toggle (ngModel + (change) both flipping the same signal) now has a single writer, and three blog components carried dead FormsModule imports — stripped.

All local gates green (intermediate builds caught two conversion slips, five-lib test+lint 183 tests, full build, 2065-test app suite, lint, fixers).

Remaining: platform/shared/game libs (~6 templates: wasm-voxel ×2, clang videos pane, language-runner, js-formatting-options, save-content-dialog) → app screens (~7) → repo-wide stray-Reactive census.

**E5 wave-2 batch 8 up: five feature libs — PR [spikersoft-angular#493](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/493). Ledger: 49/62.** blog moderation-dialog, the fundraiser trio, clue-for-sql, the sponsor trio, and trellis-3d-generator are all FormsModule-free. Mostly straight `form(signal)` conversions; handler-driven controls (sanitizing inputs, slider thumbs, native selects with number ids) documented as native-binding exceptions. Two bonus finds: a latent double-write race on trellis's wireframe toggle (ngModel + (change) both flipping the same signal) now has a single writer, and three blog components carried dead FormsModule imports — stripped. All local gates green (intermediate builds caught two conversion slips, five-lib test+lint 183 tests, full build, 2065-test app suite, lint, fixers). Remaining: platform/shared/game libs (~6 templates: wasm-voxel ×2, clang videos pane, language-runner, js-formatting-options, save-content-dialog) → app screens (~7) → repo-wide stray-Reactive census.
Author
Owner

E5 wave-2 batch 9 up: game/platform/shared libs — PR spikersoft-angular#494. Ledger: 55/62 — libraries/ is now completely ngModel/FormsModule-free.

wasm-voxel's two block editors (in-place entity mutation with engine-refresh hooks → native bindings, matching their sliders which were already native), language-runner (both monacos onto the wrapper's [(value)] — the CVA path there is now retired), clang videos pane, js-formatting panel (store-setter native bindings; @for select options need [selected] — select-level [value] races option creation on first render), save-content-dialog.

All gates green: build first pass, five-lib test+lint (245 tests), full app suite (2065), lint, fixers.

Final stretch: the seven app screens (voxel wrapper hits are done — remaining are chat, book-upload, cpu-dashboard, epub annotation-dialog, git-playground, map-tracker, ai-assistant message-input), then the stray-Reactive census closes the wave.

**E5 wave-2 batch 9 up: game/platform/shared libs — PR [spikersoft-angular#494](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/494). Ledger: 55/62 — `libraries/` is now completely ngModel/FormsModule-free.** wasm-voxel's two block editors (in-place entity mutation with engine-refresh hooks → native bindings, matching their sliders which were already native), language-runner (both monacos onto the wrapper's `[(value)]` — the CVA path there is now retired), clang videos pane, js-formatting panel (store-setter native bindings; `@for` select options need `[selected]` — select-level `[value]` races option creation on first render), save-content-dialog. All gates green: build first pass, five-lib test+lint (245 tests), full app suite (2065), lint, fixers. Final stretch: the seven app screens (voxel wrapper hits are done — remaining are chat, book-upload, cpu-dashboard, epub annotation-dialog, git-playground, map-tracker, ai-assistant message-input), then the stray-Reactive census closes the wave.
Author
Owner

E5 wave-2 ngModel migration: COMPLETE (pending merges). Batches 10+11 up: PRs #495 (final seven app screens) and #496 (census).

Ledger correction from the census: the inventory was built from .html files, so six components with inline templates never made it in (program-library, robot-programming-panel, application-review, staff-issues' two embedded dialogs, delete-account-dialog, sql-theme-picker). True denominator was 68, and all 68 are now converted across batches 1–11. #496 also strips 17 verified-dead FormsModule imports (day-scheduler deliberately kept — bare <form> needs a submit guard before its NgForm can go).

Open PRs awaiting merge: #492 (marks-site), #493 (feature libs), #494 (platform/shared/game libs), #495 (app screens), #496 (census). All five are independent branches off master, each fully gated locally (build, 2065-test app suite, per-lib tests, lint, fixers). Once merged, the repo is at zero ngModel.

Remaining E5 work after this wave (separate follow-up series, not ngModel debt): the stray-Reactive migrations — 10 keycloak-admin components, dev-tools jwt, interests-management, calendar event-modal, landing-page, geography-explorer — plus the locked ngx-mask trio which stays Reactive until ngx-mask supports Signal Forms, and day-scheduler's guarded FormsModule.

**E5 wave-2 ngModel migration: COMPLETE (pending merges). Batches 10+11 up: PRs [#495](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/495) (final seven app screens) and [#496](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/496) (census).** Ledger correction from the census: the inventory was built from `.html` files, so six components with **inline templates** never made it in (program-library, robot-programming-panel, application-review, staff-issues' two embedded dialogs, delete-account-dialog, sql-theme-picker). True denominator was **68, and all 68 are now converted** across batches 1–11. #496 also strips 17 verified-dead FormsModule imports (day-scheduler deliberately kept — bare `<form>` needs a submit guard before its NgForm can go). **Open PRs awaiting merge: #492 (marks-site), #493 (feature libs), #494 (platform/shared/game libs), #495 (app screens), #496 (census).** All five are independent branches off master, each fully gated locally (build, 2065-test app suite, per-lib tests, lint, fixers). Once merged, the repo is at zero ngModel. **Remaining E5 work after this wave (separate follow-up series, not ngModel debt):** the stray-Reactive migrations — 10 keycloak-admin components, dev-tools jwt, interests-management, calendar event-modal, landing-page, geography-explorer — plus the locked ngx-mask trio which stays Reactive until ngx-mask supports Signal Forms, and day-scheduler's guarded FormsModule.
Author
Owner

E5 Reactive follow-up series started. R1 up: keycloak-admin small six — PR spikersoft-angular#497.

role-create-dialog, user-password-reset, user-federated-identity, user-list (debounced search → toObservable(model); its distinctUntilChanged() was a reference-compare no-op and is dropped), user-roles (client select now writes the resource-driving signal directly, selectedClientId middleman deleted; the two search fields turned out to be bound-but-never-consumed — preserved as inert), and user-information (dynamic attributes Record via [formField]="userForm.attributes[key]", root disabled() schema replacing the parent-triggered enable/disable, so updateFormState() is gone). Plus the landing-page dead stepper FormGroups from the census.

New playbook entry: [readonly] is another NG8022-forbidden binding on formField nodes (and redundant under a disabled() schema).

All gates green: intermediate builds, keycloak-admin test+lint (147), full app suite (2065), lint, fixers. Specs rewritten to the field API where they drove the old controls.

Next: R2 — keycloak-admin heavy trio (client-create-dialog, client-edit-dialog, realm-settings — 34 controls), then R3 — app screens (interests-management, calendar event-modal, geography-explorer, dev-tools jwt). After R3, FormBuilder/formControlName remains only in the locked ngx-mask screens.

**E5 Reactive follow-up series started. R1 up: keycloak-admin small six — PR [spikersoft-angular#497](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/497).** role-create-dialog, user-password-reset, user-federated-identity, user-list (debounced search → `toObservable(model)`; its `distinctUntilChanged()` was a reference-compare no-op and is dropped), user-roles (client select now writes the resource-driving signal directly, `selectedClientId` middleman deleted; the two search fields turned out to be bound-but-never-consumed — preserved as inert), and user-information (dynamic attributes Record via `[formField]="userForm.attributes[key]"`, root `disabled()` schema replacing the parent-triggered enable/disable, so `updateFormState()` is gone). Plus the landing-page dead stepper FormGroups from the census. New playbook entry: **`[readonly]` is another NG8022-forbidden binding on formField nodes** (and redundant under a `disabled()` schema). All gates green: intermediate builds, keycloak-admin test+lint (147), full app suite (2065), lint, fixers. Specs rewritten to the field API where they drove the old controls. Next: **R2 — keycloak-admin heavy trio** (client-create-dialog, client-edit-dialog, realm-settings — 34 controls), then **R3 — app screens** (interests-management, calendar event-modal, geography-explorer, dev-tools jwt). After R3, `FormBuilder`/`formControlName` remains only in the locked ngx-mask screens.
Author
Owner

R2 up: keycloak-admin heavy trio — PR spikersoft-angular#498. With R1+R2 merged, keycloak-admin is 100% Signal Forms.

Client dialogs (17-field shared model, required+pattern schema), realm-settings (34-field model, disabled(path.realm) replacing the disabled-control config). Notable latent-bug fix: Reactive .value excludes disabled controls, so realm-settings has been sending realm: undefined on every save (masked by Keycloak keying updates off the URL) — the model form now sends the real value.

Gates green: build, keycloak-admin 147 tests + lint, full 2065-test suite, lint, fixers.

Next: R3 — the last four Reactive app screens (interests-management, calendar event-modal, geography-explorer, dev-tools jwt).

**R2 up: keycloak-admin heavy trio — PR [spikersoft-angular#498](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/498). With R1+R2 merged, keycloak-admin is 100% Signal Forms.** Client dialogs (17-field shared model, required+pattern schema), realm-settings (34-field model, `disabled(path.realm)` replacing the disabled-control config). Notable latent-bug fix: Reactive `.value` excludes disabled controls, so realm-settings has been sending `realm: undefined` on every save (masked by Keycloak keying updates off the URL) — the model form now sends the real value. Gates green: build, keycloak-admin 147 tests + lint, full 2065-test suite, lint, fixers. Next: R3 — the last four Reactive app screens (interests-management, calendar event-modal, geography-explorer, dev-tools jwt).
Author
Owner

R3 up — the Reactive→Signal Forms series is COMPLETE: PR spikersoft-angular#499 (interests-management, calendar event-modal, geography-explorer, dev-tools jwt).

E5 end state once #498 and #499 merge:

  • ngModel: zero (wave 2, 68/68, all merged)
  • FormsModule: only day-scheduler's guarded import (bare <form> needs a submit guard first)
  • FormBuilder/formControlName: only marks-customer (locked until ngx-mask supports Signal Forms) and a comment in the contact pilot

Highlights from R3: event-modal is the workspace's first datepicker/timepicker on [formField] (Signal Forms drives their CVAs) and drops its valueChanges→markForCheck OnPush hack; jwt's parallel jwtToken/secret signals became computed() views of the model after a naive mirror-effect port looped (decode pretty-prints into the model → mirror clobbers the token — the spec caught it, which is exactly what the gates are for). One process slip, self-reported on the PR: the R3 commit briefly went up with a red event-modal spec because my gate chain short-circuited; fixed in the follow-up commit and the branch head is fully green (2065 tests).

**R3 up — the Reactive→Signal Forms series is COMPLETE: PR [spikersoft-angular#499](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/499)** (interests-management, calendar event-modal, geography-explorer, dev-tools jwt). **E5 end state once #498 and #499 merge:** - ngModel: zero (wave 2, 68/68, all merged) - FormsModule: only day-scheduler's guarded import (bare `<form>` needs a submit guard first) - FormBuilder/formControlName: only marks-customer (locked until ngx-mask supports Signal Forms) and a comment in the contact pilot Highlights from R3: event-modal is the workspace's first datepicker/timepicker on `[formField]` (Signal Forms drives their CVAs) and drops its `valueChanges→markForCheck` OnPush hack; jwt's parallel `jwtToken`/`secret` signals became `computed()` views of the model after a naive mirror-effect port looped (decode pretty-prints into the model → mirror clobbers the token — the spec caught it, which is exactly what the gates are for). One process slip, self-reported on the PR: the R3 commit briefly went up with a red event-modal spec because my gate chain short-circuited; fixed in the follow-up commit and the branch head is fully green (2065 tests).
Author
Owner

E5 R4 up — the census's third blind spot, and the true finish line: PR spikersoft-angular#500.

The FormBuilder/formControlName regex never covered standalone new FormControl() + [formControl]. Post-merge sweep found four users (reader book-search-panel's debounced search, marks-customer-list's filters, dev-tools json-storage-selector-modal's autocomplete, marks-service-contract's tab-index value boxes) — all converted — plus three more dead FormsModule imports including day-scheduler, whose pointless bare <form> is unwrapped, retiring the last documented holdout.

E5 exit state once #500 merges: zero ngModel · zero FormsModule · zero Reactive forms except marks-customer (ngx-mask lock) and explanatory comments. Census lesson recorded for posterity: sweep all THREE idioms — ngModel (word-boundary, .ts AND .html for inline templates), FormBuilder|formControlName, and new FormControl|[formControl].

All gates green (build, touched-lib tests, 2065-test app suite ×2 with exit codes checked, lint, fixers).

**E5 R4 up — the census's third blind spot, and the true finish line: PR [spikersoft-angular#500](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/500).** The FormBuilder/formControlName regex never covered standalone `new FormControl()` + `[formControl]`. Post-merge sweep found four users (reader book-search-panel's debounced search, marks-customer-list's filters, dev-tools json-storage-selector-modal's autocomplete, marks-service-contract's tab-index value boxes) — all converted — plus three more dead FormsModule imports including day-scheduler, whose pointless bare `<form>` is unwrapped, retiring the last documented holdout. **E5 exit state once #500 merges:** zero ngModel · zero FormsModule · zero Reactive forms except marks-customer (ngx-mask lock) and explanatory comments. Census lesson recorded for posterity: sweep all THREE idioms — `ngModel` (word-boundary, .ts AND .html for inline templates), `FormBuilder|formControlName`, and `new FormControl|[formControl]`. All gates green (build, touched-lib tests, 2065-test app suite ×2 with exit codes checked, lint, fixers).
Author
Owner

E8 spike delivered — the epic's final effort: PR spikersoft-angular#501

Findings, @Service: stable @publicApi in core 22.0.6, with exactly the shapes we need — @Service() auto-provided (replaces @Injectable({providedIn:'root'})), {autoProvided:false} for providers-list services, {factory} for useFactory. Core also ships provideIdleServiceWith for tuning injectAsync idle-prefetch if E6's loaders ever need it. The workspace generator doesn't scaffold services, so the "generator default" from the plan materializes as a mirrored rule doc: new services use @Service(), no sweep of the ~213 existing (locked).

Findings, Angular Aria: @angular/aria 22.0.4 is already a dependency and already piloted — the x86 playground's mobile debugger tabs run on @angular/aria/tabs. The package ships eight headless, signal-based primitives (accordion, combobox, grid, listbox, menu, tabs, toolbar, tree), each with a testing harness. Spike inventory of hand-rolled role=/keydown widgets worth opportunistic migration (recorded in the rule): clang playground shell (9 role attrs), reg-ex (8), info-vault (6), language-runner, sql-runner, js-debug-vars, art-pipe shell — and the cleanest single example, event-modal's color picker (role="radio" + three keydown handlers per swatch → listbox/radio primitive). Recommendation: adopt for new widgets + opportunistic rework, no sweep; Material components stay Material. WebMCP ban restated.

Epic status: ALL EFFORTS DELIVERED

  • Angular track: E0 · E3 · E4 · E5 (pilot + wave 1 9/9 + wave 2 68/68 + Reactive follow-up R1–R4) · E6 (4/4 clusters, measured) · E7 · E8 (#501)
  • Nx track: E1a affected-CI · E1b MinIO remote cache (trust window on --skip-nx-cache runs to ~2026-08-03) · E1c playwright plugin · E2a generator+typecheck · E2b test backfill (0 libs without a test target today)

Awaiting merge: #500 (forms-census finisher) and #501 (this spike). After those, the only deliberately-open threads are: marks-customer stays Reactive until ngx-mask ships Signal Forms support, and dropping --skip-nx-cache when the cache trust window closes. Suggest keeping the epic open until both PRs land, then closing it.

## E8 spike delivered — the epic's final effort: PR [spikersoft-angular#501](https://git.spikersoft.com/spikerj/spikersoft-angular/pulls/501) **Findings, `@Service`:** stable `@publicApi` in core 22.0.6, with exactly the shapes we need — `@Service()` auto-provided (replaces `@Injectable({providedIn:'root'})`), `{autoProvided:false}` for providers-list services, `{factory}` for useFactory. Core also ships `provideIdleServiceWith` for tuning `injectAsync` idle-prefetch if E6's loaders ever need it. The workspace generator doesn't scaffold services, so the "generator default" from the plan materializes as a mirrored rule doc: **new services use `@Service()`, no sweep of the ~213 existing** (locked). **Findings, Angular Aria:** `@angular/aria` 22.0.4 is *already* a dependency and *already* piloted — the x86 playground's mobile debugger tabs run on `@angular/aria/tabs`. The package ships eight headless, signal-based primitives (accordion, combobox, grid, listbox, menu, tabs, toolbar, tree), each with a testing harness. Spike inventory of hand-rolled `role=`/keydown widgets worth opportunistic migration (recorded in the rule): clang playground shell (9 role attrs), reg-ex (8), info-vault (6), language-runner, sql-runner, js-debug-vars, art-pipe shell — and the cleanest single example, event-modal's color picker (`role="radio"` + three keydown handlers per swatch → listbox/radio primitive). Recommendation: adopt for new widgets + opportunistic rework, no sweep; Material components stay Material. WebMCP ban restated. ## Epic status: ALL EFFORTS DELIVERED - **Angular track:** E0 ✅ · E3 ✅ · E4 ✅ · E5 ✅ (pilot + wave 1 9/9 + wave 2 68/68 + Reactive follow-up R1–R4) · E6 ✅ (4/4 clusters, measured) · E7 ✅ · E8 ✅ (#501) - **Nx track:** E1a affected-CI ✅ · E1b MinIO remote cache ✅ (trust window on `--skip-nx-cache` runs to ~2026-08-03) · E1c playwright plugin ✅ · E2a generator+typecheck ✅ · E2b test backfill ✅ (0 libs without a test target today) **Awaiting merge: #500 (forms-census finisher) and #501 (this spike).** After those, the only deliberately-open threads are: marks-customer stays Reactive until ngx-mask ships Signal Forms support, and dropping `--skip-nx-cache` when the cache trust window closes. Suggest keeping the epic open until both PRs land, then closing it.
Author
Owner

Epic status roll-up — audited the E-series against origin/master. This epic is close to done and its remaining items are small, which the 27-comment thread makes hard to see at a glance.

Effort State
E0/E3 — Nx self-hosted remote cache live in CI
E1a — affected lint/test on PRs shipped
E2a — per-project typecheck shipped, and it routed around #664
E5 — Signal Forms migration effectively complete — 1 file left
E7 — strip SSR done
E8 — @Service / @angular/aria conventions rules landed, adoption opportunistic by design

E0/E3: main.yml:387 sets NX_SELF_HOSTED_REMOTE_CACHE_SERVER=https://nx-cache.spikersoft.com with the token pulled from OpenBao at :399 (secret/ci/angular/nx-cache). The locked MinIO-backed decision is live.

E1a/E2a: confirmed while auditing #824 (now closed). PRs run nx affected -t lint/-t test against origin/master with full run-many on pushes; the per-project typecheck target inferred by tools/workspace-plugin replaced the unusable root tsc --noEmit. Worth recording explicitly: that means #664 was routed around rather than resolved — the root typecheck is still unusable, but nothing depends on it now.

E5 is one file from done. git grep -l "ngModel" across all app and library templates returns exactly one: libraries/features/art-studio/src/lib/art-asset-viewer/art-asset-viewer.component.html. Notably that is not one of the three documented ngx-mask blockers — only marks-customer.component.html still carries ngx-mask, so the blocker set has shrunk too. Worth checking whether the art-asset-viewer one is a genuine blocker or simply missed by the wave.

E7: no server.ts, @angular/ssr or prerender references remain in projects/spikersoft/project.json.

E8 is behaving as the locked decision intended — 3 files use @Service() and 5 import @angular/aria. Both were scoped as "new code adopts, no sweep", so low counts are the expected outcome, not a gap.

What's genuinely left, and it's outside the E-series:

  • #742 — the workspace-plugin library generator emits ^undefined peers (reads dependencies, but framework deps moved to devDependencies at E0) plus a second bug I found: tslib already carries its caret, so the template emits ^^2.8.1. All three peer entries are malformed. Only one commit has ever touched that file.
  • #728 — the Storybook static build is broken on master (all four items), and nothing runs build-storybook in CI, so it has no rot detection.
  • #830 — the flaky-spec fix landed on the named spec only; 9 sibling keycloak-admin specs still use the fixed-hop settle().

None of those blocks the modernization itself. Suggested: finish the one ngModel file, then either close this epic and let #742/#728/#830 stand alone, or explicitly re-scope it to include them — right now they're orbiting the epic without being tracked in it.

**Epic status roll-up** — audited the E-series against `origin/master`. **This epic is close to done and its remaining items are small**, which the 27-comment thread makes hard to see at a glance. | Effort | State | |---|---| | E0/E3 — Nx self-hosted remote cache | ✅ live in CI | | E1a — affected lint/test on PRs | ✅ shipped | | E2a — per-project typecheck | ✅ shipped, and it routed around #664 | | E5 — Signal Forms migration | ✅ **effectively complete — 1 file left** | | E7 — strip SSR | ✅ done | | E8 — `@Service` / `@angular/aria` conventions | ✅ rules landed, adoption opportunistic by design | **E0/E3:** `main.yml:387` sets `NX_SELF_HOSTED_REMOTE_CACHE_SERVER=https://nx-cache.spikersoft.com` with the token pulled from OpenBao at `:399` (`secret/ci/angular/nx-cache`). The locked MinIO-backed decision is live. **E1a/E2a:** confirmed while auditing #824 (now closed). PRs run `nx affected -t lint`/`-t test` against `origin/master` with full `run-many` on pushes; the per-project `typecheck` target inferred by `tools/workspace-plugin` replaced the unusable root `tsc --noEmit`. Worth recording explicitly: **that means #664 was routed around rather than resolved** — the root typecheck is still unusable, but nothing depends on it now. **E5 is one file from done.** `git grep -l "ngModel"` across all app and library templates returns exactly **one**: `libraries/features/art-studio/src/lib/art-asset-viewer/art-asset-viewer.component.html`. Notably that is *not* one of the three documented ngx-mask blockers — only `marks-customer.component.html` still carries `ngx-mask`, so the blocker set has shrunk too. Worth checking whether the art-asset-viewer one is a genuine blocker or simply missed by the wave. **E7:** no `server.ts`, `@angular/ssr` or `prerender` references remain in `projects/spikersoft/project.json`. **E8** is behaving as the locked decision intended — 3 files use `@Service()` and 5 import `@angular/aria`. Both were scoped as "new code adopts, no sweep", so low counts are the expected outcome, not a gap. **What's genuinely left, and it's outside the E-series:** - **#742** — the workspace-plugin library generator emits `^undefined` peers (reads `dependencies`, but framework deps moved to `devDependencies` at E0) **plus** a second bug I found: `tslib` already carries its caret, so the template emits `^^2.8.1`. All three peer entries are malformed. Only one commit has ever touched that file. - **#728** — the Storybook static build is broken on master (all four items), and **nothing runs `build-storybook` in CI**, so it has no rot detection. - **#830** — the flaky-spec fix landed on the named spec only; **9 sibling keycloak-admin specs** still use the fixed-hop `settle()`. None of those blocks the modernization itself. **Suggested:** finish the one ngModel file, then either close this epic and let #742/#728/#830 stand alone, or explicitly re-scope it to include them — right now they're orbiting the epic without being tracked in it.
Sign in to join this conversation.