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.
| Tab | Module | Load Strategy | Permission Gate |
|---|---|---|---|
| Overview | dashboard | Inline (always loaded) | None (default tab) |
| Audit | audit | React.lazy + Suspense | audit_logs.view |
| Security | security | React.lazy + Suspense | Security View |
| Analytics | analytics | React.lazy + Suspense | None (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.
"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).
| Module | Entities | Interface | Repository |
|---|---|---|---|
| Audit | AuditLogEntry, AuditLogDetail, AuditLogPage, AuditFilterParams, AuditAnalyticsSummary, TopAuditUser, ComplianceReport | IAuditRepository | AuditRepository |
| Security | SecurityEvent, BlockedIP, LoginActivityPoint, SecurityChange | ISecurityRepository | SecurityRepository |
| Analytics | AnalyticsSummary, DistributionData, ComparisonDataPoint | IAnalyticsRepository | AnalyticsRepository |
| Dashboard | DashboardSummary, DashboardKPIs (overview only) | IDashboardRepository | DashboardRepository |
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.
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| Layer | Audit Module | Security Module | Analytics Module |
|---|---|---|---|
| Models | AuditModels.ts | SecurityModels.ts | AnalyticsModels.ts |
| Entities | AuditEntities.ts | SecurityEntities.ts | AnalyticsEntities.ts |
| Interfaces | IAuditRepository.ts, IAuditService.ts | ISecurityRepository.ts, ISecurityService.ts | IAnalyticsRepository.ts, IAnalyticsService.ts |
| Mapper | AuditMapper.ts | SecurityMapper.ts | AnalyticsMapper.ts |
| Service | AuditService.ts | SecurityService.ts | AnalyticsService.ts |
| Repository | AuditRepository.ts | SecurityRepository.ts | AnalyticsRepository.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.
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.
| ViewModel | Before (God Interface) | After (Dedicated Repository) |
|---|---|---|
| useAuditViewModel | dashboardRepository.getAuditLogs() | auditRepository.getLogs() |
| useSecurityDashboardViewModel | dashboardRepository.getSecurityEvents() | securityRepository.getSecurityEvents() |
| useTenantAnalyticsViewModel | dashboardRepository.getSummary() | analyticsRepository.getSummary() |
| useDashboardViewModel | 8 methods (overview + audit + security) | 4 methods (overview only) |
"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.
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.
// 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.
// 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.
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