Dashboard Hub (Hub-and-Spoke)

Modular tabbed dashboard with domain-segregated sub-modules (Audit, Security, Analytics), 6-layer clean architecture per module, ISP-compliant interfaces, lazy loading, and permission-gated tab visibility.

The Dashboard Hub is SCRIPE's central operational command center — a tabbed interface that aggregates four domain-specific views (Overview, Audit, Security, Analytics) into a unified hub. Each domain module follows a strict 6-layer clean architecture (Models → Entities → Interfaces → Services → Repositories → Mappers) with dedicated DI registration. Sub-views are lazy-loaded via React.lazy and permission-gated to ensure users only see tabs they are authorized to access.

Hub-and-Spoke Architecture

The Dashboard Hub uses a Hub-and-Spoke pattern where the main DashboardView serves as the central hub rendering a tab strip, and each tab lazy-loads an independent domain-specific view (spoke). The Overview tab is inlined for instant rendering. Audit, Security, and Analytics tabs are loaded on-demand via React.lazy with Suspense fallbacks, keeping the initial bundle lean.

Hub-and-Spoke Dashboard Architecture
DashboardView (Tabbed Hub)
Inline
Overview Tab
Audit Tab
Security Tab
Analytics Tab
TabModuleLoad StrategyPermission Gate
OverviewdashboardInline (always loaded)None (default tab)
AuditauditReact.lazy + Suspenseaudit_logs.view
SecuritysecurityReact.lazy + SuspenseSecurity View
AnalyticsanalyticsReact.lazy + SuspenseNone (visible to all)

Real-Time SignalR Updates

The dashboard hub integrates with SignalR to provide real-time updates and cache invalidation. It shares the connection with the audit log listener, invalidating the 'dashboard' query cache whenever new audit events are received. This triggers automatic TanStack query updates without page reloads or interval-based polling.

useDashboardRealtime.tsTypeScript
"use client";
import { useEffect, useCallback } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { useSignalR } from "@core/hooks/useSignalR";
import { HUB_EVENTS, HUB_METHODS } from "@core/common/constants/signalr";

/**
 * Documentation for module export
 */
export function useDashboardRealtime() {
  const queryClient = useQueryClient();
  const { connection, connectionState } = useSignalR();

  const handleAuditEvent = useCallback(() => {
    // Invalidate dashboard query cache to trigger automatic TanStack query refetching
    queryClient.invalidateQueries({ queryKey: ["dashboard"] });
  }, [queryClient]);

  useEffect(() => {
    if (!connection || connectionState !== "connected") return;

    // Listen for audit events broadcasted by the server
    connection.on(HUB_EVENTS.AUDIT_EVENT, handleAuditEvent);
    // Join tenant-scoped global notifications group
    connection.invoke(HUB_METHODS.JOIN_GLOBAL_GROUP).catch(() => {});

    return () => {
      connection.off(HUB_EVENTS.AUDIT_EVENT, handleAuditEvent);
      connection.invoke(HUB_METHODS.LEAVE_GLOBAL_GROUP).catch(() => {});
    };
  }, [connection, connectionState, handleAuditEvent]);

  return { connectionState };
}

Domain Segregation (Interface Segregation Principle)

Previously, all dashboard data flowed through a single DashboardRepository (God Interface) with 8+ methods spanning audit, security, and analytics concerns. The refactored architecture extracts each domain into an independent module with its own repository interface, eliminating monolithic coupling and adhering to the Interface Segregation Principle (ISP).

Before vs After — Interface Segregation
Before: DashboardRepository (God Interface — 8 methods)
Slimmed
After: DashboardRepository (4 overview methods)
After: AuditRepository (5 audit methods)
After: SecurityRepository (4 security methods)
After: AnalyticsRepository (3 analytics methods)
ModuleEntitiesInterfaceRepository
AuditAuditLogEntry, AuditLogDetail, AuditLogPage, AuditFilterParams, AuditAnalyticsSummary, TopAuditUser, ComplianceReportIAuditRepositoryAuditRepository
SecuritySecurityEvent, BlockedIP, LoginActivityPoint, SecurityChangeISecurityRepositorySecurityRepository
AnalyticsAnalyticsSummary, DistributionData, ComparisonDataPointIAnalyticsRepositoryAnalyticsRepository
DashboardDashboardSummary, DashboardKPIs (overview only)IDashboardRepositoryDashboardRepository

6-Layer Clean Architecture

Each extracted module (Audit, Security, Analytics) implements the full SCRIPE frontend clean architecture stack. The 6 layers ensure strict separation of concerns: Models hold raw API response shapes, Entities are rich domain objects with computed properties, Interfaces define contracts, Services handle HTTP calls via IApiService, Repositories orchestrate services and mappers to return domain entities, and Mappers perform DTO-to-entity conversion with null coalescing.

6-Layer Module Structure (per domain)Text
modules/monitoring/{audit|security|analytics|dashboard}/
├── src/
│   ├── domain/
│   │   ├── entities/     # Rich domain entity classes (getters + computed)
│   │   └── interfaces/   # IService + IRepository contracts
│   ├── data/
│   │   ├── models/       # Raw DTO types (match backend responses)
│   │   ├── mappers/      # DTO ↔ Entity static converters
│   │   ├── services/     # HTTP API calls (via IApiService)
│   │   └── repositories/ # Wraps service → returns domain entities
│   └── presentation/
│       ├── viewmodels/   # React hooks (TanStack Query + repository)
│       ├── views/        # Full-page view components
│       └── components/   # Reusable UI components
LayerAudit ModuleSecurity ModuleAnalytics Module
ModelsAuditModels.tsSecurityModels.tsAnalyticsModels.ts
EntitiesAuditEntities.tsSecurityEntities.tsAnalyticsEntities.ts
InterfacesIAuditRepository.ts, IAuditService.tsISecurityRepository.ts, ISecurityService.tsIAnalyticsRepository.ts, IAnalyticsService.ts
MapperAuditMapper.tsSecurityMapper.tsAnalyticsMapper.ts
ServiceAuditService.tsSecurityService.tsAnalyticsService.ts
RepositoryAuditRepository.tsSecurityRepository.tsAnalyticsRepository.ts

DI Container Wiring

All three new modules are registered in the SystemContainer (modules/system/di.ts). Each module wire follows the pattern: Service (takes IApiService) → Repository (takes Service) → SystemContainer interface declaration → lazy getter export. ViewModels consume repositories exclusively through the DI container — never instantiating services directly.

modules/system/di.ts — SystemContainer RegistrationTypeScript
export interface SystemContainer {
  auditRepository: IAuditRepository;
  securityRepository: ISecurityRepository;
  analyticsRepository: IAnalyticsRepository;
}

function getSystemContainer(): SystemContainer {
  const apiService = getModuleApiService("IDENTITY");
  const auditService = new AuditService(apiService);
  const securityService = new SecurityService(apiService);
  const analyticsService = new AnalyticsService(apiService);

  return {
    auditRepository: new AuditRepository(auditService),
    securityRepository: new SecurityRepository(securityService),
    analyticsRepository: new AnalyticsRepository(analyticsService),
  };
}

/**
 * Documentation for module export
 */
export const systemContainer = {
  get auditRepository() { return getSystemContainer().auditRepository; },
  get securityRepository() { return getSystemContainer().securityRepository; },
  get analyticsRepository() { return getSystemContainer().analyticsRepository; },
};

ViewModel Decoupling

Each ViewModel hook now imports its dedicated repository from the DI container instead of sharing a single dashboard repository. This eliminates cross-domain coupling: useAuditViewModel consumes only auditRepository, useSecurityDashboardViewModel consumes only securityRepository, and useTenantAnalyticsViewModel consumes only analyticsRepository. The slimmed useDashboardViewModel retains only 4 methods for the Overview tab.

ViewModelBefore (God Interface)After (Dedicated Repository)
useAuditViewModeldashboardRepository.getAuditLogs()auditRepository.getLogs()
useSecurityDashboardViewModeldashboardRepository.getSecurityEvents()securityRepository.getSecurityEvents()
useTenantAnalyticsViewModeldashboardRepository.getSummary()analyticsRepository.getSummary()
useDashboardViewModel8 methods (overview + audit + security)4 methods (overview only)
ViewModel Pattern — useAuditViewModel.tsTypeScript
"use client";
import { systemContainer } from "@modules/system/di";
import { useQuery } from "@tanstack/react-query";

export function useAuditViewModel(tenantId: string) {
  const { auditRepository } = systemContainer;

  const logsQuery = useQuery({
    queryKey: ["audit-logs", tenantId, page, pageSize],
    queryFn: () => auditRepository.getLogs(tenantId, params),
  });

  // ... domain-specific audit logic
}

Tabbed Hub Implementation

The DashboardView component serves as the hub, rendering a TabsList with 4 TabsTrigger elements (Overview, Audit, Security, Analytics). Audit and Security tabs are conditionally rendered based on the current admin's permissions using the usePermission hook. Each tab's content is wrapped in a Suspense boundary with a Skeleton fallback that matches the expected layout, preventing layout shifts during lazy loading.

DashboardView.tsx — Tabbed Hub with Lazy LoadingTypeScript
const AuditView = lazy(() => import("../../audit/.../AuditView"));
const SecurityView = lazy(() => import("../../security/.../SecurityDashboardView"));
const AnalyticsView = lazy(() => import("../../analytics/.../TenantAnalyticsView"));

function DashboardView() {
  const [activeTab, setActiveTab] = useState("overview");
  const canViewAudit = usePermission("audit_logs.view");
  const canViewSecurity = usePermission("security.view");

  return (
    <Tabs value={activeTab} onValueChange={setActiveTab}>
      <TabsList>
        <TabsTrigger value="overview">{t("dashboard.tabs.overview")}</TabsTrigger>
        {canViewAudit && <TabsTrigger value="audit">{t("dashboard.tabs.audit")}</TabsTrigger>}
        {canViewSecurity && <TabsTrigger value="security">{t("dashboard.tabs.security")}</TabsTrigger>}
        <TabsTrigger value="analytics">{t("dashboard.tabs.analytics")}</TabsTrigger>
      </TabsList>

      <TabsContent value="overview"><OverviewContent /></TabsContent>
      <TabsContent value="audit"><Suspense fallback={<Skeleton />}><AuditView /></Suspense></TabsContent>
      <TabsContent value="security"><Suspense fallback={<Skeleton />}><SecurityView /></Suspense></TabsContent>
      <TabsContent value="analytics"><Suspense fallback={<Skeleton />}><AnalyticsView /></Suspense></TabsContent>
    </Tabs>
  );
}

Tenant-Aware Caching

All TanStack Query keys across the hub include the current tenantId as a partition key. This ensures that when a SuperAdmin uses the 'Enter Tenant World' drill-down, switching tenants automatically invalidates and re-fetches all dashboard data for the new tenant context. Without tenant-partitioned keys, stale data from a previously viewed tenant could persist in the cache.

Tenant-Partitioned Query KeysTypeScript
// CORRECT — query keys include tenantId for cache isolation
queryKey: ["audit-logs", tenantId, page, pageSize, filters]
queryKey: ["security-events", tenantId, severity]
queryKey: ["analytics-summary", tenantId, dateRange]

// WRONG — missing tenantId causes cross-tenant cache pollution
queryKey: ["audit-logs", page, pageSize]

Backward Compatibility

To prevent build breaks during the migration, DashboardEntities.ts retains deprecated type aliases that re-export types from the new domain-specific modules. Components that still import from the dashboard module's entities file will continue to work, but will receive TypeScript deprecation warnings guiding developers toward the canonical import paths.

DashboardEntities.ts — Deprecated AliasesTypeScript
// Backward-compatible aliases for legacy components
/** @deprecated Use AuditLogPage from audit/domain/entities */
export type AuditLogPage = import("../../audit/...").AuditLogPage;

/** @deprecated Use BlockedIP from security/domain/entities */
export type BlockedIPSummary = import("../../security/...").BlockedIP;

/** @deprecated Use ComparisonDataPoint from analytics/domain/entities */
export type LoginActivityPoint = import("../../analytics/...").ComparisonDataPoint;

Source File Reference

The refactored Dashboard Hub spans 4 modules (dashboard, audit, security, analytics), each with its own complete 6-layer stack. The DI container (di.ts) is the single integration point where all modules are wired together.

Source File MapText
Hub:
  modules/monitoring/dashboard/src/presentation/views/DashboardView.tsx

Audit Module:
  modules/monitoring/audit/src/domain/entities/AuditEntities.ts
  modules/monitoring/audit/src/domain/interfaces/IAuditRepository.ts
  modules/monitoring/audit/src/data/services/AuditService.ts
  modules/monitoring/audit/src/data/repositories/AuditRepository.ts
  modules/monitoring/audit/src/data/mappers/AuditMapper.ts
  modules/monitoring/audit/src/data/models/AuditModels.ts

Security Module:
  modules/monitoring/security/src/domain/entities/SecurityEntities.ts
  modules/monitoring/security/src/domain/interfaces/ISecurityRepository.ts
  modules/monitoring/security/src/data/services/SecurityService.ts
  modules/monitoring/security/src/data/repositories/SecurityRepository.ts

Analytics Module:
  modules/monitoring/analytics/src/domain/entities/AnalyticsEntities.ts
  modules/monitoring/analytics/src/domain/interfaces/IAnalyticsRepository.ts
  modules/monitoring/analytics/src/data/services/AnalyticsService.ts
  modules/monitoring/analytics/src/data/repositories/AnalyticsRepository.ts

DI Container:
  modules/system/di.ts

Locales:
  modules/monitoring/dashboard/locales/dashboard.en.ts
  modules/monitoring/dashboard/locales/dashboard.ar.ts