Migrating a Retired Kubernetes Dashboard
from Angular 16 to 21 using Claude Code
Abstract
The official Kubernetes Dashboard was archived in January 2026 with its Angular 16 frontend unmaintained and its upgrade path blocked by deprecated tooling, an unported Material Design Components migration, and a community fork of @angular/flex-layout incompatible beyond Angular 16. Rather than replace the dashboard, this project upgraded it — five Angular major versions, six gated migration steps, forty-four catalogued issues resolved — and extended it with fourteen new native features on top of the stable Angular 21 base.
The entire migration was conducted as a pair-programming exercise with Claude Code (Anthropic's Claude Sonnet 4.6), supplemented by Claude Desktop for deep analysis sessions and Claude Web for upstream documentation lookup. This paper documents the methodology, the AI workflow, the per-step technical decisions, the QA process, the OWASP security audit, and the engineering lessons that would not survive in a git commit message.
The result is a production deployment on a bare-metal K3s cluster at 10.0.0.61, verified across all fourteen feature pages, all nine build locales, and all OWASP Top 10 categories applicable to a Kubernetes API gateway frontend.
Contents
1. Introduction
The kubernetes/dashboard repository was archived in January 2026. The maintainers announced that the Angular 16 frontend was no longer maintainable — the upstream toolchain had become fragile, peer dependency conflicts blocked every version bump, and the team had moved to a separate React rebuild. The Angular codebase, which had been the canonical Kubernetes web UI for almost a decade, was marked read-only.
The natural response to an archived project is to move on. The React rebuild was available. Lens, Headlamp, and k9s offered alternatives. And yet, for operations teams running the Angular dashboard in production, "upgrade" was not an option upstream would provide, and migration to a completely different UI was a training and workflow problem, not just a technical one.
This project chose a third path: upgrade the archived codebase rather than replace it. The motivation was not nostalgia. The Go API backend — which handles Kubernetes authentication, RBAC, resource CRUD, WebSocket proxying, and a configmap-based settings system — was worth preserving intact. The Angular Material design language, familiar to every operations team that had used the original dashboard, was worth keeping. The i18n infrastructure, covering nine locales, was worth inheriting rather than rebuilding.
The practical question was whether a five-version Angular upgrade was tractable. This paper answers that question affirmatively — and documents exactly how it was done.
2. Codebase Characterisation
2.1 Scale and architecture
The Angular frontend lives in modules/web/src/ within a Go workspace monorepo. At the start of the migration it comprised 587 TypeScript, HTML, and SCSS source files. The component count exceeded 200, organised into 30+ lazy-loaded feature modules and 5 eagerly loaded core modules. The codebase contained 31 modal dialog components, 10 Jest spec files, and a custom Makefile orchestrating a multi-locale production build.
The architecture is NgModule-based — not standalone components. Every component carries explicit standalone: false in its decorator, added during the Angular 21 upgrade step. Routing is hash-based (useHash: true), which means the Go server always receives / and the Angular router handles all subsequent navigation client-side. Locale switching requires a full page reload and a server-side redirect, because Angular's localize package generates nine separate compiled bundles — one per locale — at build time.
The build system was an important discovery in itself. The original Kubernetes Dashboard used a custom Makefile with fragile perl regex substitutions to patch locale paths into the Angular build output. Several of these regexes were written without \Q...\E quoting, meaning certain path strings could be interpreted as regex metacharacters and silently produce wrong output. This was documented as a known risk before the migration began.
2.2 Key dependencies and their problems
Several dependencies were not just outdated — they were architecturally blocked.
@angular/flex-layout was archived alongside the dashboard codebase, pinned at 15.0.0-beta.42. It had no release supporting Angular 17 or later. The community fork @ngbracket/ngx-layout preserved the identical fxFlex/fxLayout directive API and supported Angular through 21. However, as documented in Section 9, the fork introduced an important behavioral difference: flex styles are applied during a second change detection pass rather than synchronously, which became the root cause of a class of blank-page rendering bugs.
@swimlane/ngx-charts was pinned at 21.1.3, incompatible with any Angular version beyond 16. The upgrade path required precise version pinning: v22.0.0 for Angular 17, v23.1.0 for Angular 18 through 21. The wrong version compiles successfully, passes linting, and passes tests — then renders blank chart containers at runtime with no console error. This silent failure mode (catalogued as B-44) was the single most dangerous dependency in the upgrade.
codelyzer, a dead TSLint package, compiled on Node 16 but crashed on Node 20 due to native C++ bindings. It had to be removed before any Node 20 build was attempted. The removal was safe: ESLint had already replaced TSLint in the codebase, and codelyzer provided no runtime functionality.
sockjs-client and systemjs lacked proper ESM exports compatible with the ES2022 module target introduced by the Angular application builder. Both had to remain as global scripts loaded via the angular.json scripts array, accessed via declare let SockJS: any. This is a technical debt item that survives into the Angular 21 codebase.
3. Methodology
3.1 Pre-flight: the 44-issue registry
Before a single line of code was changed, every known migration issue was catalogued into a reference document: instructions/00-MIGRATION-REFERENCE.md. The document assigned each issue a B-xx identifier, a severity code (red for runtime crash, orange for build error, yellow for visual regression, green for deprecation warning), a root cause description, affected components, and the fix. Forty-four issues were catalogued in this initial audit.
The reference document served three purposes. First, it prevented duplicate investigation — when a symptom appeared during a later step, the first action was to check whether B-xx already described it. Second, it served as the authoritative source when the per-step guide and the reference disagreed: the reference always wins. Third, it provided the AI pair programmer with a structured brief at the start of each session, eliminating the need to rediscover context.
The issues broke down by step as follows: 25 issues were assigned to Step 0 (the Material Design Components migration), four to Step 1, seven to Step 2 (the most breaking step), four to Steps 3 through 5, and four were cross-cutting concerns affecting multiple steps.
3.2 The gated step protocol
The migration was divided into six discrete steps, each corresponding to a major change boundary:
- Step 0 — Material Design Components migration (Angular 14→16 legacy to MDC)
- Step 1 — Layout migration (
@angular/flex-layout→@ngbracket/ngx-layout) - Step 2 — Angular 17 (builder migration, ESLint flat config,
window.globalpolyfill) - Step 3 — Angular 18 (signal warnings, RxJS operator updates, ngx-charts v23)
- Step 4 — Angular 19 and 20 (router guard API modernisation)
- Step 5 — Angular 21 (final CSS custom property updates, locale verification)
Each step had its own guide document (02-STEP0 through 07-STEP5) specifying the exact changes required, the order in which to apply them, and the gate check: yarn build:prod and yarn test must both pass before the next step may begin. No step was started until the previous step's gate check passed. This constraint was non-negotiable — it meant that at every point in the migration, the codebase was in a known-good state for a specific Angular version, and any new failure could be attributed unambiguously to the current step's changes.
3.3 Context management across sessions
The migration spanned multiple weeks and many Claude Code sessions. Large language models have finite context windows, and the codebase is large. The solution was a shared project log — PLAN.md — maintained as a living document tracking the current step, known blockers, gate check status, and a session history table. At the start of each new session, the log and the migration reference were the first documents read. This eliminated the time otherwise spent re-establishing context from scratch.
An important observation: the migration reference and per-step guides were written in a format that was simultaneously readable by a human and useful as AI context. Short paragraphs, explicit identifiers (B-xx), and concrete before/after code examples made the documents work as both documentation and prompts.
4. The AI Development Workflow
Three distinct Claude interfaces were used at different stages of the project, each with a different role. Understanding which tool to use for which task was itself a skill developed over the course of the migration.
Claude Code (CLI)
- Primary pair programmer throughout
- Pattern matching across 200+ template files
- Breaking change analysis per Angular version
- MDC migration edge case diagnostics
- Real-time debugging during build failures
- Root cause documentation
- File editing and multi-file refactors
Claude Desktop
- Deep analysis sessions on large documents
- Reviewing entire step guide files
- Cross-referencing issues registry against code
- Architecture analysis and design review
- Quirks and oddities documentation
Claude Web
- Angular release notes and changelog lookup
- Angular Material MDC migration guides
- ngx-charts compatibility research
- Security advisory and CVE lookups
- OWASP category reference
Human (The ISMS Core Contributors)
- All architectural decisions
- Real K3s cluster smoke testing
- Visual screenshots and UI judgment
- RBAC and security policy review
- Feature design and UX direction
- Go backend and API design
- Final gate check sign-off
4.1 Claude Code — the primary interface
Claude Code ran as the terminal pair programmer throughout the migration. Its most valuable capability was not code generation but pattern recognition at scale. When a breaking change affected a directive attribute name across 847 template files, Claude Code could locate all occurrences, categorise them by context, and apply the correct fix to each — in a single session without manually opening a single file. This kind of systematic, context-aware find-and-replace across a large TypeScript/HTML/SCSS codebase is exactly where human attention fatigues and makes errors.
The workflow was conversational rather than prompt-and-execute. A typical session began with a brief stating the current step, any known blockers from the previous session (from the project log), and the gate check status. Claude Code would then read the relevant step guide, cross-reference the issues registry, propose a plan, execute it file by file, and report what changed. The human engineer would verify each change before approving the next batch.
Claude Code's memory system — persistent across sessions within the same project directory — proved essential. Feedback from earlier sessions ("don't use overflow: hidden in card containers, it breaks sticky columns") persisted automatically and influenced later decisions without requiring explicit re-instruction.
4.2 Claude Desktop — deep analysis
Claude Desktop was used for sessions requiring full-document analysis rather than file editing. The 44-issue registry, at 2,400 lines, could be loaded in its entirety and cross-referenced against the codebase without the context pressure of an active editing session. Architecture reviews — examining the GlobalServicesModule anti-pattern, the HttpHeaders immutability bug, and the IntersectionObserver memory leak — were conducted in Desktop where the full component trees could be held in context simultaneously.
The quirks-and-oddities document (a catalogue of pre-existing bugs, anti-patterns, and non-obvious behaviors in the original codebase) was produced entirely in Desktop sessions. Its findings informed decisions about what to fix versus what to leave as known technical debt.
4.3 Claude Web — upstream research
Claude Web handled any question requiring current upstream documentation. Angular's MDC migration guide, the @angular/material v15 CHANGELOG, ngx-charts release notes, and the OWASP Top 10 category definitions were all researched via Web rather than relying on training data that might be stale or incomplete for recent releases. Security advisory lookups (Go CVEs via OSV, npm advisories) were also conducted via Web to get current data rather than training-cutoff-era information.
4.4 Human role
The AI interfaces handled analysis, pattern matching, file editing, and documentation. What they cannot do:
Architectural decisions. Whether to preserve the NgModule architecture or migrate to standalone components, whether to keep the Go API intact or refactor it, whether to add a feature as a new page or a panel within an existing page — these decisions require context about the team, the deployment environment, and constraints the AI does not have access to.
Real-cluster smoke testing. Every gated step ended with a deployment to a K3s cluster running on NUC-02 at 10.0.0.61, namespace k8s-native. Screenshots were taken of every feature page. Visual regressions — a tooltip appearing in the wrong color, a chart rendering at the wrong size, a status pill clipping at the wrong point — were identified by a human looking at a real browser, not by a linter or a Jest test. Several of the most subtle bugs in the QA report were found exclusively through this process.
Design judgment. Color choices for status pills, the decision to use 24px toggle buttons rather than the default 36px, the placement of the AI chat drawer — these involved aesthetic and UX decisions that were made by the human engineer and then implemented by Claude Code.
5. Migration Steps: Technical Detail
5.1 Step 0 — Material Foundation
Step 0 was the longest and most consequential step. It did not upgrade Angular at all — it prepared the codebase for the MDC migration that Angular Material v15 required. Twenty-five of the forty-four catalogued issues belonged here.
The Angular Material Design Components (MDC) migration replaced every Material v2 legacy component with an MDC equivalent. The visual output was similar, but the internal DOM structure, CSS class names, and form field APIs changed in breaking ways. The most impactful changes:
Mat-chip-list → mat-chip-set (B-02). The mat-chip-list element was removed and replaced by either mat-chip-listbox (for interactive/selectable chips) or mat-chip-set (for display-only chips). The dashboard used chips extensively for labels and annotations on every resource detail page. Choosing the wrong replacement produced chips that were styled as interactive even when they were display-only, breaking keyboard navigation and screen reader semantics.
CSS class renaming (B-05). All Material class names changed from mat-* to mat-mdc-*. The entire table CSS section in index.scss — covering list page padding, header cell styling, row borders, action column positioning, and sort indicators — had to be rewritten against the new class names. Importantly, Angular Material only renames its own generated classes; user-defined Angular column definition classes (e.g., mat-column-actions) retain the original mat-column- prefix. Misapplying the rename caused the action column to become invisible on all list pages (documented later as QA finding D-06).
Form field appearance (B-42). Material MDC changed the default form field appearance from legacy (underline only) to fill (filled background). Thirty-five form field instances across the codebase — including the login token input — reverted to the fill appearance. The fix was a single global provider: {provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: {appearance: 'outline'}} in shared.module.ts. This one-line change fixed all 35 instances but introduced a secondary regression: the namespace selector in the toolbar inherited appearance: 'outline' and rendered a visible border box in the nav bar, requiring a targeted override in the toolbar context.
Mat-slider (B-01). The MDC mat-slider requires a child <input matSliderThumb> element; the value binding and form control must be placed on the child input, not on the slider host. The original dashboard placed [formControlName] directly on <mat-slider>. After migration this silently disconnected the form binding — sliders rendered correctly but were completely unresponsive. This bug survived into QA (S-01) because it required testing the actual settings save flow, not just visual inspection.
The SCSS theming layer was rewritten from scratch. The original _dark.scss, _light.scss, and _theming.scss files used Material v2 palette APIs that were removed in MDC. New files defined three custom palettes — $kd-blue, $kd-dark, and $kd-light — and overrode the MDC CSS custom properties directly rather than using the deprecated mat.define-light-theme()/mat.define-dark-theme() approach.
5.2 Step 1 — Layout Migration
The @angular/flex-layout package was archived and released no version supporting Angular 17 or later. The community fork @ngbracket/ngx-layout preserved the identical fxFlex, fxLayout, and fxLayoutAlign directive API surface and is maintained through Angular 21.
The migration was largely mechanical: update the package, update the import in shared.module.ts, verify 847 directive usages across 200+ templates. No directive attribute syntax changed. Gate check passed without further intervention.
The behavioral difference between the original package and the fork — that flex styles are applied during Angular's second change detection pass rather than the first — was not immediately apparent. It would surface as a class of blank-page rendering bugs (Section 9) only after feature integration, when HTTP callbacks triggered outside Angular's zone began creating components that used fxLayout.
5.3 Step 2 — Angular 17
Step 2 was the most breaking step. Angular 17 replaced the Webpack-based browser builder with the esbuild-based application builder, changed the locale output path, and removed several APIs that had been deprecated for multiple versions.
Builder migration. The application builder required updating angular.json builder targets, updating the serve configuration's buildTarget key (renamed from browserTarget, B-35), and migrating from polyfills.ts to inline polyfill declarations. Esbuild produces significantly faster builds but has stricter ES module requirements — which is why sockjs-client and systemjs could not be migrated to regular imports.
Locale output path (B-31). The Angular 17 application builder added a browser/ subdirectory to localized build output. Where the original builder produced dist/public/en/index.html, the new builder produced dist/public/browser/en/index.html. The Go locale server expected the original path structure. The fix required two changes: setting "browser": "" in the angular.json outputPath configuration to suppress the subdirectory, and hardening the Makefile perl substitutions with \Q...\E quoting to prevent metacharacter interpretation. This bug is invisible during development (ng serve is unaffected) and produces no error during build — the only symptom is a 404 for every asset in production.
ESLint flat config (B-23, B-24). ESLint 9 requires a flat configuration file (eslint.config.js) rather than the legacy .eslintrc.yaml format. The migration involved replacing eslint-plugin-rxjs (incompatible with ESLint 9) with eslint-plugin-rxjs-x and renaming eslint-plugin-node to eslint-plugin-n.
window.global polyfill (B-16). The esbuild migration removed polyfills.ts, which had contained the Ace editor's window.global = window polyfill. Without this, the Ace editor crashed on every edit dialog open. The fix was a single inline script tag in src/index.html.
5.4 Step 3 — Angular 18
Angular 18 introduced signal-based component APIs with deprecation warnings for the legacy @Component decorator patterns. These warnings did not block builds but required suppression via suppressImplicitAnyIndexErrors compiler options in cases where the linter interpreted them as errors.
B-44 reappeared: the ngx-charts version required for Angular 18 is 23.1.0, different from the 22.0.0 required for Angular 17. Additionally, 23.1.0 requires a peer dependency override in package.json because it declares a peer on Angular 18 but the version range is too narrow for Angular 19-21. The override must be maintained through all remaining steps.
5.5 Steps 4 and 5 — Angular 19 through 21
Steps 4 and 5 were comparatively minor. The router guard API modernisation (replacing class-based guards implementing CanActivate with function-based CanActivateFn guards) was the largest change in Step 4. Step 5 required updating Angular Material CSS custom property references where v18 had changed the property names for button container colors and slider thumb sizing.
The final gate check for Step 5 included an explicit verification that ls .dist/public/ showed locale directories (en/, de/, fr/, etc.) at the top level and not inside a browser/ subdirectory — the B-31 check, repeated after every major version bump as a discipline.
6. Feature Integration
After the Angular 21 gate check passed, fourteen new routes and feature pages were added. The upstream dashboard provided Workloads, Service & Discovery, Config & Storage, and Cluster-level resource views. The feature integration added the following:
| Route | Feature | Backend | Key technical detail |
|---|---|---|---|
/overview | Cluster Overview | metrics.k8s.io + VictoriaMetrics | Network traffic sparklines; 1h/6h/24h/7d time range toggles; forkJoin + CDR |
/clustermap | Cluster Map | nodes + pods API | Force-directed graph; node/pod status colouring |
/timeline | Event Timeline | api/v1/event | 500-event poll; kind colour map; live toggle gates polling |
/rbac | RBAC Viewer | api/v1/rbac | Wildcard flag; multiTemplateDataRows required for expand |
/certs | Certificate Tracker | api/v1/certs | crypto/x509 backend parsing; days-remaining coloured label |
/audit | Policy Audit | api/v1/audit | 14 OPA-style checks against live pod specs; namespace summary tiles |
/registries | Registry Manager | secrets API | Registry type icon by hostname pattern; expandable workload rows |
/efficiency | Resource Efficiency | api/v1/efficiency | Goldilocks-style per-container verdicts; CSV export via Blob |
/certmanager | Certificate Manager | cert-manager.io CRDs | CRD-based detection; forkJoin parallel fetch |
/metallb | MetalLB | metallb.io CRDs | IPAddressPool utilisation bar; avoidBuggyIPs flag |
/gateway | Gateway API | gateway.networking.k8s.io CRDs | Listener chips; backend ref chips; CRD-based detection |
/security | Kubescape Scanner | Kubescape operator CRDs | 404 → not-installed notice; NVD links; remediation field |
/storageoverview | Storage Overview | kubelet stats API | PVC usage bars; volume health/capacity pie charts; human-readable byte tooltips |
/about | About | — | Version, license, acknowledgements |
All new feature pages follow a universal implementation pattern established during integration:
- Subscription cleanup via
takeUntilDestroyed(inject(DestroyRef)) - Polling via
interval(N).pipe(startWith(0), takeUntilDestroyed) - HTTP error handling via
catchError(() => of({items: []}))to prevent one failing resource type from blocking others this.cdr_.detectChanges()as the last line of every.subscribe()callback:host { display: block }in every feature component's SCSS
Optional integrations (VictoriaMetrics, Kubescape, Gateway API, MetalLB, cert-manager) are detected at runtime via CRD presence checks. Nav items appear only when the corresponding operator is running in the cluster. No configuration is required.
7. Quality Assurance
QA was conducted in two phases: a structured code review pass (five regression stages covering the entire codebase) and a live inspection against a real K3s cluster deployment.
7.1 Regression testing stages
The regression review was divided into five stages, each targeting a different layer of the application:
- Stage 1 — Core services and architecture: Bootstrap sequence, module structure, service injection, APP_INITIALIZER chain, router configuration.
- Stage 2 — CSS, services, and shell: Global SCSS, theme system, nav component, toolbar, namespace selector.
- Stage 3 — Bootstrap guards and services: AuthGuard, cache interceptor, notification service, history service.
- Stage 4 — Resource lists and detail pages: All upstream resource list components, detail pages, pagination, sorting.
- Stage 5 — Dialogs, dynamic components, and list details: All 31 modal dialogs, dynamic column components, logs, exec terminal.
Notable findings from the regression stages:
GraphCardComponent.shouldShowGraph() returned true for null (because null !== undefined in JavaScript). On clusters without a metrics server, selectedMetric would be null rather than undefined, producing a NullReferenceError at runtime. Fix: return !!this.selectedMetric.
VerberService.getHttpHeaders_() calls .set() on Angular's HttpHeaders but discards the return value. HttpHeaders is immutable — every .set() returns a new instance; the original is unchanged. The method returns empty headers. This means all edit/delete/patch operations rely on Angular's HttpClient auto-adding Content-Type: application/json when the body is a JSON object, which it does — making the bug invisible in practice, but the code is wrong.
7.2 Live inspection findings
The live inspection against the cluster deployment found 14 issues across four categories, all fixed over four rounds:
- Critical functional (2): Ace editor blank in all edit dialogs (MDC animation timing); settings sliders visually correct but form-unbound (B-01 missed in initial Step 0 pass).
- Critical layout (9): All nine new feature pages used a
div.kd-feature-container+mat-toolbarlayout pattern imported from the React project, rendering as a floating toolbar on a dark void. All nine were replaced with nativekd-cardcomponents. - Critical theme (3): Toolbar background color mismatch; namespace selector visible border box in dark theme; action column (three-dot menu) invisible at 100% zoom due to incorrect MDC class rename.
8. Security Hardening
An OWASP Top 10 audit was conducted against the deployed application and the Go API backend. The relevant findings and resolutions:
| Category | Finding | Resolution |
|---|---|---|
| A05 — Security Misconfiguration | No HTTP security headers in gin router | Added securityHeaders() middleware: X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: strict-origin, Permissions-Policy: camera=(), microphone=(), geolocation=(). HSTS added via Kong response-transformer plugin at the TLS termination layer. |
| A06 — Vulnerable Components | Multiple CVEs in Go modules: golang.org/x/net, golang.org/x/crypto, golang.org/x/sys |
All three updated to current versions, resolving 21 CVEs including HTTP/2 DoS, HPACK header injection, and cryptographic issues. 111 CVEs in 49 npm packages were dev toolchain only (babel, webpack, jest) — no production bundle exposure. |
| A07 — CSRF | CSRF middleware fires on POST only; PUT/DELETE not covered | Accepted: extending coverage to PUT/DELETE caused immediate validation failures because the Angular AuthInterceptor never sends the CSRF token on those methods. The Bearer token custom header is itself a CSRF defence — cross-origin requests cannot set custom headers without CORS preflight approval. |
| A02 — Cryptographic | Auth token in JavaScript-readable cookie (not httpOnly) | Accepted architectural constraint: the SPA must read the token to inject it as a custom Authorization header. Mitigated by Angular's XSS protection and cluster-internal deployment scope. |
| A03 — Injection | System banner uses [innerHTML] |
Not an issue: Angular auto-sanitizes all [innerHTML] bindings. No bypassSecurityTrust* usage on user-supplied content. |
A Python script (cve_audit.py) was written to automate future CVE scanning, querying the OSV API against both yarn.lock and go.mod. It runs with --web, --go, or no flag for both, and outputs JSON for CI integration when CI is eventually added.
9. Change Detection: A Recurring Pattern
The most pervasive class of bugs in the post-integration phase — appearing on fifteen separate component pages — was a rendering failure that looked like a blank page on first navigation, resolved by moving the mouse. The symptom was consistent; the root causes were three distinct mechanisms that produced the identical user-visible effect.
9.1 Root cause 1 — Missing detectChanges()
Angular's OnPush change detection strategy defers re-render until an input reference changes or an event fires. HTTP observable callbacks run partially outside Angular's zone.js — assigning component properties inside .subscribe() does not guarantee that Angular will schedule a re-render. The fix is explicit:
constructor(private readonly cdr_: ChangeDetectorRef) {}
ngOnInit(): void {
this.http_.get<DataType>('api/v1/endpoint').subscribe(data => {
this.someProperty = data;
this.loading = false;
this.cdr_.detectChanges(); // required — last line of every subscribe
});
}
This pattern is now applied universally: every component that assigns properties inside a .subscribe() callback calls detectChanges() as its last action. The call is cheap and idempotent.
9.2 Root cause 2 — Missing :host { display: block }
Angular component host elements default to display: inline. An inline element does not participate in block or flex layout correctly — its children's heights collapse on the first browser layout pass. The MetalLB component had detectChanges() implemented correctly, but its host element (<kd-metallb>) rendered inline, causing all flex children to have zero computed height on first paint. Moving the mouse triggered a browser reflow that corrected the heights. Fix: :host { display: block } in every feature component's SCSS. This rule is now applied defensively to all twelve custom feature pages.
9.3 Root cause 3 — fxLayout inside *ngIf with detectChanges()
The Cluster Overview component wrapped its entire content in <div *ngIf="!loading">. When detectChanges() fired outside zone.js after the HTTP callbacks resolved, Angular created all child elements — including the ngx-layout fxLayout directives. The @ngbracket/ngx-layout fork applies flex CSS during Angular's lifecycle hooks, but because element creation happened outside zone.js, that application was deferred until the next real zone.js event (mouse move). The fix was to replace all fxLayout, fxLayoutAlign, and fxLayoutGap directives in the affected templates with static CSS classes defined in the component's SCSS file:
/* clusteroverview/style.scss */
.kd-chart-row {
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
align-items: center;
}
.kd-chart-col {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
All three root causes produce an identical symptom — blank page on first navigation, correct on mouse move — but require different fixes. The diagnostic order is: (1) check whether detectChanges() is called; (2) check whether :host { display: block } is present; (3) check whether fxLayout directives are used inside conditionally-rendered content blocks.
10. Results
The migration produced a production deployment verified on a real K3s cluster. The following table summarises the before/after state:
| Metric | Before | After |
|---|---|---|
| Angular version | 16.2.1 (archived) | 21.x |
| Angular Material | 14.2.7 (v2 legacy) | 21.x (MDC) |
| Build system | Webpack (browser builder) | esbuild (application builder) |
| ESLint config | .eslintrc.yaml (legacy) | eslint.config.js (flat) |
| Node requirement | >=16.14.0 | >=20.0.0 |
| Feature pages | Workloads, Discovery, Config, Cluster | + 14 new routes |
| HTTP security headers | None | 4 headers + HSTS via Kong |
| CVEs (Go modules) | 21 known | 0 |
| i18n locales | 9 (preserved) | 9 (preserved) |
| Go backend | Original intact | Original intact + 14 new endpoints |
The Go API backend was preserved entirely. No routes, authentication middleware, RBAC handling, or WebSocket proxying code was modified except for two targeted bug fixes: dynamic configmap naming in settings.go and prefs.go (the original hardcoded kubernetes-dashboard-web-settings regardless of namespace, breaking deployments in non-default namespaces), and expansion of the AI FallbackModels list from 3 to 9 entries.
11. Key Technical Lessons
The following are the engineering lessons from this migration that are most likely to apply to any large Angular upgrade project:
Catalogue before you code
Forty-four issues were known before the first line of code changed. The alternative — discovering issues as you go — would have produced a similar number of issues but without the structured B-xx identifiers, severity codes, or root cause documentation. The reference document paid for its own creation cost within the first three days of Step 0.
B-44: ngx-charts version is a silent runtime killer
Wrong version compiles, lints, and tests successfully. It renders blank chart containers at runtime with no console error. Pin it explicitly in package.json overrides at every major Angular version, and add a post-build visual check to your gate criteria.
B-31: locale output path changes silently
The Angular 17 builder output path change produces no build error and no warning. Verify ls .dist/public/ after every major version bump. Do not rely on the build succeeding as a proxy for the output being correct.
overflow: clip, not overflow: hidden
overflow: hidden creates a new stacking context, which prevents position: sticky on descendant elements from working. The action column (three-dot menu) in every table relies on sticky positioning. Use overflow: clip (Chrome 90+, Safari 16+) — it clips content without creating a stacking context.
Every subscribe needs detectChanges()
HTTP callbacks in lazy-loaded OnPush components run partially outside zone.js. Property assignment inside .subscribe() does not guarantee a re-render. Add this.cdr_.detectChanges() as the last line of every subscribe callback, unconditionally. The overhead is negligible; the alternative is fifteen pages of debugging.
:host { display: block } is not optional
Angular component host elements default to display: inline. Any component that acts as a block-level layout container must declare :host { display: block } in its SCSS. Add it defensively to every feature component, not just ones where you've seen the symptom.
The AI pair's strongest contribution is pattern recognition, not code generation
The most impactful sessions were not "write this component" but "find every occurrence of this pattern across 200 files and tell me which ones need to change and why." That kind of systematic cross-file analysis is where human attention fatigues and AI assistance provides genuine leverage.
12. Conclusion
The conventional response to an archived frontend project is replacement. This project demonstrates that systematic upgrade is tractable — at a scale (587 files, five major Angular versions, 200+ components) that most practitioners would consider prohibitive — given three preconditions: a complete issue catalogue before work begins, a gated step protocol that never permits forward progress on a broken build, and a pair programmer capable of pattern recognition at file-system scale.
The AI pair programming workflow that emerged from this project is not a replacement for engineering judgment. Every architectural decision, every gate check sign-off, and every real-cluster smoke test required a human. What Claude Code replaced was the mechanical work: finding all 847 directive usages, applying a consistent fix pattern to fifteen components, cross-referencing a 44-item issue registry against hundreds of files. That mechanical work is also where human error accumulates — missed occurrences, inconsistent fixes, decisions made from fatigue rather than analysis.
The Kubernetes Dashboard is archived upstream. This version runs in production at 10.0.0.61. The gap between those two facts is documented here.
Manifests repository: github.com/isms-core-project/kubernetes-dashboard — deployment manifests (public). The Angular and Go source repositories are private.
License: Apache 2.0. Original Kubernetes Dashboard © 2017 The Kubernetes Authors. Extensions © 2026 The ISMS Core Project.
AI tooling: Claude Code (claude-sonnet-4-6), Claude Desktop, and Claude Web — all Anthropic products. The AI interfaces assisted implementation; all architectural decisions, gate check approvals, and cluster smoke testing were conducted by the human engineer.
Not affiliated with the Cloud Native Computing Foundation (CNCF) or the Kubernetes project.