The Logs page appends to an uncapped array and forces a synchronous full change-detection pass on every SSE log record, with no virtualization and track $index. During DEBUG logging on an active sync this is unbounded memory growth and steadily rising per-record render cost.
1. Live log: unbounded records + detectChanges() per record
Severity: medium · Effort: quick · Dimension: performance · Verdict: confirmed
Location: src/angular/src/app/pages/logs/logs-page.component.ts:44,59-77,103-109
Corrected/extra locations: src/angular/src/app/pages/logs/logs-page.component.ts:69-70 (append+detectChanges) and logs-page.component.html:58-74 (uncapped @for, track $index)
Impact
records is never capped: this.records = [...this.records, record] on every 'log-record' SSE event, then this.changeDetector.detectChanges() forces a synchronous full CD pass. The template renders @for (record of records; track $index) with NO virtualization, so the DOM grows one
per log line forever. With DEBUG logging during an active sync the backend can emit many records/second; over a long-lived session this is unbounded memory growth and steadily increasing per-record render cost (full array re-create + detectChanges + ngAfterViewChecked getBoundingClientRect on every record). track $index also defeats DOM node reuse on any prepend/trim.
Recommended fix
Cap records to a ring buffer (e.g. keep last 1000-2000, slice on overflow) and render with CDK virtual scroll instead of a plain @for. Use a stable track key (record timestamp+seq) rather than $index. Optionally batch incoming records (throttle/auditTime) before detectChanges so bursts collapse into one render.
2. (dup-context) records grow unbounded with detectChanges per record
Severity: low · Effort: quick · Dimension: performance · Verdict: unverified
Location: src/angular/src/app/pages/logs/logs-page.component.ts:44, 60-77
Impact
On a busy server with the Logs page left open, records accumulates without bound; each new record copies the entire array (O(n) per event) and forces a synchronous full change-detection pass, so cost grows quadratically over time and the tab's memory climbs steadily. Only mitigated by navigating away (LogService.logs$ is a non-replay Subject, so records reset on remount).
Recommended fix
Cap the retained records to a sane window (e.g. last 1000–5000) by slicing the array when it exceeds the limit, mirroring a ring-buffer. Consider trackBy on the rendered list to reduce DOM churn.
3. Log history list uses track $index → full DOM rebuild on filter
Severity: low · Effort: quick · Dimension: performance · Verdict: unverified
Location: src/angular/src/app/pages/logs/logs-page.component.html:29-42
Impact
@for (entry of historyRecords; track $index) means Angular keys rows by position. fetchHistory returns up to 500 entries; on every search/level change the whole historyRecords array is replaced and, because tracking is by index, Angular re-renders/re-binds all up-to-500
nodes instead of reusing them. With debounced search this still rebuilds the full 500-row list per committed query.
Recommended fix
Track by a stable identity (e.g. entry.timestamp + entry.message, or a synthetic id) so unchanged rows are reused. For 500 rows consider the same virtual-scroll treatment as the live log.
Acceptance criteria
From the codebase audit — see REVIEW.md. Generated by the codebase-audit workflow; findings adversarially verified against the code.
The Logs page appends to an uncapped array and forces a synchronous full change-detection pass on every SSE log record, with no virtualization and
track $index. During DEBUG logging on an active sync this is unbounded memory growth and steadily rising per-record render cost.1. Live log: unbounded records + detectChanges() per record
Severity: medium · Effort: quick · Dimension: performance · Verdict: confirmed
Location:
src/angular/src/app/pages/logs/logs-page.component.ts:44,59-77,103-109Impact
records is never capped: this.records = [...this.records, record] on every 'log-record' SSE event, then this.changeDetector.detectChanges() forces a synchronous full CD pass. The template renders @for (record of records; track $index) with NO virtualization, so the DOM grows one
per log line forever. With DEBUG logging during an active sync the backend can emit many records/second; over a long-lived session this is unbounded memory growth and steadily increasing per-record render cost (full array re-create + detectChanges + ngAfterViewChecked getBoundingClientRect on every record). track $index also defeats DOM node reuse on any prepend/trim.
Recommended fix
Cap records to a ring buffer (e.g. keep last 1000-2000, slice on overflow) and render with CDK virtual scroll instead of a plain @for. Use a stable track key (record timestamp+seq) rather than $index. Optionally batch incoming records (throttle/auditTime) before detectChanges so bursts collapse into one render.
2. (dup-context) records grow unbounded with detectChanges per record
Severity: low · Effort: quick · Dimension: performance · Verdict: unverified
Location:
src/angular/src/app/pages/logs/logs-page.component.ts:44, 60-77Impact
On a busy server with the Logs page left open,
recordsaccumulates without bound; each new record copies the entire array (O(n) per event) and forces a synchronous full change-detection pass, so cost grows quadratically over time and the tab's memory climbs steadily. Only mitigated by navigating away (LogService.logs$ is a non-replay Subject, so records reset on remount).Recommended fix
Cap the retained records to a sane window (e.g. last 1000–5000) by slicing the array when it exceeds the limit, mirroring a ring-buffer. Consider trackBy on the rendered list to reduce DOM churn.
3. Log history list uses track $index → full DOM rebuild on filter
Severity: low · Effort: quick · Dimension: performance · Verdict: unverified
Location:
src/angular/src/app/pages/logs/logs-page.component.html:29-42Impact
@for (entry of historyRecords; track $index) means Angular keys rows by position. fetchHistory returns up to 500 entries; on every search/level change the whole historyRecords array is replaced and, because tracking is by index, Angular re-renders/re-binds all up-to-500
nodes instead of reusing them. With debounced search this still rebuilds the full 500-row list per committed query.
Recommended fix
Track by a stable identity (e.g. entry.timestamp + entry.message, or a synthetic id) so unchanged rows are reused. For 500 rows consider the same virtual-scroll treatment as the live log.
Acceptance criteria
@fortrack $indexFrom the codebase audit — see
REVIEW.md. Generated by thecodebase-auditworkflow; findings adversarially verified against the code.