# GbDashboard & GbDashboardAction Deep Analysis
**Date:** 2026-02-24
**Scope:** `features/gbdashboard/` and `features/gbdashboardaction/` — all components, services, templates, styles
**Role:** Main dashboard shell — portlet grid, page management, pub/sub widget communication

---

## 1. Component Inventory

### features/gbdashboard/

| File | Lines | Type | Purpose |
|---|---|---|---|
| `gbdashboard.component.ts` | 782 | Component | Main dashboard grid container with portlet management |
| `gbdashboard.component.html` | 107 | Template | Gridster layout with lightbox, modal, tab content |
| `gbdashboard.component.scss` | 460 | Styling | Grid, modal, widget, lightbox, responsive styles |
| `gbdashboard.component.spec.ts` | 1 | Test | **Empty** |
| `service/gbdashboard.service.ts` | 74 | Service | API orchestration layer |
| `service/WidgetRegistry.Service.ts` | 28 | Service | Dynamic component registry |
| `service/widgetcomponentmap.ts` | 32 | Config | Widget type → component mapping |
| `dbservice/gbdashboard.db.service.ts` | 28 | DBService | HTTP client wrapper |
| `model/idashboard.ts` | 27 | Interface | DashboardItem & DashboardWidgetInteraction types |

**Total: 9 files, 1,539 lines**

### features/gbdashboardaction/

| File | Lines | Type | Purpose |
|---|---|---|---|
| `gbdashboardaction.component.ts` | 77 | Component | Action bar — tabs and control buttons |
| `gbdashboardaction.component.html` | 45 | Template | Tab group and button layout |
| `gbdashboardaction.component.scss` | 93 | Styling | Button and tab styling |
| `gbdashboardaction.component.spec.ts` | 1 | Test | **Empty** |

**Total: 4 files, 216 lines**

---

## 2. Critical Issues (P0 — Fix Before Any Release)

### SEC-01 — sessionStorage Usage for LoginDTO (Known P0 Violation)
**Files:**
- [gbdashboard.component.ts:135-138](features/gbdashboard/gbdashboard.component.ts#L135)
- [gbdashboard.service.ts:27](features/gbdashboard/service/gbdashboard.service.ts#L27)

```typescript
// gbdashboard.component.ts
const TabId = sessionStorage.getItem('TabId');
const loginDTOStr = sessionStorage.getItem('LoginDTO');   // ← P0 violation
const CurrentTabId = sessionStorage.getItem('CurrentTabId');

// gbdashboard.service.ts
this.LoginDTODetail = JSON.parse(sessionStorage.getItem('LoginDTO') as any);  // ← P0
```
**Impact:** LoginDTO in sessionStorage is accessible to any JavaScript on the page, including injected XSS payloads. Listed as a P0 in CLAUDE.md.
**Fix:** LoginDTO must come from an httpOnly cookie or a trusted signals-based auth service. Access via `DataPassingService` or `GbAppStateService` if already loaded.

---

### PERF-01 — Missing ChangeDetectionStrategy.OnPush on DashboardComponent
**File:** [gbdashboard.component.ts:13](features/gbdashboard/gbdashboard.component.ts#L13)
**Severity:** P0 Performance — Critical for a dashboard

```typescript
@Component({
  selector: 'gb-dashboard',
  standalone: true,
  // ❌ No changeDetection: ChangeDetectionStrategy.OnPush
})
```
**Impact:** Without OnPush, Angular's default change detection runs on every browser event (click, keypress, mouse move) and re-evaluates all bindings in this 782-line component. For a dashboard that renders potentially dozens of portlets via gridster, this is extremely costly. `gbdashboardaction` correctly has OnPush — the parent dashboard does not.

**Fix:**
```typescript
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
})
```
With signals this mostly becomes automatic, but OnPush must still be declared.

---

### DEBUG-01 — Debug CSS Colors Left in Production SCSS
**File:** [gbdashboard.component.scss:269,275](features/gbdashboard/gbdashboard.component.scss#L269)

```scss
// Line 269
::ng-deep gridster.fixed.display-grid {
  background-color: rgba(0, 0, 255, 0.3);  // ← DEBUG: bright blue background
}

// Line 275
.grid-bg {
  background: greenyellow;                  // ← DEBUG: green debug color
}
```
**Impact:** Visible debug colors render in production. The blue gridster background and greenyellow overlay will show to end users.
**Fix:** Remove both declarations entirely.

---

## 3. Memory Leaks

### LEAK-01 — setTimeout Without Cleanup
**File:** [gbdashboard.component.ts:656](features/gbdashboard/gbdashboard.component.ts#L656)

```typescript
setTimeout(() => {
  dialogRef.close();
}, 1000);
```
**Impact:** If the component is destroyed before 1 second elapses (user navigates away), the callback still executes on a destroyed component reference. Minor risk since it only calls `dialogRef.close()`, but violates cleanup standards.

**Fix:**
```typescript
// Option 1: rxjs timer (no cleanup needed — observable completes)
timer(1000).pipe(take(1)).subscribe(() => dialogRef.close());

// Option 2: If in ngAfterViewInit context
timer(1000).pipe(takeUntil(this.destroy$)).subscribe(() => dialogRef.close());
```

---

### LEAK-02 — All Other Subscriptions (✅ Correctly Handled)
The following subscriptions all correctly use `takeUntil(this.destroy$)`:
- Line 186: `reloadPortletData()` — GetReportData
- Line 235: `GetPages()` — GetPages
- Line 264: `GetPortletData()` — GetPortletType
- Line 280: `GetPagesRoleBased()` — GetPagesRoleBased
- Line 435: `GetPortletdashboardData()` — GetPortletType
- Line 500: nested GetMenuReport
- Line 535: nested GetReportData
- Line 607: `AddPortletType()`
- Line 637: `SaveDashboard()`
- Line 643: `DeletePortlet()`

And `destroy$` is properly completed in `ngOnDestroy()` (lines 764–766). ✅

---

## 4. Performance Issues

### PERF-02 — All HTTP Data Uses Raw .subscribe() — Not Signal-Based
**File:** [gbdashboard.component.ts:186,235,264,280,435,500,535,607,637,643](features/gbdashboard/gbdashboard.component.ts#L186)

```typescript
// Every API call follows this pattern:
this.service.GetPages(url, params)
  .pipe(takeUntil(this.destroy$))
  .subscribe((result: any) => {
    this.AllPage = result?.responseValue;
    this.cdr.detectChanges();   // ← manual CD needed because no signals
  });
```
**Impact:** Requires manual `ChangeDetectorRef.detectChanges()` calls throughout (lines 202, 268, 494, 563). The component cannot use `ChangeDetectionStrategy.OnPush` efficiently without signals. This pattern is pre-Angular 16.

**Fix:** Convert to `rxResource()` for HTTP state management:
```typescript
// Old pattern (10 separate subscribes with takeUntil)
pagesResource = rxResource({
  request: () => ({ userId: this.loginData?.UserId }),
  loader: ({ request }) => this.service.GetPages(this.pagesUrl, request)
});

// Then in template:
pages = computed(() => this.pagesResource.value()?.responseValue ?? []);
isLoadingPages = this.pagesResource.isLoading;
```

---

### PERF-03 — ChangeDetectorRef.detectChanges() Called Manually 4+ Times
**File:** [gbdashboard.component.ts:202,268,494,563](features/gbdashboard/gbdashboard.component.ts#L202)

```typescript
this.cdr.detectChanges();   // Repeated after every subscribe callback
```
**Impact:** Manual CD calls bypass Angular's scheduler, cause synchronous rendering, and become unnecessary with OnPush + signals.

**Fix:** Eliminate by adopting signals pattern — `rxResource()` values are signals, automatically triggering OnPush when they change.

---

### PERF-04 — Unused Static Methods Creating Dead Code
**File:** [gbdashboard.component.ts:128-132](features/gbdashboard/gbdashboard.component.ts#L128)

```typescript
static itemChange(item: any, itemComponent: any) { }
static itemResize(item: any, itemComponent: any) { }
```
These are empty callbacks for gridster events, never called. They exist only as stubs.
**Fix:** Delete both or implement real gridster event handling to persist layout changes on resize/move.

---

### PERF-05 — GridsterConfig Callbacks Use `any` Parameters
**File:** [gbdashboard.component.ts:65-102](features/gbdashboard/gbdashboard.component.ts#L65)

```typescript
options: GridsterConfig = {
  itemChangeCallback: GbDashboardComponent.itemChange,   // static method
  itemResizeCallback: GbDashboardComponent.itemResize,   // static method
  // ...
};
```
Since both callbacks are empty, layout changes (drag, resize) are not persisted. Users who rearrange their dashboard will lose changes on reload.
**Fix:** Implement `SaveDashboard()` as part of the item change callback, or add a dirty flag + save button.

---

## 5. Code Quality & Maintainability Issues

### QUAL-01 — Excessive `any` Types (20+ Instances)
**File:** [gbdashboard.component.ts:54-62,106-108](features/gbdashboard/gbdashboard.component.ts#L54)

```typescript
DrillDownFilterCriteria: any;           // line 54 — undefined typed
selectedviewID: any;                     // line 55
Reportdetails: any;                      // line 56
MenuDetail!: any;                        // line 57
ReportCriteriaArray!: any[];            // line 60
CriteriaConfigArray!: any;              // line 61
WebServiceSettingArray!: any[];         // line 62
loginData: any = '';                     // line 106 — initialized as string but treated as object
Portlets: any;                           // line 107
AllPage: any;                            // line 108
```

And in `idashboard.ts`:
```typescript
Label: any;   // DashboardItem.Label — should be string
Data: any;    // DashboardItem.Data — should be typed
```

**Fix:** Define interfaces for all these:
```typescript
interface LoginDTO {
  UserId: string;
  UserCode: string;
  OUId: number;
  PeriodId: number;
  // ...
}

interface PortletData {
  PortletId: number;
  PortletName: string;
  PortletType: string;
  MenuId: string;
  // ...
}

interface PageDetail {
  PageId: number;
  PageName: string;
  IsDefault: boolean;
  SortOrder: number;
}
```

---

### QUAL-02 — GbDashboardService Mixes Injection Styles
**File:** [gbdashboard.service.ts:23-26](features/gbdashboard/service/gbdashboard.service.ts#L23)

```typescript
export class GBDashboardService {
  public dialog = inject(MatDialog);           // ← inject()
  public translate = inject(TranslocoService); // ← inject() but translate never used

  constructor(
    private formActiondbservice: FormActionDBService,  // ← constructor injection
    private localhttp: HttpClient,
    private dbservice: GbdashboardDBService,
    private reportdbservice: Reportviewerdbservice
  ) { }
}
```
**Fix:** Use `inject()` consistently for all:
```typescript
export class GBDashboardService {
  private dialog = inject(MatDialog);
  private formActiondbservice = inject(FormActionDBService);
  private localhttp = inject(HttpClient);
  private dbservice = inject(GbdashboardDBService);
  private reportdbservice = inject(Reportviewerdbservice);
  // Remove TranslocoService — it's injected but never used
}
```

---

### QUAL-03 — TranslocoService Injected but Never Used
**File:** [gbdashboard.service.ts:24](features/gbdashboard/service/gbdashboard.service.ts#L24)

```typescript
public translate = inject(TranslocoService);  // ← injected but never called
```
**Impact:** Dead dependency. Adds bundle weight.
**Fix:** Remove. When i18n is added, inject it only where needed.

---

### QUAL-04 — Direct DOM Manipulation via nativeElement
**File:** [gbdashboard.component.ts:432,611,663](features/gbdashboard/gbdashboard.component.ts#L432)

```typescript
this.myModal.nativeElement.style.display = 'none';    // line 432
this.myModal.nativeElement.style.display = 'block';   // line 611
this.myModal.nativeElement.style.display = 'none';    // line 663
```
**Impact:** Direct style manipulation bypasses Angular's rendering, breaks SSR, and doesn't work with `ChangeDetectionStrategy.OnPush` tracking.

**Fix:** Use a signal to control modal visibility:
```typescript
isModalOpen = signal(false);
// Template: @if (isModalOpen()) { <div class="modal"> ... </div> }
// Or: [style.display]="isModalOpen() ? 'block' : 'none'"
```

---

### QUAL-05 — Non-Null Assertions on ViewChild Without Safety Checks
**File:** [gbdashboard.component.ts:45-46](features/gbdashboard/gbdashboard.component.ts#L45)

```typescript
@ViewChild('container', { read: ViewContainerRef }) container!: ViewContainerRef;
@ViewChild("myModal") myModal!: ElementRef;
```
If the template ref is conditionally rendered (inside `*ngIf`), these will be `undefined` at runtime causing runtime errors.

**Fix:** Add null guards before use:
```typescript
if (this.myModal?.nativeElement) {
  this.myModal.nativeElement.style.display = 'none';
}
```
Or better, use the signal-based visibility approach from QUAL-04.

---

### QUAL-06 — Overuse of `::ng-deep` (7 Instances)
**File:** [gbdashboard.component.scss:84,240,267,290,388,392,400](features/gbdashboard/gbdashboard.component.scss#L84)

`::ng-deep` breaks ViewEncapsulation and is deprecated. 7 usages affect:
- Gridster item styling
- Material tab styles
- Widget header styles
- Scroll container overrides

**Fix:** Use Angular Material's theming API or CSS custom properties for Material overrides. For third-party libraries (gridster2), use `ViewEncapsulation.None` on a wrapper component or global styles in `styles.scss`.

---

### QUAL-07 — Empty Test Files
**Files:**
- `gbdashboard.component.spec.ts` — 1 line, empty
- `gbdashboardaction.component.spec.ts` — 1 line, empty

Zero test coverage for critical dashboard functionality.
**Fix:** Add minimum unit tests for:
- Page switching logic
- Portlet add/delete/save workflows
- Widget registry service (get, register, getAll)
- Data transformation functions

---

### QUAL-08 — `DashBoardOutput` EventEmitter Typed as `any`
**File:** [gbdashboardaction.component.ts:26](features/gbdashboardaction/gbdashboardaction.component.ts#L26)

```typescript
@Output() DashBoardOutput: EventEmitter<any> = new EventEmitter();
```
And the parent subscribes via:
```typescript
(DashBoardOutput)="handleDashBoardOutput($event)"
```
**Fix:** Define a discriminated union type for all action events:
```typescript
type DashboardAction =
  | { type: 'save' }
  | { type: 'delete' }
  | { type: 'AddPortlet' }
  | { type: 'edit' }
  | { type: 'visibility' }
  | { type: 'PAGE_CHANGE'; page: PageDetail };

@Output() DashBoardOutput = new EventEmitter<DashboardAction>();
```

---

### QUAL-09 — Widget Registry is Not Tree-Shakeable
**File:** [service/widgetcomponentmap.ts](features/gbdashboard/service/widgetcomponentmap.ts)

The `widgetcomponentmap.ts` imports all widget components upfront:
```typescript
import { ChartWidget } from '...';
import { TableWidget } from '...';
import { CardWidget } from '...';
// ...all 10+ widget types always bundled
```
**Impact:** All widget components are included in the bundle even if only a subset are used in any given deployment.
**Fix:** Use dynamic imports via `import()` in the registry loader:
```typescript
const WIDGET_MAP: Record<string, () => Promise<Type<unknown>>> = {
  'chart': () => import('../widgets/chart-widget').then(m => m.ChartWidget),
  'table': () => import('../widgets/table-widget').then(m => m.TableWidget),
};
```

---

## 6. i18n Issues

### I18N-01 — Hardcoded English Strings in Templates
**File:** [gbdashboard.component.html:5,6,23,47,50,53,75](features/gbdashboard/gbdashboard.component.html#L5)

```html
<!-- All hardcoded — must use Transloco keys -->
"No Data found"
"Add or Select Page to View Portlet Dashboard"
matTooltip="Expand"
"Delete"
"Settings"
"More Info"
"Portlet"
```

**File:** [gbdashboardaction.component.html:19,24,27,31,35,40](features/gbdashboardaction/gbdashboardaction.component.html#L19)
```html
"Create Page"
"Add Portlet"
matTooltip="Save"
matTooltip="Delete"
matTooltip="View"
matTooltip="Edit"
```

**Fix:** Replace all with Transloco keys following `module.section.key` convention:
```html
{{ 'dashboard.empty.no_data' | transloco }}
{{ 'dashboard.empty.select_page' | transloco }}
[matTooltip]="'dashboard.portlet.expand' | transloco"
{{ 'dashboard.portlet.delete' | transloco }}
{{ 'dashboard.portlet.settings' | transloco }}
{{ 'dashboard.portlet.more_info' | transloco }}

<!-- Action bar -->
{{ 'dashboard.action.create_page' | transloco }}
{{ 'dashboard.action.add_portlet' | transloco }}
[matTooltip]="'dashboard.action.save' | transloco"
```

---

## 7. Responsive Design Issues

### RESP-01 — Fixed Pixel Heights/Widths in SCSS
**File:** [gbdashboard.component.scss:10,45,46,49,77,118,119,147,167,256](features/gbdashboard/gbdashboard.component.scss#L10)

```scss
.my-gridster { height: 525px; }          // line 10 — clips on short screens
.modal-content { height: 560px; }        // line 45 — overflow on mobile
.modal-content { width: 280px; }         // line 46 — too narrow on wide screens
.modal-content { right: 517px; }         // line 49 — breaks on different resolutions
.NoPortlet { top: 200px; }               // line 77 — magic offset
.grid-item { width: 96px; height: 80px; }  // line 118-119
.Naviconcontainer { margin-left: 220px; }  // line 167 — sidebar assumption
.gridster-wrapper { height: calc(100vh - 150px); }  // line 256 — magic number
```

**Fix per CLAUDE.md:**
```scss
.gridster-wrapper {
  height: calc(100dvh - var(--header-height, 58px) - var(--footer-height, 0px));
}

.modal-content {
  width: min(280px, 95vw);
  height: auto;
  max-height: 90vh;
  overflow-y: auto;
}
```

---

### RESP-02 — No RTL CSS Support
**File:** [gbdashboard.component.scss](features/gbdashboard/gbdashboard.component.scss)

The following directional properties have no RTL counterpart:
- `.modal-content { right: 517px; }` — should flip to `left` in RTL
- `.Naviconcontainer { margin-left: 220px; }` — should flip to `margin-right`
- Multiple `left:`, `margin-left:` values throughout

**Fix:** Add RTL section at end of SCSS:
```scss
[dir="rtl"] {
  .modal-content {
    right: auto;
    left: 517px;
  }
  .Naviconcontainer {
    margin-left: 0;
    margin-right: 220px;
  }
}
```

---

## 8. Functional Issues

### FUNC-01 — Layout Changes (Drag/Resize) Not Persisted
**File:** [gbdashboard.component.ts:128-132](features/gbdashboard/gbdashboard.component.ts#L128)

```typescript
// Gridster callbacks — empty, never save layout
static itemChange(item: any, itemComponent: any) { }
static itemResize(item: any, itemComponent: any) { }
```
**Impact:** When a user drags a portlet to a new position or resizes it, the new grid coordinates (x, y, cols, rows) are updated in the gridster model but never saved back to the API. On next page load, the layout resets to the default.

**Fix:** Implement debounced save on change:
```typescript
private layoutChange$ = new Subject<void>();

constructor() {
  this.layoutChange$.pipe(
    debounceTime(1000),
    takeUntil(this.destroy$)
  ).subscribe(() => this.SaveDashboard());
}

itemChange(item: DashboardItem) {
  this.layoutChange$.next();
}
```

---

### FUNC-02 — Page Tab Selection Synchronization Between Components
**File:** [gbdashboardaction.component.ts:45-49](features/gbdashboardaction/gbdashboardaction.component.ts#L45)

```typescript
ngOnChanges(changes: SimpleChanges): void {
  if (changes['selectedIndex']) {
    this.selectedPage = this.PageDetails[this.selectedIndex];
  }
}
```
The action bar receives `selectedIndex` from the parent, but the parent computes the selected page from a separate internal state. If the parent's page list is refreshed asynchronously after the action component renders, `selectedIndex` may point to a stale or non-existent page.

**Fix:** Pass the selected page object directly instead of relying on index:
```typescript
@Input() selectedPage: PageDetail | null = null;
// Remove selectedIndex input — fragile coupling
```

---

### FUNC-03 — Portlet Data Load Doesn't Handle Empty Response Gracefully
**File:** [gbdashboard.component.ts:264-280](features/gbdashboard/gbdashboard.component.ts#L264)

```typescript
this.service.GetPortletData(url, params)
  .pipe(takeUntil(this.destroy$))
  .subscribe((result: any) => {
    this.Portlets = result?.responseValue;  // Could be null/undefined
    // No null guard — subsequent code may crash
  });
```
**Impact:** If the API returns `null` or an unexpected shape, `this.Portlets` becomes `null`/`undefined`, and any downstream `.map()`, `.filter()`, or `.forEach()` on it will throw.

**Fix:**
```typescript
this.Portlets = result?.responseValue ?? [];
```

---

### FUNC-04 — Widget Registry Has No Fallback for Unknown Portlet Types
**File:** [service/WidgetRegistry.Service.ts](features/gbdashboard/service/WidgetRegistry.Service.ts)

```typescript
get(type: string): Type<any> | undefined {
  return this.registry.get(type);
}
```
When `get()` returns `undefined` for an unknown portlet type, the dynamic component loader will fail silently or crash. There's no fallback "unknown widget" component to display.

**Fix:**
```typescript
get(type: string): Type<unknown> {
  return this.registry.get(type) ?? this.unknownWidgetComponent;
}
```
Register an `UnknownWidgetComponent` that displays `"Widget type '{{type}}' not found"` with the portlet type name for debugging.

---

### FUNC-05 — Dashboard State Not Reactive to Tab Changes in Parent
**File:** [gbdashboard.component.ts](features/gbdashboard/gbdashboard.component.ts)

When a user switches pages (tabs), `GetPortletdashboardData()` is called to reload portlets. However, there is no loading state shown to the user during this transition — the old portlet content remains visible until the new data arrives, then the grid re-renders.

**Fix:** Add a loading state signal:
```typescript
isLoading = signal(false);

GetPortletdashboardData() {
  this.isLoading.set(true);
  this.service.GetPortletData(url, params).pipe(
    takeUntil(this.destroy$),
    finalize(() => this.isLoading.set(false))
  ).subscribe(result => {
    this.dashboard = this.transformPortlets(result);
  });
}
// Template: @if (isLoading()) { <mat-spinner/> }
```

---

### FUNC-06 — PubSub / Widget Cross-Communication Not Implemented
**File:** [model/idashboard.ts:17-27](features/gbdashboard/model/idashboard.ts#L17)

```typescript
export interface DashboardWidgetInteraction {
  type: 'WIDGET_INTERACTION';
  payload: {
    sourceWidgetId: number;
    targetWidgetId?: number;
    action: string;
    data: any;
  };
}
```
This interface exists but is **never used anywhere** in the codebase. The pub/sub infrastructure for cross-widget communication is defined at the model level but not wired up. Portlets currently cannot communicate with each other (e.g., clicking a bar in a chart portlet to filter a table portlet).

**Fix (Architecture):** Implement a dashboard-scoped signal bus:
```typescript
// In GBDashboardService or a new DashboardEventBusService
@Injectable()
export class DashboardEventBusService {
  private events = signal<DashboardWidgetInteraction | null>(null);
  readonly latestEvent = this.events.asReadonly();

  publish(event: DashboardWidgetInteraction) {
    this.events.set(event);
  }
}

// In each widget component:
private bus = inject(DashboardEventBusService);
effect(() => {
  const event = this.bus.latestEvent();
  if (event?.payload.targetWidgetId === this.portletId()) {
    this.applyFilter(event.payload.data);
  }
});
```

---

### FUNC-07 — Page Delete Doesn't Validate Active Page
**File:** [gbdashboard.component.ts:643](features/gbdashboard/gbdashboard.component.ts#L643)

```typescript
DeletePortlet(portlet: any) {
  this.service.DeletePortlet(url, params)
    .pipe(takeUntil(this.destroy$))
    .subscribe((result: any) => {
      this.GetPortletdashboardData();  // reloads portlet list
    });
}
```
After deleting a portlet, the grid reloads. But if the deleted portlet was the last one on a page, the page becomes empty without any empty state guidance. And if a page itself is deleted (via action bar), the `selectedIndex` in the action component may be out of bounds for the new page list.

**Fix:** After page deletion, reset `selectedIndex` to 0 and verify the page list is non-empty before reloading portlet data.

---

## 9. Architecture Recommendations

### ARCH-01 — Convert to Full Signals Architecture
**Current state:** RxJS subscriptions throughout with `takeUntil`, manual `detectChanges()`, missing OnPush.
**Target state:**

```typescript
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class GbDashboardComponent {
  private service = inject(GBDashboardService);
  private sharedservice = inject(DataPassingService);

  // Page state
  selectedPageId = signal<number | null>(null);

  // HTTP resources — loading/error/value all signals
  pagesResource = rxResource({
    loader: () => this.service.GetPages(this.pagesUrl, this.loginParams())
  });

  portletsResource = rxResource({
    request: () => ({ pageId: this.selectedPageId() }),
    loader: ({ request }) => request.pageId
      ? this.service.GetPortletData(this.portletsUrl, request)
      : EMPTY
  });

  // Derived state — free, lazy, cached
  pages = computed(() => this.pagesResource.value()?.responseValue ?? []);
  portlets = computed(() => this.portletsResource.value()?.responseValue ?? []);
  dashboard = computed(() => this.buildDashboard(this.portlets()));
  isLoading = computed(() => this.pagesResource.isLoading() || this.portletsResource.isLoading());
}
```
This eliminates: 10 subscriptions, 4 `detectChanges()` calls, `destroy$` + `ngOnDestroy` for subscriptions.

---

### ARCH-02 — Implement Real PubSub for Cross-Portlet Communication
The `DashboardWidgetInteraction` interface already defines the contract. The missing piece is:

1. **DashboardEventBusService** — signal-based event bus scoped to the dashboard
2. **Widget subscription in dynamic widgets** — each widget subscribes to events targeting it
3. **Widget publication** — chart/table widgets emit events when user interacts

This enables the full "select a row in portlet A to filter portlet B" pattern that is expected in a powerful dashboard platform.

```
User clicks bar in Chart Portlet
  → ChartWidget emits { sourceWidgetId: 5, action: 'FILTER', data: { period: 'Q1' } }
  → DashboardEventBusService.publish(event)
  → TablePortlet (subscribed with targetWidgetId: 6) receives event
  → TablePortlet reloads with filter: { period: 'Q1' }
```

---

### ARCH-03 — Widget Registry Should Support Lazy Loading
**Current:** All widgets imported statically in `widgetcomponentmap.ts` — full bundle always loaded.
**Recommended:** Dynamic imports with `import()` for each widget type. Register as async loaders:

```typescript
export const WIDGET_LOADERS: Record<string, () => Promise<Type<unknown>>> = {
  'chart': () => import('../widgets/chart').then(m => m.ChartWidget),
  'table': () => import('../widgets/table').then(m => m.TableWidget),
  'html': () => import('../widgets/html').then(m => m.HtmlWidget),
  // ...
};
```
This enables Angular's route-level code splitting to exclude unused widget types per MFE.

---

### ARCH-04 — Portlet Add Dialog Should Refresh Registry
**Current:** `AddPortletType()` calls the API to add a portlet, then reloads `GetPortletdashboardData()`. If a new portlet type is registered dynamically (e.g., via backend config), it won't appear until the page is hard-refreshed because `widgetcomponentmap.ts` is static.

**Fix:** Make `WidgetRegistryService.register()` callable at runtime, potentially from an API response that returns `{ type: string, componentName: string }` pairs. Combine with lazy loading from ARCH-03.

---

### ARCH-05 — GbDashboardService Should Be Split
**Current responsibilities (too broad):**
- Form metadata (`GetFormDetails`)
- Portlet CRUD (`GetPortletType`, `AddPortletType`, `SavePagePortlet`, `DeletePortlet`)
- Page management (`GetPages`, `GetPagesRoleBased`)
- Report data (`GetMenuReport`, `GetReportData`)
- Form deletion (delegated to FormActionDBService)

**Proposed split:**
| New Service | Responsibility |
|---|---|
| `DashboardPageService` | Page CRUD: GetPages, GetPagesRoleBased, SavePage, DeletePage |
| `DashboardPortletService` | Portlet CRUD: GetPortletType, Add, Save, Delete |
| `DashboardReportService` | Report data: GetMenuReport, GetReportData |
| `WidgetRegistryService` | Already separated — keep as-is |

---

## 10. Summary — Priority Matrix

| ID | Issue | Severity | Effort |
|---|---|---|---|
| SEC-01 | LoginDTO in sessionStorage | P0 Critical | Medium |
| PERF-01 | Missing OnPush on DashboardComponent | P0 Critical | Low |
| DEBUG-01 | Debug CSS colors in production | P0 | Trivial |
| FUNC-01 | Drag/resize not persisted | P1 High | Medium |
| FUNC-06 | PubSub interface unused | P1 High | High |
| FUNC-03 | No null guard on portlet response | P1 High | Trivial |
| FUNC-04 | No fallback for unknown widget type | P1 High | Low |
| LEAK-01 | setTimeout without cleanup | P1 High | Trivial |
| QUAL-01 | 20+ `any` types — no interfaces | P2 Medium | Medium |
| QUAL-02 | Mixed injection styles in service | P2 Medium | Low |
| QUAL-03 | TranslocoService injected, never used | P2 Medium | Trivial |
| QUAL-04 | Direct DOM manipulation via nativeElement | P2 Medium | Low |
| QUAL-06 | 7× `::ng-deep` usage | P2 Medium | Medium |
| PERF-02 | All HTTP via subscribe() not toSignal/rxResource | P2 Medium | High |
| PERF-03 | 4× manual detectChanges() | P2 Medium | Low |
| PERF-04 | Empty static itemChange/itemResize methods | P2 Medium | Low |
| I18N-01 | 15+ hardcoded English strings | P2 Medium | Medium |
| RESP-01 | Fixed pixel heights/widths | P2 Medium | Medium |
| RESP-02 | No RTL CSS | P2 Medium | Medium |
| FUNC-02 | selectedIndex fragile coupling | P2 Medium | Low |
| FUNC-05 | No loading state on page switch | P2 Medium | Low |
| FUNC-07 | Page delete edge cases | P2 Medium | Low |
| QUAL-05 | ViewChild non-null assertions | P2 Medium | Low |
| QUAL-07 | Empty test files | P3 Low | High |
| QUAL-08 | EventEmitter typed as `any` | P3 Low | Low |
| QUAL-09 | Widget registry not tree-shakeable | P3 Low | High |
| ARCH-01 | Convert to signals architecture | P3 Low | High |
| ARCH-02 | Wire up PubSub cross-portlet bus | P3 Low | High |
| ARCH-03 | Lazy-load widget registry | P3 Low | High |
| ARCH-05 | Split GBDashboardService | P3 Low | Medium |

---

## 11. Quick Wins (< 30 minutes each)

1. **Add `changeDetection: ChangeDetectionStrategy.OnPush`** to `GbDashboardComponent` (PERF-01)
2. **Remove debug CSS colors** — `greenyellow` (line 275) and `rgba(0,0,255,0.3)` (line 269) from SCSS (DEBUG-01)
3. **Add null guard** `?? []` to all portlet/page responses (FUNC-03)
4. **Remove `TranslocoService` injection** from service — it's never used (QUAL-03)
5. **Fix setTimeout** — replace with `timer(1000).pipe(take(1))` (LEAK-01)
6. **Add null guard** before `this.myModal.nativeElement` access (QUAL-05)
7. **Replace all hardcoded tooltip strings** with Transloco keys — `matTooltip="Save"` → `[matTooltip]="'dashboard.action.save' | transloco"` (I18N-01)
8. **Fix modal width** to `min(280px, 95vw)` in SCSS (RESP-01)
9. **Delete empty static methods** `itemChange()` and `itemResize()` and wire up real gridster callbacks (PERF-04)
10. **Remove `cdr.detectChanges()` calls** once OnPush is added with proper signal-based state (PERF-03)

---

## 12. Vision: Signal-Based Pub/Sub Dashboard

The existing `DashboardWidgetInteraction` interface hints at the intended architecture. The full vision for a powerful dashboard platform with signals:

```typescript
// 1. Dashboard-scoped event bus (provide in DashboardComponent)
@Injectable()
export class DashboardEventBusService {
  private event$ = signal<DashboardWidgetInteraction | null>(null);
  readonly event = this.event$.asReadonly();
  publish(e: DashboardWidgetInteraction) { this.event$.set(e); }
}

// 2. Each widget reads and reacts
export class ChartWidgetComponent {
  private bus = inject(DashboardEventBusService);
  private portletId = input.required<number>();

  // React to external filter events targeting this widget
  _ = effect(() => {
    const e = this.bus.event();
    if (e?.payload.targetWidgetId === this.portletId()) {
      this.applyFilter(e.payload.data);
    }
  });

  // Publish click events
  onBarClick(data: FilterData) {
    this.bus.publish({
      type: 'WIDGET_INTERACTION',
      payload: { sourceWidgetId: this.portletId(), action: 'FILTER', data }
    });
  }
}

// 3. Dashboard provides the bus — scoped, not global
@Component({
  providers: [DashboardEventBusService]  // scoped to dashboard instance
})
export class GbDashboardComponent { }
```

This pattern enables:
- Chart → Table drill-down filtering
- KPI card → Detail grid expansion
- Period selector → All portlets refresh
- Zero shared global state — bus is scoped per dashboard instance
