# Error Dashboard Phase 1 — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Build a workspace-level Error Dashboard (listing + detail views) under the Monitor sidebar section, providing unified error triage across all container types.

**Architecture:** Two new standalone page components connected by Angular router — a listing page with filters, summary cards, and sparkline table, and a detail page with a two-column layout showing error context, histogram, performance comparison, and stack issues. Backed by a new `errorDashboard` NgRx feature slice that coexists with the existing `errorLog` slice.

**Tech Stack:** Angular 20 (standalone components, signals, OnPush), NgRx (actions/effects/reducer/selectors), Bootstrap 5, FontAwesome Pro, Luxon for relative dates.

**Spec:** `docs/superpowers/specs/2026-03-26-error-dashboard-design.md`

**Codebase root:** `/Users/brice/git/cloud-frontend`

---

## File Map

### New Files

| File | Purpose |
|------|---------|
| `src/app/workspace/interfaces/error-dashboard-state.interface.ts` | State shape for errorDashboard slice |
| `src/app/workspace/interfaces/error-dashboard-stats.interface.ts` | IErrorDashboardStats + TErrorContainerType |
| `src/app/workspace/enums/error-dashboard-actions.enum.ts` | Action type string constants |
| `src/app/workspace/actions/error-dashboard.action.ts` | NgRx action creators |
| `src/app/workspace/reducers/error-dashboard.reducer.ts` | Reducer with initial state |
| `src/app/workspace/selectors/error-dashboard-feature.selector.ts` | Feature selector |
| `src/app/workspace/selectors/get-error-dashboard-signatures.selector.ts` | Signatures list selector |
| `src/app/workspace/selectors/get-error-dashboard-stats.selector.ts` | Stats selector |
| `src/app/workspace/selectors/get-error-dashboard-loading.selector.ts` | Loading state selector |
| `src/app/workspace/selectors/get-error-dashboard-filters.selector.ts` | All filters selector |
| `src/app/workspace/selectors/get-error-dashboard-active-signature.selector.ts` | Active signature selector |
| `src/app/workspace/selectors/get-error-dashboard-active-detail.selector.ts` | Active error detail selector |
| `src/app/workspace/selectors/get-error-dashboard-history-list.selector.ts` | Error history list selector |
| `src/app/workspace/selectors/get-error-dashboard-loading-detail.selector.ts` | Detail loading selector |
| `src/app/workspace/selectors/get-error-dashboard-page.selector.ts` | Page number selector |
| `src/app/workspace/selectors/get-error-dashboard-total-pages.selector.ts` | Total pages selector |
| `src/app/workspace/effects/error-dashboard.effect.ts` | Side effects for API calls |
| `src/app/workspace/utils/trend.util.ts` | Linear regression + trend classification |
| `src/app/workspace/components/error-sparkline/error-sparkline.component.ts` | Inline bar chart |
| `src/app/workspace/components/error-sparkline/error-sparkline.component.html` | Sparkline template |
| `src/app/workspace/components/error-sparkline/error-sparkline.component.scss` | Sparkline styles |
| `src/app/workspace/components/error-summary-cards/error-summary-cards.component.ts` | Container type cards |
| `src/app/workspace/components/error-summary-cards/error-summary-cards.component.html` | Cards template |
| `src/app/workspace/components/error-summary-cards/error-summary-cards.component.scss` | Cards styles |
| `src/app/workspace/components/error-histogram/error-histogram.component.ts` | 24h occurrence chart |
| `src/app/workspace/components/error-histogram/error-histogram.component.html` | Histogram template |
| `src/app/workspace/components/error-histogram/error-histogram.component.scss` | Histogram styles |
| `src/app/workspace/components/error-performance-panel/error-performance-panel.component.ts` | Error vs standard comparison |
| `src/app/workspace/components/error-performance-panel/error-performance-panel.component.html` | Performance template |
| `src/app/workspace/components/error-performance-panel/error-performance-panel.component.scss` | Performance styles |
| `src/app/workspace/components/error-stack-issues-panel/error-stack-issues-panel.component.ts` | Other errors in same stack |
| `src/app/workspace/components/error-stack-issues-panel/error-stack-issues-panel.component.html` | Stack issues template |
| `src/app/workspace/components/error-stack-issues-panel/error-stack-issues-panel.component.scss` | Stack issues styles |
| `src/app/workspace/pages/workspace-error-dashboard-page/workspace-error-dashboard-page.component.ts` | Listing page |
| `src/app/workspace/pages/workspace-error-dashboard-page/workspace-error-dashboard-page.component.html` | Listing template |
| `src/app/workspace/pages/workspace-error-dashboard-page/workspace-error-dashboard-page.component.scss` | Listing styles |
| `src/app/workspace/pages/workspace-error-detail-page/workspace-error-detail-page.component.ts` | Detail page |
| `src/app/workspace/pages/workspace-error-detail-page/workspace-error-detail-page.component.html` | Detail template |
| `src/app/workspace/pages/workspace-error-detail-page/workspace-error-detail-page.component.scss` | Detail styles |

### Modified Files

| File | Change |
|------|--------|
| `src/app/workspace/services/error-log-data.service.ts` | Add `getDashboardStats()` and `getSignaturesForDashboard()` methods |
| `src/app/app.module.ts` | Register `errorDashboard` reducer and `ErrorDashboardEffect` |
| `src/app/core/config/routes.config.ts` | Add error-dashboard and error-detail routes |
| `src/app/core/components/workspace-side-nav/workspace-side-nav.component.ts` | Add Error Dashboard to monitoringNavItems |

---

## Task 1: Interfaces & Types

**Files:**
- Create: `src/app/workspace/interfaces/error-dashboard-stats.interface.ts`
- Create: `src/app/workspace/interfaces/error-dashboard-state.interface.ts`

- [ ] **Step 1: Create the dashboard stats interface**

```typescript
// src/app/workspace/interfaces/error-dashboard-stats.interface.ts
import { TErrorSignatureStatus } from './error-signature.interface';

export type TErrorContainerType = 'query' | 'function' | 'task' | 'middleware' | 'trigger';

export interface IErrorDashboardTypeStat {
  type: TErrorContainerType;
  total: number;
  gain24h: number;
}

export interface IErrorDashboardStats {
  byType: IErrorDashboardTypeStat[];
}
```

- [ ] **Step 2: Create the dashboard state interface**

```typescript
// src/app/workspace/interfaces/error-dashboard-state.interface.ts
import { IErrorSignature, TErrorSignatureStatus } from './error-signature.interface';
import { IErrorHistory } from './error-history.interface';
import { IErrorDashboardStats, TErrorContainerType } from './error-dashboard-stats.interface';

export interface IErrorDashboardState {
  // Listing
  signatures: IErrorSignature[];
  stats: IErrorDashboardStats | null;
  loading: boolean;
  page: number;
  totalPages: number;

  // Filters
  textFilter: string;
  containerFilter: TErrorContainerType | null;
  statementFilter: string | null;
  statusFilter: TErrorSignatureStatus | null;

  // Detail
  activeSignature: IErrorSignature | null;
  activeErrorDetail: IErrorHistory | null;
  errorHistoryList: IErrorHistory[];
  loadingDetail: boolean;
}
```

- [ ] **Step 3: Commit**

```bash
git add src/app/workspace/interfaces/error-dashboard-stats.interface.ts src/app/workspace/interfaces/error-dashboard-state.interface.ts
git commit -m "feat(error-dashboard): add interfaces for dashboard state and stats"
```

---

## Task 2: NgRx Actions

**Files:**
- Create: `src/app/workspace/enums/error-dashboard-actions.enum.ts`
- Create: `src/app/workspace/actions/error-dashboard.action.ts`

- [ ] **Step 1: Create the action enum**

```typescript
// src/app/workspace/enums/error-dashboard-actions.enum.ts
export enum EErrorDashboardActions {
  // Effects
  ErrorDashboardLoadSignatures = '[Error Dashboard] Load Signatures',
  ErrorDashboardLoadStats = '[Error Dashboard] Load Stats',
  ErrorDashboardLoadDetail = '[Error Dashboard] Load Detail',
  ErrorDashboardLoadHistoryList = '[Error Dashboard] Load History List',
  ErrorDashboardUpdateStatus = '[Error Dashboard] Update Status',

  // Reducers
  ErrorDashboardSetSignatures = '[Error Dashboard] Set Signatures',
  ErrorDashboardSetStats = '[Error Dashboard] Set Stats',
  ErrorDashboardSetActiveSignature = '[Error Dashboard] Set Active Signature',
  ErrorDashboardSetActiveDetail = '[Error Dashboard] Set Active Detail',
  ErrorDashboardSetHistoryList = '[Error Dashboard] Set History List',
  ErrorDashboardSetFilters = '[Error Dashboard] Set Filters',
  ErrorDashboardSetPage = '[Error Dashboard] Set Page',
  ErrorDashboardSetLoading = '[Error Dashboard] Set Loading',
  ErrorDashboardSetLoadingDetail = '[Error Dashboard] Set Loading Detail',
  ErrorDashboardResetState = '[Error Dashboard] Reset State',
}
```

- [ ] **Step 2: Create the action creators**

```typescript
// src/app/workspace/actions/error-dashboard.action.ts
import { createAction, props } from '@ngrx/store';
import { EErrorDashboardActions } from '../enums/error-dashboard-actions.enum';
import { IErrorSignature, TErrorSignatureStatus } from '../interfaces/error-signature.interface';
import { IErrorHistory } from '../interfaces/error-history.interface';
import { IErrorDashboardStats, TErrorContainerType } from '../interfaces/error-dashboard-stats.interface';

// Effects
export const ErrorDashboardLoadSignatures = createAction(
  EErrorDashboardActions.ErrorDashboardLoadSignatures,
  props<{
    workspaceId: number;
    branchId: number;
    page?: number;
    perPage?: number;
    type?: TErrorContainerType;
    status?: TErrorSignatureStatus;
    search?: string;
    statementFilter?: string;
    refresh?: boolean;
  }>(),
);

export const ErrorDashboardLoadStats = createAction(
  EErrorDashboardActions.ErrorDashboardLoadStats,
  props<{ workspaceId: number; branchId: number }>(),
);

export const ErrorDashboardLoadDetail = createAction(
  EErrorDashboardActions.ErrorDashboardLoadDetail,
  props<{ workspaceId: number; signatureId: number }>(),
);

export const ErrorDashboardLoadHistoryList = createAction(
  EErrorDashboardActions.ErrorDashboardLoadHistoryList,
  props<{ workspaceId: number; signatureId: number; page?: number; perPage?: number }>(),
);

export const ErrorDashboardUpdateStatus = createAction(
  EErrorDashboardActions.ErrorDashboardUpdateStatus,
  props<{
    workspaceId: number;
    branchId: number;
    signatureId: number;
    status: TErrorSignatureStatus;
  }>(),
);

// Reducers
export const ErrorDashboardSetSignatures = createAction(
  EErrorDashboardActions.ErrorDashboardSetSignatures,
  props<{ signatures: IErrorSignature[]; totalPages: number }>(),
);

export const ErrorDashboardSetStats = createAction(
  EErrorDashboardActions.ErrorDashboardSetStats,
  props<{ stats: IErrorDashboardStats }>(),
);

export const ErrorDashboardSetActiveSignature = createAction(
  EErrorDashboardActions.ErrorDashboardSetActiveSignature,
  props<{ signature: IErrorSignature | null }>(),
);

export const ErrorDashboardSetActiveDetail = createAction(
  EErrorDashboardActions.ErrorDashboardSetActiveDetail,
  props<{ detail: IErrorHistory | null }>(),
);

export const ErrorDashboardSetHistoryList = createAction(
  EErrorDashboardActions.ErrorDashboardSetHistoryList,
  props<{ historyList: IErrorHistory[] }>(),
);

export const ErrorDashboardSetFilters = createAction(
  EErrorDashboardActions.ErrorDashboardSetFilters,
  props<{
    textFilter?: string;
    containerFilter?: TErrorContainerType | null;
    statementFilter?: string | null;
    statusFilter?: TErrorSignatureStatus | null;
  }>(),
);

export const ErrorDashboardSetPage = createAction(
  EErrorDashboardActions.ErrorDashboardSetPage,
  props<{ page: number }>(),
);

export const ErrorDashboardSetLoading = createAction(
  EErrorDashboardActions.ErrorDashboardSetLoading,
  props<{ loading: boolean }>(),
);

export const ErrorDashboardSetLoadingDetail = createAction(
  EErrorDashboardActions.ErrorDashboardSetLoadingDetail,
  props<{ loadingDetail: boolean }>(),
);

export const ErrorDashboardResetState = createAction(
  EErrorDashboardActions.ErrorDashboardResetState,
);
```

- [ ] **Step 3: Commit**

```bash
git add src/app/workspace/enums/error-dashboard-actions.enum.ts src/app/workspace/actions/error-dashboard.action.ts
git commit -m "feat(error-dashboard): add NgRx actions and action enum"
```

---

## Task 3: NgRx Reducer

**Files:**
- Create: `src/app/workspace/reducers/error-dashboard.reducer.ts`

- [ ] **Step 1: Create the reducer**

```typescript
// src/app/workspace/reducers/error-dashboard.reducer.ts
import { createReducer, on } from '@ngrx/store';
import { IErrorDashboardState } from '../interfaces/error-dashboard-state.interface';
import {
  ErrorDashboardSetSignatures,
  ErrorDashboardSetStats,
  ErrorDashboardSetActiveSignature,
  ErrorDashboardSetActiveDetail,
  ErrorDashboardSetHistoryList,
  ErrorDashboardSetFilters,
  ErrorDashboardSetPage,
  ErrorDashboardSetLoading,
  ErrorDashboardSetLoadingDetail,
  ErrorDashboardResetState,
} from '../actions/error-dashboard.action';

const initialState: IErrorDashboardState = {
  signatures: [],
  stats: null,
  loading: false,
  page: 1,
  totalPages: 1,
  textFilter: '',
  containerFilter: null,
  statementFilter: null,
  statusFilter: null,
  activeSignature: null,
  activeErrorDetail: null,
  errorHistoryList: [],
  loadingDetail: false,
};

export const ErrorDashboardReducer = createReducer(
  initialState,
  on(ErrorDashboardSetSignatures, (state, { signatures, totalPages }) => ({
    ...state,
    signatures,
    totalPages,
    loading: false,
  })),
  on(ErrorDashboardSetStats, (state, { stats }) => ({
    ...state,
    stats,
  })),
  on(ErrorDashboardSetActiveSignature, (state, { signature }) => ({
    ...state,
    activeSignature: signature,
    activeErrorDetail: null,
    errorHistoryList: [],
  })),
  on(ErrorDashboardSetActiveDetail, (state, { detail }) => ({
    ...state,
    activeErrorDetail: detail,
    loadingDetail: false,
  })),
  on(ErrorDashboardSetHistoryList, (state, { historyList }) => ({
    ...state,
    errorHistoryList: historyList,
  })),
  on(ErrorDashboardSetFilters, (state, action) => ({
    ...state,
    textFilter: action.textFilter ?? state.textFilter,
    containerFilter: action.containerFilter !== undefined ? action.containerFilter : state.containerFilter,
    statementFilter: action.statementFilter !== undefined ? action.statementFilter : state.statementFilter,
    statusFilter: action.statusFilter !== undefined ? action.statusFilter : state.statusFilter,
    page: 1,
  })),
  on(ErrorDashboardSetPage, (state, { page }) => ({
    ...state,
    page,
  })),
  on(ErrorDashboardSetLoading, (state, { loading }) => ({
    ...state,
    loading,
  })),
  on(ErrorDashboardSetLoadingDetail, (state, { loadingDetail }) => ({
    ...state,
    loadingDetail,
  })),
  on(ErrorDashboardResetState, () => structuredClone(initialState)),
);
```

- [ ] **Step 2: Commit**

```bash
git add src/app/workspace/reducers/error-dashboard.reducer.ts
git commit -m "feat(error-dashboard): add NgRx reducer with initial state"
```

---

## Task 4: NgRx Selectors

**Files:**
- Create: `src/app/workspace/selectors/error-dashboard-feature.selector.ts`
- Create: `src/app/workspace/selectors/get-error-dashboard-signatures.selector.ts`
- Create: `src/app/workspace/selectors/get-error-dashboard-stats.selector.ts`
- Create: `src/app/workspace/selectors/get-error-dashboard-loading.selector.ts`
- Create: `src/app/workspace/selectors/get-error-dashboard-filters.selector.ts`
- Create: `src/app/workspace/selectors/get-error-dashboard-active-signature.selector.ts`
- Create: `src/app/workspace/selectors/get-error-dashboard-active-detail.selector.ts`
- Create: `src/app/workspace/selectors/get-error-dashboard-history-list.selector.ts`
- Create: `src/app/workspace/selectors/get-error-dashboard-loading-detail.selector.ts`
- Create: `src/app/workspace/selectors/get-error-dashboard-page.selector.ts`
- Create: `src/app/workspace/selectors/get-error-dashboard-total-pages.selector.ts`

- [ ] **Step 1: Create the feature selector**

```typescript
// src/app/workspace/selectors/error-dashboard-feature.selector.ts
import { createFeatureSelector } from '@ngrx/store';
import { IErrorDashboardState } from '../interfaces/error-dashboard-state.interface';

export const errorDashboardFeature = createFeatureSelector<IErrorDashboardState>('errorDashboard');
```

- [ ] **Step 2: Create all child selectors**

Each file follows the same pattern. Create all 10 selector files:

```typescript
// src/app/workspace/selectors/get-error-dashboard-signatures.selector.ts
import { createSelector } from '@ngrx/store';
import { errorDashboardFeature } from './error-dashboard-feature.selector';
import { IErrorSignature } from '../interfaces/error-signature.interface';

export const GetErrorDashboardSignaturesSelector = createSelector(
  errorDashboardFeature,
  (state): IErrorSignature[] => state.signatures,
);
```

```typescript
// src/app/workspace/selectors/get-error-dashboard-stats.selector.ts
import { createSelector } from '@ngrx/store';
import { errorDashboardFeature } from './error-dashboard-feature.selector';
import { IErrorDashboardStats } from '../interfaces/error-dashboard-stats.interface';

export const GetErrorDashboardStatsSelector = createSelector(
  errorDashboardFeature,
  (state): IErrorDashboardStats | null => state.stats,
);
```

```typescript
// src/app/workspace/selectors/get-error-dashboard-loading.selector.ts
import { createSelector } from '@ngrx/store';
import { errorDashboardFeature } from './error-dashboard-feature.selector';

export const GetErrorDashboardLoadingSelector = createSelector(
  errorDashboardFeature,
  (state): boolean => state.loading,
);
```

```typescript
// src/app/workspace/selectors/get-error-dashboard-filters.selector.ts
import { createSelector } from '@ngrx/store';
import { errorDashboardFeature } from './error-dashboard-feature.selector';
import { TErrorSignatureStatus } from '../interfaces/error-signature.interface';
import { TErrorContainerType } from '../interfaces/error-dashboard-stats.interface';

export const GetErrorDashboardFiltersSelector = createSelector(
  errorDashboardFeature,
  (state): {
    textFilter: string;
    containerFilter: TErrorContainerType | null;
    statementFilter: string | null;
    statusFilter: TErrorSignatureStatus | null;
  } => ({
    textFilter: state.textFilter,
    containerFilter: state.containerFilter,
    statementFilter: state.statementFilter,
    statusFilter: state.statusFilter,
  }),
);
```

```typescript
// src/app/workspace/selectors/get-error-dashboard-active-signature.selector.ts
import { createSelector } from '@ngrx/store';
import { errorDashboardFeature } from './error-dashboard-feature.selector';
import { IErrorSignature } from '../interfaces/error-signature.interface';

export const GetErrorDashboardActiveSignatureSelector = createSelector(
  errorDashboardFeature,
  (state): IErrorSignature | null => state.activeSignature,
);
```

```typescript
// src/app/workspace/selectors/get-error-dashboard-active-detail.selector.ts
import { createSelector } from '@ngrx/store';
import { errorDashboardFeature } from './error-dashboard-feature.selector';
import { IErrorHistory } from '../interfaces/error-history.interface';

export const GetErrorDashboardActiveDetailSelector = createSelector(
  errorDashboardFeature,
  (state): IErrorHistory | null => state.activeErrorDetail,
);
```

```typescript
// src/app/workspace/selectors/get-error-dashboard-history-list.selector.ts
import { createSelector } from '@ngrx/store';
import { errorDashboardFeature } from './error-dashboard-feature.selector';
import { IErrorHistory } from '../interfaces/error-history.interface';

export const GetErrorDashboardHistoryListSelector = createSelector(
  errorDashboardFeature,
  (state): IErrorHistory[] => state.errorHistoryList,
);
```

```typescript
// src/app/workspace/selectors/get-error-dashboard-loading-detail.selector.ts
import { createSelector } from '@ngrx/store';
import { errorDashboardFeature } from './error-dashboard-feature.selector';

export const GetErrorDashboardLoadingDetailSelector = createSelector(
  errorDashboardFeature,
  (state): boolean => state.loadingDetail,
);
```

```typescript
// src/app/workspace/selectors/get-error-dashboard-page.selector.ts
import { createSelector } from '@ngrx/store';
import { errorDashboardFeature } from './error-dashboard-feature.selector';

export const GetErrorDashboardPageSelector = createSelector(
  errorDashboardFeature,
  (state): number => state.page,
);
```

```typescript
// src/app/workspace/selectors/get-error-dashboard-total-pages.selector.ts
import { createSelector } from '@ngrx/store';
import { errorDashboardFeature } from './error-dashboard-feature.selector';

export const GetErrorDashboardTotalPagesSelector = createSelector(
  errorDashboardFeature,
  (state): number => state.totalPages,
);
```

- [ ] **Step 3: Commit**

```bash
git add src/app/workspace/selectors/error-dashboard-feature.selector.ts \
  src/app/workspace/selectors/get-error-dashboard-*.selector.ts
git commit -m "feat(error-dashboard): add NgRx feature and child selectors"
```

---

## Task 5: Data Service Extensions

**Files:**
- Modify: `src/app/workspace/services/error-log-data.service.ts`

- [ ] **Step 1: Add `getSignaturesForDashboard` method**

This method extends the existing `getSignatures` with additional filter support for container type and text search. Add after the existing `getSignatures` method:

```typescript
getSignaturesForDashboard(options: {
  workspaceId: number;
  branchId: number;
  page?: number;
  perPage?: number;
  type?: string;
  status?: string;
  search?: string;
  statementFilter?: string;
  refresh?: boolean;
}): Observable<{ items: IErrorSignature[]; totalPages: number }> {
  const params: Record<string, string | number | boolean> = {
    branch_id: options.branchId,
    page: options.page ?? 1,
    per_page: options.perPage ?? 50,
  };
  if (options.type) params['type'] = options.type;
  if (options.status) params['status'] = options.status;
  if (options.search) params['search'] = options.search;
  if (options.statementFilter) params['statement_name'] = options.statementFilter;
  if (options.refresh) params['refresh'] = true;

  return this.userService
    .get(`/workspace/${options.workspaceId}/error_signatures`, params)
    .pipe(
      first(),
      map((response: any) => ({
        items: response.items as IErrorSignature[],
        totalPages: response.total_pages ?? 1,
      })),
    );
}
```

- [ ] **Step 2: Add `getDashboardStats` method**

Add after the new method above:

```typescript
getDashboardStats(options: {
  workspaceId: number;
  branchId: number;
}): Observable<IErrorDashboardStats> {
  return this.userService
    .get(`/workspace/${options.workspaceId}/error_signatures/stats`, {
      branch_id: options.branchId,
      group_by_type: true,
    })
    .pipe(
      first(),
      map((response: any) => ({
        byType: response.by_type ?? [],
      } as IErrorDashboardStats)),
    );
}
```

- [ ] **Step 3: Add import for `IErrorDashboardStats`**

At the top of the file, add:

```typescript
import { IErrorDashboardStats } from '../interfaces/error-dashboard-stats.interface';
```

- [ ] **Step 4: Commit**

```bash
git add src/app/workspace/services/error-log-data.service.ts
git commit -m "feat(error-dashboard): add dashboard stats and filtered signatures service methods"
```

---

## Task 6: NgRx Effects

**Files:**
- Create: `src/app/workspace/effects/error-dashboard.effect.ts`

- [ ] **Step 1: Create the effects class**

```typescript
// src/app/workspace/effects/error-dashboard.effect.ts
import { Injectable, inject } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { switchMap, map, catchError } from 'rxjs/operators';
import { of } from 'rxjs';
import { EErrorDashboardActions } from '../enums/error-dashboard-actions.enum';
import {
  ErrorDashboardLoadSignatures,
  ErrorDashboardLoadStats,
  ErrorDashboardLoadDetail,
  ErrorDashboardLoadHistoryList,
  ErrorDashboardUpdateStatus,
  ErrorDashboardSetSignatures,
  ErrorDashboardSetStats,
  ErrorDashboardSetActiveSignature,
  ErrorDashboardSetActiveDetail,
  ErrorDashboardSetHistoryList,
  ErrorDashboardSetLoading,
  ErrorDashboardSetLoadingDetail,
} from '../actions/error-dashboard.action';
import { ErrorLogDataService } from '../services/error-log-data.service';

@Injectable()
export class ErrorDashboardEffect {
  private actions$ = inject(Actions);
  private errorLogDataService = inject(ErrorLogDataService);

  loadSignatures$ = createEffect(() =>
    this.actions$.pipe(
      ofType(EErrorDashboardActions.ErrorDashboardLoadSignatures),
      switchMap((action: ReturnType<typeof ErrorDashboardLoadSignatures>) => {
        return this.errorLogDataService
          .getSignaturesForDashboard({
            workspaceId: action.workspaceId,
            branchId: action.branchId,
            page: action.page,
            perPage: action.perPage,
            type: action.type,
            status: action.status,
            search: action.search,
            statementFilter: action.statementFilter,
            refresh: action.refresh,
          })
          .pipe(
            switchMap((response) => [
              ErrorDashboardSetSignatures({
                signatures: response.items,
                totalPages: response.totalPages,
              }),
              ErrorDashboardLoadStats({
                workspaceId: action.workspaceId,
                branchId: action.branchId,
              }),
            ]),
            catchError(() =>
              of(ErrorDashboardSetSignatures({ signatures: [], totalPages: 1 })),
            ),
          );
      }),
    ),
  );

  loadStats$ = createEffect(() =>
    this.actions$.pipe(
      ofType(EErrorDashboardActions.ErrorDashboardLoadStats),
      switchMap((action: ReturnType<typeof ErrorDashboardLoadStats>) => {
        return this.errorLogDataService
          .getDashboardStats({
            workspaceId: action.workspaceId,
            branchId: action.branchId,
          })
          .pipe(
            map((stats) => ErrorDashboardSetStats({ stats })),
            catchError(() => of(ErrorDashboardSetStats({ stats: { byType: [] } }))),
          );
      }),
    ),
  );

  loadDetail$ = createEffect(() =>
    this.actions$.pipe(
      ofType(EErrorDashboardActions.ErrorDashboardLoadDetail),
      switchMap((action: ReturnType<typeof ErrorDashboardLoadDetail>) => {
        return this.errorLogDataService
          .getErrorHistory({
            workspaceId: action.workspaceId,
            historyId: action.signatureId,
          })
          .pipe(
            map((detail) => ErrorDashboardSetActiveDetail({ detail })),
            catchError(() => of(ErrorDashboardSetActiveDetail({ detail: null }))),
          );
      }),
    ),
  );

  loadHistoryList$ = createEffect(() =>
    this.actions$.pipe(
      ofType(EErrorDashboardActions.ErrorDashboardLoadHistoryList),
      switchMap((action: ReturnType<typeof ErrorDashboardLoadHistoryList>) => {
        return this.errorLogDataService
          .getErrorHistoryBySignature({
            workspaceId: action.workspaceId,
            signatureId: action.signatureId,
            page: action.page,
            perPage: action.perPage,
          })
          .pipe(
            map((historyList) => ErrorDashboardSetHistoryList({ historyList })),
            catchError(() => of(ErrorDashboardSetHistoryList({ historyList: [] }))),
          );
      }),
    ),
  );

  updateStatus$ = createEffect(() =>
    this.actions$.pipe(
      ofType(EErrorDashboardActions.ErrorDashboardUpdateStatus),
      switchMap((action: ReturnType<typeof ErrorDashboardUpdateStatus>) => {
        return this.errorLogDataService
          .updateSignatureStatus({
            workspaceId: action.workspaceId,
            signatureId: action.signatureId,
            status: action.status,
          })
          .pipe(
            map(() =>
              ErrorDashboardLoadSignatures({
                workspaceId: action.workspaceId,
                branchId: action.branchId,
                refresh: true,
              }),
            ),
            catchError(() => of(ErrorDashboardSetLoading({ loading: false }))),
          );
      }),
    ),
  );
}
```

- [ ] **Step 2: Commit**

```bash
git add src/app/workspace/effects/error-dashboard.effect.ts
git commit -m "feat(error-dashboard): add NgRx effects for signatures, stats, detail, and status"
```

---

## Task 7: Register NgRx in App Module

**Files:**
- Modify: `src/app/app.module.ts`

- [ ] **Step 1: Add imports at top of file**

Add these imports near the existing error-log imports:

```typescript
import { ErrorDashboardEffect } from './workspace/effects/error-dashboard.effect';
import { ErrorDashboardReducer } from './workspace/reducers/error-dashboard.reducer';
```

- [ ] **Step 2: Register the reducer**

In the `StoreModule.forRoot({...})` object, add `errorDashboard: ErrorDashboardReducer` next to the existing `errorLog: ErrorLogReducer` entry.

- [ ] **Step 3: Register the effect**

In the `EffectsModule.forRoot([...])` array, add `ErrorDashboardEffect` next to the existing `ErrorLogEffect` entry.

- [ ] **Step 4: Build to verify no compilation errors**

Run: `ng build --configuration development 2>&1 | head -20`
Expected: Build succeeds (or only pre-existing warnings).

- [ ] **Step 5: Commit**

```bash
git add src/app/app.module.ts
git commit -m "feat(error-dashboard): register NgRx reducer and effect in app module"
```

---

## Task 8: Trend Computation Utility

**Files:**
- Create: `src/app/workspace/utils/trend.util.ts`

- [ ] **Step 1: Create the trend utility**

```typescript
// src/app/workspace/utils/trend.util.ts
import { IErrorSignature } from '../interfaces/error-signature.interface';

export type TErrorTrend = 'new' | 'escalating' | 'shrinking' | 'ongoing';

export interface IErrorTrendResult {
  trend: TErrorTrend;
  slope: number;
}

/**
 * Compute linear regression slope from hourly_counts.
 * Returns the slope `a` in `f(x) = ax + b` fitted to the 24 hourly data points.
 */
export function computeRegressionSlope(hourlyCounts: Record<string, number>): number {
  const entries = Object.entries(hourlyCounts);
  if (entries.length < 2) return 0;

  // Sort by key (hour timestamp) ascending
  entries.sort((a, b) => a[0].localeCompare(b[0]));

  const n = entries.length;
  const values = entries.map(([, count]) => count);

  // Simple linear regression: x = index (0..n-1), y = count
  let sumX = 0;
  let sumY = 0;
  let sumXY = 0;
  let sumX2 = 0;

  for (let i = 0; i < n; i++) {
    sumX += i;
    sumY += values[i];
    sumXY += i * values[i];
    sumX2 += i * i;
  }

  const denominator = n * sumX2 - sumX * sumX;
  if (denominator === 0) return 0;

  return (n * sumXY - sumX * sumY) / denominator;
}

/**
 * Classify an error signature's trend based on its hourly_counts and first_seen_at.
 */
export function classifyTrend(signature: IErrorSignature): IErrorTrendResult {
  const firstSeenAt = new Date(signature.first_seen_at);
  const fourHoursAgo = new Date(Date.now() - 4 * 60 * 60 * 1000);

  // New: first seen within last 4 hours
  if (firstSeenAt > fourHoursAgo) {
    return { trend: 'new', slope: 0 };
  }

  const hourlyCounts = signature.hourly_counts;
  if (!hourlyCounts || Object.keys(hourlyCounts).length === 0) {
    return { trend: 'ongoing', slope: 0 };
  }

  const slope = computeRegressionSlope(hourlyCounts);

  if (slope > 1.2) {
    return { trend: 'escalating', slope };
  }
  if (slope < 0.8) {
    return { trend: 'shrinking', slope };
  }
  return { trend: 'ongoing', slope };
}

/**
 * Get CSS class for trend badge display.
 */
export function getTrendColor(trend: TErrorTrend): string {
  switch (trend) {
    case 'new': return 'text-danger';
    case 'escalating': return 'text-warning';
    case 'shrinking': return 'text-success';
    case 'ongoing': return 'text-muted';
  }
}
```

- [ ] **Step 2: Commit**

```bash
git add src/app/workspace/utils/trend.util.ts
git commit -m "feat(error-dashboard): add linear regression trend computation utility"
```

---

## Task 9: Sparkline Component

**Files:**
- Create: `src/app/workspace/components/error-sparkline/error-sparkline.component.ts`
- Create: `src/app/workspace/components/error-sparkline/error-sparkline.component.html`
- Create: `src/app/workspace/components/error-sparkline/error-sparkline.component.scss`

- [ ] **Step 1: Create the component class**

```typescript
// src/app/workspace/components/error-sparkline/error-sparkline.component.ts
import { Component, ChangeDetectionStrategy, input, computed } from '@angular/core';

export interface ISparklineBar {
  height: number;   // percentage 0-100
  isRecent: boolean; // true for last 6 hours
}

@Component({
  selector: 'app-error-sparkline',
  standalone: true,
  templateUrl: './error-sparkline.component.html',
  styleUrls: ['./error-sparkline.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ErrorSparklineComponent {
  hourlyCounts = input.required<Record<string, number> | undefined>();

  bars = computed<ISparklineBar[]>(() => {
    const counts = this.hourlyCounts();
    if (!counts) return [];

    const entries = Object.entries(counts);
    entries.sort((a, b) => a[0].localeCompare(b[0]));

    const values = entries.map(([, count]) => count);
    const max = Math.max(...values, 1);
    const totalBars = values.length;

    return values.map((value, index) => ({
      height: (value / max) * 100,
      isRecent: index >= totalBars - 6 && value > 0,
    }));
  });
}
```

- [ ] **Step 2: Create the template**

```html
<!-- src/app/workspace/components/error-sparkline/error-sparkline.component.html -->
<div class="sparkline" [attr.data-pw]="'error-sparkline'">
  @for (bar of bars(); track $index) {
    <div
      class="sparkline-bar"
      [class.sparkline-bar--recent]="bar.isRecent"
      [style.height.%]="bar.height"
    ></div>
  }
</div>
```

- [ ] **Step 3: Create the styles**

```scss
// src/app/workspace/components/error-sparkline/error-sparkline.component.scss
.sparkline {
  display: flex;
  align-items: flex-end;
  gap: 1px;
  height: 20px;
  min-width: 60px;
}

.sparkline-bar {
  flex: 1;
  min-width: 2px;
  max-width: 4px;
  background-color: var(--bs-secondary);
  border-radius: 1px 1px 0 0;
  min-height: 1px;

  &--recent {
    background-color: var(--bs-danger);
  }
}
```

- [ ] **Step 4: Commit**

```bash
git add src/app/workspace/components/error-sparkline/
git commit -m "feat(error-dashboard): add reusable sparkline bar chart component"
```

---

## Task 10: Summary Cards Component

**Files:**
- Create: `src/app/workspace/components/error-summary-cards/error-summary-cards.component.ts`
- Create: `src/app/workspace/components/error-summary-cards/error-summary-cards.component.html`
- Create: `src/app/workspace/components/error-summary-cards/error-summary-cards.component.scss`

- [ ] **Step 1: Create the component class**

```typescript
// src/app/workspace/components/error-summary-cards/error-summary-cards.component.ts
import { Component, ChangeDetectionStrategy, input, output } from '@angular/core';
import { IErrorDashboardStats, TErrorContainerType } from '../../interfaces/error-dashboard-stats.interface';

@Component({
  selector: 'app-error-summary-cards',
  standalone: true,
  templateUrl: './error-summary-cards.component.html',
  styleUrls: ['./error-summary-cards.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ErrorSummaryCardsComponent {
  stats = input.required<IErrorDashboardStats | null>();
  activeFilter = input.required<TErrorContainerType | null>();
  containerSelected = output<TErrorContainerType | null>();

  readonly containerTypes: { type: TErrorContainerType; label: string }[] = [
    { type: 'task', label: 'Task' },
    { type: 'query', label: 'API' },
    { type: 'function', label: 'Function' },
    { type: 'middleware', label: 'Middleware' },
  ];

  getStatForType(type: TErrorContainerType): { total: number; gain24h: number } {
    const stat = this.stats()?.byType.find((s) => s.type === type);
    return stat ?? { total: 0, gain24h: 0 };
  }

  toggleFilter(type: TErrorContainerType): void {
    if (this.activeFilter() === type) {
      this.containerSelected.emit(null);
    } else {
      this.containerSelected.emit(type);
    }
  }
}
```

- [ ] **Step 2: Create the template**

```html
<!-- src/app/workspace/components/error-summary-cards/error-summary-cards.component.html -->
<div class="summary-cards d-flex gap-3" [attr.data-pw]="'error-summary-cards'">
  @for (ct of containerTypes; track ct.type) {
    @let stat = getStatForType(ct.type);
    <div
      class="summary-card flex-fill text-center p-3"
      [class.summary-card--active]="activeFilter() === ct.type"
      [attr.data-pw]="'summary-card-' + ct.type"
      (click)="toggleFilter(ct.type)"
    >
      <div class="summary-card__gain" [class.text-danger]="stat.gain24h > 0">
        @if (stat.gain24h > 0) {
          +{{ stat.gain24h }}
        } @else {
          {{ stat.gain24h }}
        }
      </div>
      <div class="summary-card__total text-muted">{{ stat.total | number }}</div>
      <div class="summary-card__label text-muted">{{ ct.label }}</div>
    </div>
  }
</div>
```

- [ ] **Step 3: Add `DecimalPipe` import**

Update the component's imports array:

```typescript
import { DecimalPipe } from '@angular/common';

// In @Component decorator:
imports: [DecimalPipe],
```

- [ ] **Step 4: Create the styles**

```scss
// src/app/workspace/components/error-summary-cards/error-summary-cards.component.scss
.summary-card {
  border: 1px solid var(--bs-border-color);
  border-radius: 8px;
  cursor: pointer;
  transition: border-color 0.15s;

  &:hover {
    border-color: var(--bs-primary);
  }

  &--active {
    border-color: var(--bs-primary);
    border-width: 2px;
    background-color: rgba(var(--bs-primary-rgb), 0.05);
  }

  &__gain {
    font-size: 1.4em;
    font-weight: bold;
  }

  &__total {
    font-size: 0.85em;
  }

  &__label {
    font-size: 0.8em;
  }
}
```

- [ ] **Step 5: Commit**

```bash
git add src/app/workspace/components/error-summary-cards/
git commit -m "feat(error-dashboard): add container summary cards component"
```

---

## Task 11: Error Histogram Component

**Files:**
- Create: `src/app/workspace/components/error-histogram/error-histogram.component.ts`
- Create: `src/app/workspace/components/error-histogram/error-histogram.component.html`
- Create: `src/app/workspace/components/error-histogram/error-histogram.component.scss`

- [ ] **Step 1: Create the component class**

```typescript
// src/app/workspace/components/error-histogram/error-histogram.component.ts
import { Component, ChangeDetectionStrategy, input, computed } from '@angular/core';
import { IErrorTrendResult, classifyTrend, getTrendColor } from '../../utils/trend.util';
import { IErrorSignature } from '../../interfaces/error-signature.interface';

export interface IHistogramBar {
  hour: string;
  count: number;
  heightPercent: number;
  isMax: boolean;
}

@Component({
  selector: 'app-error-histogram',
  standalone: true,
  templateUrl: './error-histogram.component.html',
  styleUrls: ['./error-histogram.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ErrorHistogramComponent {
  signature = input.required<IErrorSignature>();

  bars = computed<IHistogramBar[]>(() => {
    const counts = this.signature().hourly_counts;
    if (!counts) return [];

    const entries = Object.entries(counts);
    entries.sort((a, b) => a[0].localeCompare(b[0]));

    const values = entries.map(([, count]) => count);
    const max = Math.max(...values, 1);

    return entries.map(([hour, count]) => ({
      hour,
      count,
      heightPercent: (count / max) * 100,
      isMax: count === max && count > 0,
    }));
  });

  maxCount = computed<number>(() => {
    const counts = this.signature().hourly_counts;
    if (!counts) return 0;
    return Math.max(...Object.values(counts), 0);
  });

  trendResult = computed<IErrorTrendResult>(() => classifyTrend(this.signature()));

  trendColorClass = computed<string>(() => getTrendColor(this.trendResult().trend));

  trendLabel = computed<string>(() => {
    const result = this.trendResult();
    switch (result.trend) {
      case 'new': return 'new — appeared recently';
      case 'escalating': return `escalating — rate increasing by ${result.slope.toFixed(1)}x`;
      case 'shrinking': return `shrinking — rate decreasing`;
      case 'ongoing': return 'ongoing — steady rate';
    }
  });
}
```

- [ ] **Step 2: Create the template**

```html
<!-- src/app/workspace/components/error-histogram/error-histogram.component.html -->
<div class="histogram" [attr.data-pw]="'error-histogram'">
  <div class="d-flex justify-content-between align-items-center mb-2">
    <h6 class="mb-0">Occurrences — Past 24h</h6>
    <span class="text-danger small">max: {{ maxCount() }}</span>
  </div>

  <div class="histogram__bars">
    @for (bar of bars(); track bar.hour) {
      <div
        class="histogram__bar"
        [class.histogram__bar--max]="bar.isMax"
        [style.height.%]="bar.heightPercent"
        [title]="bar.hour + ': ' + bar.count + ' errors'"
      ></div>
    }
  </div>

  <div class="d-flex justify-content-between text-muted mt-1" style="font-size: 0.7em;">
    <span>24h ago</span>
    <span>12h</span>
    <span>now</span>
  </div>

  <div class="mt-1 pt-1 border-top small">
    <span [class]="trendColorClass()">▲ {{ trendLabel() }}</span>
  </div>
</div>
```

- [ ] **Step 3: Create the styles**

```scss
// src/app/workspace/components/error-histogram/error-histogram.component.scss
.histogram {
  &__bars {
    display: flex;
    align-items: flex-end;
    gap: 2px;
    height: 80px;
    border-bottom: 1px solid var(--bs-border-color);
  }

  &__bar {
    flex: 1;
    min-width: 2px;
    background-color: var(--bs-secondary);
    border-radius: 2px 2px 0 0;
    min-height: 1px;
    transition: background-color 0.15s;

    &--max {
      background-color: var(--bs-danger);
    }
  }
}
```

- [ ] **Step 4: Commit**

```bash
git add src/app/workspace/components/error-histogram/
git commit -m "feat(error-dashboard): add 24h error histogram component"
```

---

## Task 12: Performance Panel Component

**Files:**
- Create: `src/app/workspace/components/error-performance-panel/error-performance-panel.component.ts`
- Create: `src/app/workspace/components/error-performance-panel/error-performance-panel.component.html`
- Create: `src/app/workspace/components/error-performance-panel/error-performance-panel.component.scss`

- [ ] **Step 1: Create the component class**

```typescript
// src/app/workspace/components/error-performance-panel/error-performance-panel.component.ts
import { Component, ChangeDetectionStrategy, input, computed } from '@angular/core';
import { IErrorHistory } from '../../interfaces/error-history.interface';

export interface IPerformanceRow {
  label: string;
  calls: number;
  responseMs: number;
  isError: boolean;
}

@Component({
  selector: 'app-error-performance-panel',
  standalone: true,
  templateUrl: './error-performance-panel.component.html',
  styleUrls: ['./error-performance-panel.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ErrorPerformancePanelComponent {
  errorHistoryList = input.required<IErrorHistory[]>();
  totalCount = input<number>(0);

  rows = computed<IPerformanceRow[]>(() => {
    const list = this.errorHistoryList();
    if (list.length === 0) return [];

    const errorCount = list.length;
    const errorAvgDuration = list.reduce((sum, h) => sum + (h.duration ?? 0), 0) / errorCount;

    return [
      { label: 'error', calls: errorCount, responseMs: Math.round(errorAvgDuration), isError: true },
      { label: 'standard', calls: this.totalCount() - errorCount, responseMs: 0, isError: false },
    ];
  });

  isHighResponseTime = computed<boolean>(() => {
    const errorRow = this.rows().find((r) => r.isError);
    return !!errorRow && errorRow.responseMs > 200;
  });
}
```

- [ ] **Step 2: Create the template**

```html
<!-- src/app/workspace/components/error-performance-panel/error-performance-panel.component.html -->
<div class="performance-panel" [attr.data-pw]="'error-performance-panel'">
  <h6 class="mb-3">Performance</h6>

  @if (rows().length > 0) {
    <table class="table table-sm table-borderless mb-0">
      <thead>
        <tr>
          <th class="text-muted"></th>
          <th class="text-muted text-end">calls</th>
          <th class="text-muted text-end">response</th>
        </tr>
      </thead>
      <tbody>
        @for (row of rows(); track row.label) {
          <tr>
            <td [class.text-danger]="row.isError">{{ row.label }}</td>
            <td class="text-end">{{ row.calls }}</td>
            <td class="text-end" [class.text-danger]="row.isError && isHighResponseTime()">
              {{ row.responseMs }}ms
            </td>
          </tr>
        }
      </tbody>
    </table>
  } @else {
    <p class="text-muted small">No performance data available</p>
  }
</div>
```

- [ ] **Step 3: Create the styles**

```scss
// src/app/workspace/components/error-performance-panel/error-performance-panel.component.scss
.performance-panel {
  .table {
    font-size: 0.85em;

    th {
      font-weight: normal;
      font-size: 0.85em;
    }
  }
}
```

- [ ] **Step 4: Commit**

```bash
git add src/app/workspace/components/error-performance-panel/
git commit -m "feat(error-dashboard): add error performance comparison panel"
```

---

## Task 13: Stack Issues Panel Component

**Files:**
- Create: `src/app/workspace/components/error-stack-issues-panel/error-stack-issues-panel.component.ts`
- Create: `src/app/workspace/components/error-stack-issues-panel/error-stack-issues-panel.component.html`
- Create: `src/app/workspace/components/error-stack-issues-panel/error-stack-issues-panel.component.scss`

- [ ] **Step 1: Create the component class**

```typescript
// src/app/workspace/components/error-stack-issues-panel/error-stack-issues-panel.component.ts
import { Component, ChangeDetectionStrategy, input, output } from '@angular/core';
import { IErrorSignature } from '../../interfaces/error-signature.interface';

@Component({
  selector: 'app-error-stack-issues-panel',
  standalone: true,
  templateUrl: './error-stack-issues-panel.component.html',
  styleUrls: ['./error-stack-issues-panel.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ErrorStackIssuesPanelComponent {
  /** Other error signatures sharing the same obj (function/API/task) */
  stackIssues = input.required<IErrorSignature[]>();
  issueClicked = output<IErrorSignature>();

  onIssueClick(sig: IErrorSignature): void {
    this.issueClicked.emit(sig);
  }
}
```

- [ ] **Step 2: Create the template**

```html
<!-- src/app/workspace/components/error-stack-issues-panel/error-stack-issues-panel.component.html -->
<div class="stack-issues-panel" [attr.data-pw]="'error-stack-issues-panel'">
  <h6 class="mb-3">Stack Issues</h6>

  @if (stackIssues().length > 0) {
    @for (issue of stackIssues(); track issue.id) {
      <div
        class="stack-issue p-2 mb-2 rounded"
        [attr.data-pw]="'stack-issue-' + issue.id"
        (click)="onIssueClick(issue)"
      >
        <div class="text-warning small fw-bold">{{ issue.error_code }}</div>
        <div class="text-muted" style="font-size: 0.8em;">
          in {{ issue.statement_name }} — same stack
        </div>
      </div>
    }
  } @else {
    <p class="text-muted small">No other issues in this stack</p>
  }
</div>
```

- [ ] **Step 3: Create the styles**

```scss
// src/app/workspace/components/error-stack-issues-panel/error-stack-issues-panel.component.scss
.stack-issue {
  background-color: rgba(var(--bs-dark-rgb), 0.3);
  cursor: pointer;
  transition: background-color 0.15s;

  &:hover {
    background-color: rgba(var(--bs-light-rgb), 0.05);
  }
}
```

- [ ] **Step 4: Commit**

```bash
git add src/app/workspace/components/error-stack-issues-panel/
git commit -m "feat(error-dashboard): add stack issues sidebar panel"
```

---

## Task 14: Error Dashboard Listing Page

**Files:**
- Create: `src/app/workspace/pages/workspace-error-dashboard-page/workspace-error-dashboard-page.component.ts`
- Create: `src/app/workspace/pages/workspace-error-dashboard-page/workspace-error-dashboard-page.component.html`
- Create: `src/app/workspace/pages/workspace-error-dashboard-page/workspace-error-dashboard-page.component.scss`

- [ ] **Step 1: Create the component class**

```typescript
// src/app/workspace/pages/workspace-error-dashboard-page/workspace-error-dashboard-page.component.ts
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, inject, signal, computed, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { FormsModule } from '@angular/forms';
import { DecimalPipe } from '@angular/common';

import { ErrorSparklineComponent } from '../../components/error-sparkline/error-sparkline.component';
import { ErrorSummaryCardsComponent } from '../../components/error-summary-cards/error-summary-cards.component';

import { ErrorDashboardLoadSignatures, ErrorDashboardSetFilters, ErrorDashboardSetPage, ErrorDashboardResetState } from '../../actions/error-dashboard.action';
import { GetErrorDashboardSignaturesSelector } from '../../selectors/get-error-dashboard-signatures.selector';
import { GetErrorDashboardStatsSelector } from '../../selectors/get-error-dashboard-stats.selector';
import { GetErrorDashboardLoadingSelector } from '../../selectors/get-error-dashboard-loading.selector';
import { GetErrorDashboardFiltersSelector } from '../../selectors/get-error-dashboard-filters.selector';
import { GetErrorDashboardPageSelector } from '../../selectors/get-error-dashboard-page.selector';
import { GetErrorDashboardTotalPagesSelector } from '../../selectors/get-error-dashboard-total-pages.selector';

import { IErrorSignature, TErrorSignatureStatus } from '../../interfaces/error-signature.interface';
import { IErrorDashboardStats, TErrorContainerType } from '../../interfaces/error-dashboard-stats.interface';
import { classifyTrend, getTrendColor, TErrorTrend } from '../../utils/trend.util';

import { GetWorkspaceSelector } from '../../selectors/get-workspace.selector';
import { GetBranchSelector } from '../../selectors/get-branch.selector';

import { LuxonModule } from 'luxon-angular';

@Component({
  selector: 'app-workspace-error-dashboard-page',
  standalone: true,
  imports: [
    FormsModule,
    DecimalPipe,
    ErrorSparklineComponent,
    ErrorSummaryCardsComponent,
    LuxonModule,
  ],
  templateUrl: './workspace-error-dashboard-page.component.html',
  styleUrls: ['./workspace-error-dashboard-page.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class WorkspaceErrorDashboardPageComponent implements OnInit, OnDestroy {
  private store = inject(Store);
  private router = inject(Router);
  private destroyRef = inject(DestroyRef);

  // Store state
  signatures = signal<IErrorSignature[]>([]);
  stats = signal<IErrorDashboardStats | null>(null);
  loading = signal<boolean>(false);
  page = signal<number>(1);
  totalPages = signal<number>(1);
  filters = signal<{
    textFilter: string;
    containerFilter: TErrorContainerType | null;
    statementFilter: string | null;
    statusFilter: TErrorSignatureStatus | null;
  }>({ textFilter: '', containerFilter: null, statementFilter: null, statusFilter: null });

  // Workspace context
  private workspaceId = 0;
  private branchId = 0;

  // Local filter input (debounced)
  searchText = '';

  // Unique statement names for dropdown
  statementNames = computed<string[]>(() => {
    const sigs = this.signatures();
    const names = new Set(sigs.map((s) => s.statement_name).filter(Boolean));
    return [...names].sort();
  });

  ngOnInit(): void {
    this.store.select(GetWorkspaceSelector).pipe(takeUntilDestroyed(this.destroyRef)).subscribe((ws) => {
      if (ws) this.workspaceId = ws.id;
    });

    this.store.select(GetBranchSelector).pipe(takeUntilDestroyed(this.destroyRef)).subscribe((branch) => {
      if (branch) {
        this.branchId = branch.id;
        this.loadSignatures();
      }
    });

    this.store.select(GetErrorDashboardSignaturesSelector).pipe(takeUntilDestroyed(this.destroyRef)).subscribe((sigs) => this.signatures.set(sigs));
    this.store.select(GetErrorDashboardStatsSelector).pipe(takeUntilDestroyed(this.destroyRef)).subscribe((stats) => this.stats.set(stats));
    this.store.select(GetErrorDashboardLoadingSelector).pipe(takeUntilDestroyed(this.destroyRef)).subscribe((loading) => this.loading.set(loading));
    this.store.select(GetErrorDashboardPageSelector).pipe(takeUntilDestroyed(this.destroyRef)).subscribe((page) => this.page.set(page));
    this.store.select(GetErrorDashboardTotalPagesSelector).pipe(takeUntilDestroyed(this.destroyRef)).subscribe((tp) => this.totalPages.set(tp));
    this.store.select(GetErrorDashboardFiltersSelector).pipe(takeUntilDestroyed(this.destroyRef)).subscribe((f) => this.filters.set(f));
  }

  ngOnDestroy(): void {
    this.store.dispatch(ErrorDashboardResetState());
  }

  loadSignatures(): void {
    const f = this.filters();
    this.store.dispatch(ErrorDashboardLoadSignatures({
      workspaceId: this.workspaceId,
      branchId: this.branchId,
      page: this.page(),
      perPage: 50,
      type: f.containerFilter ?? undefined,
      status: f.statusFilter ?? undefined,
      search: f.textFilter || undefined,
      statementFilter: f.statementFilter ?? undefined,
    }));
  }

  onSearchChange(text: string): void {
    this.store.dispatch(ErrorDashboardSetFilters({ textFilter: text }));
    this.loadSignatures();
  }

  onContainerFilter(type: TErrorContainerType | null): void {
    this.store.dispatch(ErrorDashboardSetFilters({ containerFilter: type }));
    this.loadSignatures();
  }

  onStatusFilter(status: TErrorSignatureStatus | null): void {
    this.store.dispatch(ErrorDashboardSetFilters({ statusFilter: status }));
    this.loadSignatures();
  }

  onStatementFilter(statement: string | null): void {
    this.store.dispatch(ErrorDashboardSetFilters({ statementFilter: statement }));
    this.loadSignatures();
  }

  onPageChange(page: number): void {
    this.store.dispatch(ErrorDashboardSetPage({ page }));
    this.loadSignatures();
  }

  navigateToDetail(signature: IErrorSignature): void {
    this.router.navigate(['/workspace', this.workspaceId, 'error-dashboard', signature.id]);
  }

  getTrend(signature: IErrorSignature): TErrorTrend {
    return classifyTrend(signature).trend;
  }

  getTrendColorClass(trend: TErrorTrend): string {
    return getTrendColor(trend);
  }
}
```

- [ ] **Step 2: Create the template**

```html
<!-- src/app/workspace/pages/workspace-error-dashboard-page/workspace-error-dashboard-page.component.html -->
<div class="error-dashboard-page p-4" [attr.data-pw]="'error-dashboard-page'">
  <h4 class="mb-4">Errors</h4>

  <!-- Filter Bar -->
  <div class="filter-bar d-flex gap-2 mb-4 flex-wrap align-items-center">
    <input
      type="text"
      class="form-control form-control-sm"
      placeholder="Filter by error message, code, stack..."
      [(ngModel)]="searchText"
      (ngModelChange)="onSearchChange($event)"
      [attr.data-pw]="'error-filter-search'"
      style="max-width: 300px;"
    >

    <select
      class="form-select form-select-sm"
      style="max-width: 160px;"
      [ngModel]="filters().containerFilter ?? ''"
      (ngModelChange)="onContainerFilter($event || null)"
      [attr.data-pw]="'error-filter-container'"
    >
      <option value="">All Containers</option>
      <option value="query">API</option>
      <option value="function">Function</option>
      <option value="task">Task</option>
      <option value="middleware">Middleware</option>
      <option value="trigger">Trigger</option>
    </select>

    <select
      class="form-select form-select-sm"
      style="max-width: 180px;"
      [ngModel]="filters().statementFilter ?? ''"
      (ngModelChange)="onStatementFilter($event || null)"
      [attr.data-pw]="'error-filter-statement'"
    >
      <option value="">All Statements</option>
      @for (name of statementNames(); track name) {
        <option [value]="name">{{ name }}</option>
      }
    </select>

    <select
      class="form-select form-select-sm"
      style="max-width: 130px;"
      [ngModel]="filters().statusFilter ?? ''"
      (ngModelChange)="onStatusFilter($event || null)"
      [attr.data-pw]="'error-filter-status'"
    >
      <option value="">All Statuses</option>
      <option value="new">New</option>
      <option value="ignored">Ignored</option>
      <option value="fixed">Fixed</option>
    </select>
  </div>

  <!-- Summary Cards -->
  <app-error-summary-cards
    [stats]="stats()"
    [activeFilter]="filters().containerFilter"
    (containerSelected)="onContainerFilter($event)"
    class="d-block mb-4"
  />

  <!-- Error Table -->
  <div class="table-responsive">
    <table class="table table-hover align-middle" [attr.data-pw]="'error-table'">
      <thead>
        <tr>
          <th class="text-muted" style="width: 120px;">past 24h</th>
          <th class="text-muted">error</th>
          <th class="text-muted">stack</th>
          <th class="text-muted" style="width: 100px;">last seen</th>
          <th class="text-muted" style="width: 100px;">first seen</th>
          <th class="text-muted" style="width: 90px;">trend</th>
        </tr>
      </thead>
      <tbody>
        @if (loading()) {
          <tr>
            <td colspan="6" class="text-center text-muted py-4">Loading...</td>
          </tr>
        } @else if (signatures().length === 0) {
          <tr>
            <td colspan="6" class="text-center text-muted py-4">No errors found</td>
          </tr>
        } @else {
          @for (sig of signatures(); track sig.id) {
            @let trend = getTrend(sig);
            <tr
              class="error-row"
              [attr.data-pw]="'error-row-' + sig.id"
              (click)="navigateToDetail(sig)"
            >
              <td>
                <app-error-sparkline [hourlyCounts]="sig.hourly_counts" />
              </td>
              <td>
                <div class="fw-bold">{{ sig.error_code }}</div>
                @if (sig.error_msg) {
                  <div class="text-muted small text-truncate" style="max-width: 300px;">{{ sig.error_msg }}</div>
                }
              </td>
              <td class="text-muted">{{ sig.statement_name || '...' }}</td>
              <td class="text-muted small">{{ sig.last_seen_at }}</td>
              <td class="text-muted small">{{ sig.first_seen_at }}</td>
              <td>
                <span class="small" [class]="getTrendColorClass(trend)">{{ trend }}</span>
              </td>
            </tr>
          }
        }
      </tbody>
    </table>
  </div>

  <!-- Pagination -->
  @if (totalPages() > 1) {
    <div class="d-flex justify-content-center gap-2 mt-3">
      <button
        class="btn btn-sm btn-outline-secondary"
        [disabled]="page() <= 1"
        (click)="onPageChange(page() - 1)"
      >Previous</button>
      <span class="align-self-center text-muted small">
        Page {{ page() }} of {{ totalPages() }}
      </span>
      <button
        class="btn btn-sm btn-outline-secondary"
        [disabled]="page() >= totalPages()"
        (click)="onPageChange(page() + 1)"
      >Next</button>
    </div>
  }
</div>
```

- [ ] **Step 3: Create the styles**

```scss
// src/app/workspace/pages/workspace-error-dashboard-page/workspace-error-dashboard-page.component.scss
.error-dashboard-page {
  max-width: 1200px;
}

.error-row {
  cursor: pointer;
}

.table {
  th {
    font-weight: normal;
    font-size: 0.85em;
    border-bottom-width: 1px;
  }
}
```

- [ ] **Step 4: Commit**

```bash
git add src/app/workspace/pages/workspace-error-dashboard-page/
git commit -m "feat(error-dashboard): add error listing page with filters, cards, and sparkline table"
```

---

## Task 15: Error Detail Page

**Files:**
- Create: `src/app/workspace/pages/workspace-error-detail-page/workspace-error-detail-page.component.ts`
- Create: `src/app/workspace/pages/workspace-error-detail-page/workspace-error-detail-page.component.html`
- Create: `src/app/workspace/pages/workspace-error-detail-page/workspace-error-detail-page.component.scss`

- [ ] **Step 1: Create the component class**

```typescript
// src/app/workspace/pages/workspace-error-detail-page/workspace-error-detail-page.component.ts
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, inject, signal, computed, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { Store } from '@ngrx/store';

import { ErrorHistogramComponent } from '../../components/error-histogram/error-histogram.component';
import { ErrorPerformancePanelComponent } from '../../components/error-performance-panel/error-performance-panel.component';
import { ErrorStackIssuesPanelComponent } from '../../components/error-stack-issues-panel/error-stack-issues-panel.component';

import {
  ErrorDashboardLoadSignatures,
  ErrorDashboardSetActiveSignature,
  ErrorDashboardLoadHistoryList,
  ErrorDashboardUpdateStatus,
  ErrorDashboardResetState,
} from '../../actions/error-dashboard.action';

import { GetErrorDashboardActiveSignatureSelector } from '../../selectors/get-error-dashboard-active-signature.selector';
import { GetErrorDashboardHistoryListSelector } from '../../selectors/get-error-dashboard-history-list.selector';
import { GetErrorDashboardSignaturesSelector } from '../../selectors/get-error-dashboard-signatures.selector';
import { GetErrorDashboardLoadingDetailSelector } from '../../selectors/get-error-dashboard-loading-detail.selector';
import { GetWorkspaceSelector } from '../../selectors/get-workspace.selector';
import { GetBranchSelector } from '../../selectors/get-branch.selector';

import { IErrorSignature, TErrorSignatureStatus } from '../../interfaces/error-signature.interface';
import { IErrorHistory } from '../../interfaces/error-history.interface';

import { ErrorLogDataService } from '../../services/error-log-data.service';
import { first } from 'rxjs/operators';

@Component({
  selector: 'app-workspace-error-detail-page',
  standalone: true,
  imports: [
    RouterLink,
    ErrorHistogramComponent,
    ErrorPerformancePanelComponent,
    ErrorStackIssuesPanelComponent,
  ],
  templateUrl: './workspace-error-detail-page.component.html',
  styleUrls: ['./workspace-error-detail-page.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class WorkspaceErrorDetailPageComponent implements OnInit, OnDestroy {
  private store = inject(Store);
  private route = inject(ActivatedRoute);
  private router = inject(Router);
  private destroyRef = inject(DestroyRef);
  private errorLogDataService = inject(ErrorLogDataService);

  signature = signal<IErrorSignature | null>(null);
  historyList = signal<IErrorHistory[]>([]);
  stackIssues = signal<IErrorSignature[]>([]);
  loadingDetail = signal<boolean>(false);

  private workspaceId = 0;
  private branchId = 0;

  containerLabel = computed<string>(() => {
    const type = this.signature()?.type;
    switch (type) {
      case 'query': return 'API';
      case 'function': return 'Function';
      case 'task': return 'Task';
      case 'middleware': return 'Middleware';
      case 'trigger': return 'Trigger';
      default: return '';
    }
  });

  ngOnInit(): void {
    this.store.select(GetWorkspaceSelector).pipe(takeUntilDestroyed(this.destroyRef)).subscribe((ws) => {
      if (ws) this.workspaceId = ws.id;
    });

    this.store.select(GetBranchSelector).pipe(takeUntilDestroyed(this.destroyRef)).subscribe((branch) => {
      if (branch) this.branchId = branch.id;
    });

    this.store.select(GetErrorDashboardActiveSignatureSelector).pipe(takeUntilDestroyed(this.destroyRef)).subscribe((sig) => this.signature.set(sig));
    this.store.select(GetErrorDashboardHistoryListSelector).pipe(takeUntilDestroyed(this.destroyRef)).subscribe((list) => this.historyList.set(list));
    this.store.select(GetErrorDashboardLoadingDetailSelector).pipe(takeUntilDestroyed(this.destroyRef)).subscribe((loading) => this.loadingDetail.set(loading));

    // Load the signature data
    const signatureId = Number(this.route.snapshot.paramMap.get('signatureId'));
    if (signatureId) {
      this.loadSignatureDetail(signatureId);
    }
  }

  ngOnDestroy(): void {
    this.store.dispatch(ErrorDashboardSetActiveSignature({ signature: null }));
  }

  private loadSignatureDetail(signatureId: number): void {
    // Load the signature from the existing list or fetch fresh
    this.errorLogDataService
      .getSignaturesForDashboard({
        workspaceId: this.workspaceId,
        branchId: this.branchId,
        perPage: 1,
        search: undefined,
      })
      .pipe(first())
      .subscribe(); // Will be populated via effect

    // Load the signatures list to find our target and stack issues
    this.store.select(GetErrorDashboardSignaturesSelector).pipe(takeUntilDestroyed(this.destroyRef)).subscribe((sigs) => {
      const target = sigs.find((s) => s.id === signatureId);
      if (target) {
        this.store.dispatch(ErrorDashboardSetActiveSignature({ signature: target }));

        // Stack issues: other signatures sharing the same obj
        const issues = sigs.filter((s) => s.id !== signatureId && s.obj.id === target.obj.id && s.type === target.type);
        this.stackIssues.set(issues);
      }
    });

    // Load error history for this signature
    this.store.dispatch(ErrorDashboardLoadHistoryList({
      workspaceId: this.workspaceId,
      signatureId,
    }));

    // Also load all signatures for this obj to populate stack issues
    this.store.dispatch(ErrorDashboardLoadSignatures({
      workspaceId: this.workspaceId,
      branchId: this.branchId,
    }));
  }

  updateStatus(status: TErrorSignatureStatus): void {
    const sig = this.signature();
    if (!sig) return;
    this.store.dispatch(ErrorDashboardUpdateStatus({
      workspaceId: this.workspaceId,
      branchId: this.branchId,
      signatureId: sig.id,
      status,
    }));
  }

  navigateToIssue(issue: IErrorSignature): void {
    this.router.navigate(['/workspace', this.workspaceId, 'error-dashboard', issue.id]);
  }

  navigateBack(): void {
    this.router.navigate(['/workspace', this.workspaceId, 'error-dashboard']);
  }
}
```

- [ ] **Step 2: Create the template**

```html
<!-- src/app/workspace/pages/workspace-error-detail-page/workspace-error-detail-page.component.html -->
<div class="error-detail-page p-4" [attr.data-pw]="'error-detail-page'">
  @if (signature(); as sig) {
    <!-- Breadcrumb -->
    <div class="mb-3">
      <a class="text-muted small" style="cursor: pointer;" (click)="navigateBack()">
        ← Error Dashboard
      </a>
    </div>

    <div class="d-flex gap-4">
      <!-- Left Column: Main Content -->
      <div class="flex-grow-1" style="flex: 2;">

        <!-- Error Header -->
        <div class="mb-4 p-3 rounded" style="background: rgba(var(--bs-dark-rgb), 0.3);">
          <div class="d-flex justify-content-between align-items-start">
            <div>
              <div class="d-flex align-items-center gap-2 mb-1">
                <span class="text-danger fw-bold fs-5">{{ sig.error_code }}</span>
                <span class="badge" [class.bg-danger]="sig.status === 'new'" [class.bg-secondary]="sig.status === 'ignored'" [class.bg-success]="sig.status === 'fixed'">
                  {{ sig.status }}
                </span>
                <span class="badge bg-secondary">{{ containerLabel() }}</span>
              </div>
              @if (sig.error_msg) {
                <div class="text-muted">{{ sig.error_msg }}</div>
              }
              <div class="text-muted small mt-1">
                {{ sig.type === 'query' ? 'Endpoint' : 'Function' }}: <span class="text-primary">{{ sig.statement_name }}</span>
              </div>
            </div>
            <div class="d-flex gap-2">
              @if (sig.status !== 'ignored') {
                <button class="btn btn-sm btn-outline-secondary" (click)="updateStatus('ignored')" [attr.data-pw]="'btn-mark-ignored'">
                  Mark Ignored
                </button>
              }
              @if (sig.status !== 'fixed') {
                <button class="btn btn-sm btn-outline-success" (click)="updateStatus('fixed')" [attr.data-pw]="'btn-mark-fixed'">
                  Mark Fixed
                </button>
              }
              @if (sig.status !== 'new') {
                <button class="btn btn-sm btn-outline-danger" (click)="updateStatus('new')" [attr.data-pw]="'btn-mark-new'">
                  Reopen
                </button>
              }
            </div>
          </div>
        </div>

        <!-- 24h Histogram -->
        <div class="mb-4 p-3 rounded" style="background: rgba(var(--bs-dark-rgb), 0.3);">
          <app-error-histogram [signature]="sig" />
        </div>

        <!-- Statement Link -->
        <div class="mb-4 p-3 rounded d-flex justify-content-between align-items-center" style="background: rgba(var(--bs-dark-rgb), 0.3);">
          <div class="d-flex align-items-center gap-2">
            <div class="px-3 py-2 border rounded small">
              {{ sig.statement_name }}
            </div>
          </div>
          <span class="text-primary small" style="cursor: pointer;">open in context →</span>
        </div>
      </div>

      <!-- Right Column: Sidebar -->
      <div style="flex: 1; min-width: 280px;">

        <!-- Performance Panel -->
        <div class="mb-3 p-3 rounded" style="background: rgba(var(--bs-dark-rgb), 0.3);">
          <app-error-performance-panel
            [errorHistoryList]="historyList()"
            [totalCount]="sig.total_count"
          />
        </div>

        <!-- Stack Issues Panel -->
        <div class="mb-3 p-3 rounded" style="background: rgba(var(--bs-dark-rgb), 0.3);">
          <app-error-stack-issues-panel
            [stackIssues]="stackIssues()"
            (issueClicked)="navigateToIssue($event)"
          />
        </div>
      </div>
    </div>
  } @else if (loadingDetail()) {
    <div class="text-center text-muted py-5">Loading error details...</div>
  } @else {
    <div class="text-center text-muted py-5">Error not found</div>
  }
</div>
```

- [ ] **Step 3: Create the styles**

```scss
// src/app/workspace/pages/workspace-error-detail-page/workspace-error-detail-page.component.scss
.error-detail-page {
  max-width: 1200px;
}
```

- [ ] **Step 4: Commit**

```bash
git add src/app/workspace/pages/workspace-error-detail-page/
git commit -m "feat(error-dashboard): add error detail page with header, histogram, performance, and stack issues"
```

---

## Task 16: Routes & Sidebar Integration

**Files:**
- Modify: `src/app/core/config/routes.config.ts`
- Modify: `src/app/core/components/workspace-side-nav/workspace-side-nav.component.ts`

- [ ] **Step 1: Add routes**

In `routes.config.ts`, find the section near the `request-errors` route (around line 782). Add two new routes in the same workspace children array:

```typescript
{
  path: 'error-dashboard',
  loadComponent: () =>
    import('../../workspace/pages/workspace-error-dashboard-page/workspace-error-dashboard-page.component').then(
      (m) => m.WorkspaceErrorDashboardPageComponent,
    ),
},
{
  path: 'error-dashboard/:signatureId',
  loadComponent: () =>
    import('../../workspace/pages/workspace-error-detail-page/workspace-error-detail-page.component').then(
      (m) => m.WorkspaceErrorDetailPageComponent,
    ),
},
```

- [ ] **Step 2: Add sidebar nav item**

In `workspace-side-nav.component.ts`, find the `monitoringNavItems` array (around line 391). Add the Error Dashboard entry as the first item:

```typescript
readonly monitoringNavItems: INavItem[] = [
  {
    path: ['error-dashboard'],
    icon: faBug,
    label: 'Error Dashboard',
    pw: 'nav-error-dashboard',
  },
  // ... existing items (Performance Insights, Audit Logs, Compliance Center)
];
```

Also add the FontAwesome import at the top of the file:

```typescript
import { faBug } from '@fortawesome/pro-solid-svg-icons';
```

If `faBug` isn't available in Pro, use `faTriangleExclamation` instead:

```typescript
import { faTriangleExclamation } from '@fortawesome/pro-solid-svg-icons';
```

- [ ] **Step 3: Build to verify everything compiles**

Run: `ng build --configuration development 2>&1 | tail -5`
Expected: Build succeeds.

- [ ] **Step 4: Commit**

```bash
git add src/app/core/config/routes.config.ts src/app/core/components/workspace-side-nav/workspace-side-nav.component.ts
git commit -m "feat(error-dashboard): add routes and sidebar navigation entry"
```

---

## Task 17: Smoke Test & Final Verification

- [ ] **Step 1: Run full build**

```bash
cd /Users/brice/git/cloud-frontend && ng build --configuration development
```

Expected: Build succeeds with no errors.

- [ ] **Step 2: Run linter**

```bash
ng lint 2>&1 | tail -20
```

Expected: No new lint errors from our files.

- [ ] **Step 3: Verify all new files exist**

```bash
find src/app/workspace -name "*error-dashboard*" -o -name "*error-sparkline*" -o -name "*error-summary-cards*" -o -name "*error-histogram*" -o -name "*error-performance-panel*" -o -name "*error-stack-issues-panel*" -o -name "trend.util.ts" | sort
```

Expected: All 36 new files listed.

- [ ] **Step 4: Final commit if any fixups were needed**

```bash
git add -A && git status
# Only commit if there are changes
git diff --cached --quiet || git commit -m "fix(error-dashboard): address build and lint issues"
```
