diff --git a/.hunspell/ignore.yaml b/.hunspell/ignore.yaml index 1fdf4340..f8862969 100644 --- a/.hunspell/ignore.yaml +++ b/.hunspell/ignore.yaml @@ -544,6 +544,7 @@ accepted_words: - GaugeVec - CounterVec - biometric + - ViewModel # --- Terminologia Standard Progetto --- - pb @@ -985,3 +986,4 @@ accepted_words: - retrocompatibilità - impersonificata - loggando + - presentazionale diff --git a/docs/13-pb/docest/specifica_tecnica_frontend/assets/01-app-architecture.puml b/docs/13-pb/docest/specifica_tecnica_frontend/assets/01-app-architecture.puml index ad2bbc6c..5b7d7d2b 100644 --- a/docs/13-pb/docest/specifica_tecnica_frontend/assets/01-app-architecture.puml +++ b/docs/13-pb/docest/specifica_tecnica_frontend/assets/01-app-architecture.puml @@ -1,432 +1,94 @@ @startuml -title NOTIP Frontend - Application Architecture +title NoTIP Frontend - Package Dependencies Architecture skinparam backgroundColor #FFFFFF -skinparam defaultFontSize 10 +skinparam defaultFontSize 11 skinparam defaultFontColor #000000 -skinparam nodesep 8 -skinparam ranksep 8 -skinparam linetype ortho +skinparam nodesep 40 +skinparam ranksep 50 skinparam packageStyle rectangle +left to right direction -package "NOTIP Frontend (Angular 21 Standalone)" <> { - class "app.config.ts" as AppConfig { - +provideKeycloak() - +provideHttpClient() - +provideRouter() - +provideMgmtApi('/api/mgmt') - +provideDataApi('/api/data') - +AutoRefreshTokenService - +UserActivityService - } - - class "app.routes.ts" as AppRoutes { - +routes: Routes[] - } +package "NoTIP Frontend (Angular 21)" { - class "AppComponent" as AppComponent { - + - } - - package "Core Infrastructure" as Core { - package "Guards" as Guards { - class "AuthGuard" as AuthGuard { - +canActivate(): Promise - } - class "RoleGuard" as RoleGuard { - +canActivate(): boolean | UrlTree - } - class "HomeRedirectGuard" as HomeRedirectGuard { - +canActivate(): UrlTree - } - } - - package "Interceptors" as Interceptors { - class "authInterceptor" as AuthInterceptor { - +HttpInterceptorFn - } - class "errorInterceptor" as ErrorInterceptor { - +HttpInterceptorFn - } - } - - package "Models" as Models { - class "enums.ts" as Enums { - UserRole - TenantStatus - GatewayStatus - CommandStatus - AlertsType - } - class "gateway.ts" as GatewayModel { - Gateway - ObfuscatedGateway - AddGatewayParameters - } - class "measure.ts" as MeasureModel { - TelemetryEnvelope - DecryptedEnvelope - CheckedEnvelope - ObfuscatedEnvelope - } - class "tenant.ts" as TenantModel { - Tenant - CreateTenantParameters - } - class "user.ts" as UserModel { - ViewUser - CreatedUser - UserParameters - } - class "alert.ts" as AlertModel { - Alerts - AlertsConfig - AlertsFilter - } - class "threshold.ts" as ThresholdModel { - Threshold - SensorThreshold - TypeThreshold - } - } - - package "Services" as CoreServices { - class "AuthService" as AuthService { - -keycloak: KeycloakService - +init(): Promise - +login(): void - +logout(): void - +getToken(): Promise - +getRole(): UserRole - +startImpersonation(): void - +stopImpersonation(): void - +isImpersonating(): boolean - } - class "ObfuscatedStreamManagerService" as ObfuscatedStreamManager { - -fetchEventSource - -subject: Subject - +openStream(): void - +closeStream(): void - +getChannel(): Observable - } - class "ThresholdPrefetchService" as ThresholdPrefetch { - -timer: Observable - +start(): void - +invalidate(): void - } - class "ThresholdService" as ThresholdService { - -cache: Map - +fetch(): void - +set(): void - +getBounds(): Threshold - } - class "MeasureBoundsEvaluationService" as MeasureBoundsEval { - +isOutOfBounds(value, thresholds): boolean - } - } - - package "Resolvers" as Resolvers { - class "DashboardResolver" as DashboardResolver { - +resolve(): {dataMode: 'clear' | 'obfuscated'} - } - } + ' --- Root / Bootstrap Level --- + package "App Bootstrap" as AppBoot { + note "Global Configuration\n(Router, HTTP, Keycloak)" as BootNote } + ' --- API Level --- package "Generated API Clients" as GeneratedAPI { - class "notip-management-api-openapi" as MgmtApi { - GatewaysService - AdminTenantsService - AdminGatewaysService - AlertsService - AuditLogService - ApiClientService - CommandsService - CostsService - KeysService - ThresholdsService - UsersService - AuthService - } - - class "notip-data-api-openapi" as DataApi { - MeasuresService - SensorsService - AppService - } + package "Mgmt API Client" as MgmtApi + package "Data API Client" as DataApi } - package "Features" as Features { - package "Dashboard" as Dashboard { - class "DataDashboardPageComponent" as DashboardPage { - -dataMode: 'clear' | 'obfuscated' - -viewMode: 'stream' | 'query' - } - class "FilterPanelComponent" as FilterPanel { - -gatewayIds: string[] - -sensorTypes: string[] - -sensorIds: string[] - -from: string - -to: string - } - class "TelemetryChartComponent" as TelemetryChart { - -chart: Chart - -datasets: Dataset[] - } - class "TelemetryTableComponent" as TelemetryTable { - -measures: CheckedEnvelope[] - } - class "DecryptedMeasureService" as DecryptedMeasure { - -cryptoSdk: DataApiService - +query(): Observable - +stream(): Observable - +export(): Observable - } - class "ObfuscatedMeasureService" as ObfuscatedMeasure { - +query(): Observable - +stream(): Observable - } - class "ValidatedMeasureFacadeService" as ValidatedMeasure { - +query(): Observable - +stream(): Observable - } - } - - package "Gateways" as Gateways { - class "GatewayListPageComponent" as GatewayListPage - class "GatewayDetailPageComponent" as GatewayDetailPage { - -gateway: Gateway - -sensors: Sensor[] - -rename modal - -command modal - -delete modal - } - class "GatewayCardComponent" as GatewayCard - class "GatewayActionsComponent" as GatewayActions - class "GatewayRenameModalComponent" as RenameModal - class "CommandModalComponent" as CommandModal { - mode: 'config' | 'firmware' - } - class "GatewayService" as GatewayService { - -listSignal: Signal - -selectedGatewaySignal: Signal - -loadingSignal: Signal - +loadList(): void - +loadDetail(id): void - +updateName(id, name): void - +delete(id): void - } - class "CommandService" as CommandService { - +sendConfigCommand(gwId, freq, status): void - +sendFirmwareCommand(gwId, ver, url): void - -pollCommandStatus(commandId): Observable - } - } - - package "Admin" as Admin { - class "TenantManagerPageComponent" as TenantManagerPage - class "TenantDetailPageComponent" as TenantDetailPage - class "AdminGatewayListPageComponent" as AdminGatewayListPage - class "AdminGatewayFormComponent" as AdminGwForm - class "AdminGatewayTableComponent" as AdminGwTable - class "ImpersonateButtonComponent" as ImpBtn - class "TenantFormComponent" as TenantForm - class "TenantTableComponent" as TenantTable - class "TenantUserListComponent" as TenantUserList - } - - package "Alerts" as Alerts { - class "AlertListPageComponent" as AlertListPage - class "AlertConfigPageComponent" as AlertConfigPage - class "AlertService" as AlertService - class "AlertConfigFormComponent" as AlertConfigForm - class "AlertFilterPanelComponent" as AlertFilterPanel - } - - package "Sensors" as Sensors { - class "SensorListPageComponent" as SensorListPage - class "SensorDetailPageComponent" as SensorDetailPage - } - - package "Management" as Mgmt { - class "UserListPageComponent" as UserListPage - class "UserFormComponent" as UserForm - class "UserTableComponent" as UserTable - class "ThresholdSettingsPageComponent" as ThresholdSettingsPage - class "ThresholdFormComponent" as ThresholdForm - class "ThresholdTableComponent" as ThresholdTable - class "ApiClientListPageComponent" as ApiClientListPage - class "ApiClientTableComponent" as ApiClientTable - class "AuditLogPageComponent" as AuditLogPage - class "AuditFilterPanelComponent" as AuditFilterPanel - class "AuditLogTableComponent" as AuditLogTable - class "CostDashboardPageComponent" as CostDashboardPage - class "CostCardComponent" as CostCard - } + ' --- Core Level (Global State and Security) --- + package "Core Infrastructure" as Core { + package "Services (Auth, Streams, Bounds)" as CoreServices + package "Security (Guards, Interceptors)" as CoreSecurity + package "Models & Enums" as CoreModels } - package "Shared Components" as Shared { - class "SidebarComponent" as Sidebar { - -role-based menu items - -impersonation banner - -profile section - -logout button - } - class "ModalLayerComponent" as ModalLayer { - -backdrop - - - -closeOnBackdropClick - } - class "MultiSelectDropdownComponent" as MultiSelect { - -options: SelectOption[] - -selected: string[] - -searchQuery: string - -filteredOptions: SelectOption[] - } - class "StatusBadgeComponent" as StatusBadge { - -status: string - -cssClass: 'is-good' | 'is-warn' | 'is-bad' | 'is-neutral' - } - class "DeleteConfirmModalComponent" as DeleteConfirmModal - class "ImpersonationBannerComponent" as ImpersonationBanner - class "ImpersonationTagComponent" as ImpersonationTag - class "ProfileSectionComponent" as ProfileSection - class "LogoutButtonComponent" as LogoutButton - class "PlaceholderPageComponent" as PlaceholderPage + ' --- Utility and Shared UI Level --- + package "Shared Components & Utils" as Shared { + package "UI Components (Modals, Badges, Sidebar)" as SharedUI + package "Pipes & Utils (Timezone)" as SharedUtils } - package "Pipes & Utils" as Pipes { - class "RomeTimezoneDatePipe" as RomeDatePipe { - +transform(date: string): string - // Europe/Rome timezone conversion - } - class "rome-timezone.ts" as RomeUtils { - +convertToRomeTimezone(date): string - +formatDateTime(date): string - } + ' --- Features Level (Business Domains) --- + package "Feature Modules" as Features { + package "Dashboard" as FeatDashboard + package "Gateways" as FeatGateways + package "Sensors" as FeatSensors + package "Alerts" as FeatAlerts + package "System Admin" as FeatAdmin + package "Management" as FeatMgmt } - ' Core relationships - AppConfig --> AppRoutes - AppConfig --> AppComponent - AppConfig --> AuthInterceptor - AppConfig --> ErrorInterceptor - AppConfig --> MgmtApi - AppConfig --> DataApi - AppConfig --> AuthService + ' ========================================== + ' RELATIONSHIPS AND DEPENDENCIES BETWEEN PACKAGES + ' ========================================== - AppRoutes --> AuthGuard - AppRoutes --> RoleGuard - AppRoutes --> HomeRedirectGuard - AppRoutes --> DashboardResolver - AppRoutes --> DashboardPage - AppRoutes --> GatewayListPage - AppRoutes --> GatewayDetailPage - AppRoutes --> SensorListPage - AppRoutes --> SensorDetailPage - AppRoutes --> AlertListPage - AppRoutes --> AlertConfigPage - AppRoutes --> UserListPage - AppRoutes --> ThresholdSettingsPage - AppRoutes --> ApiClientListPage - AppRoutes --> AuditLogPage - AppRoutes --> CostDashboardPage - AppRoutes --> TenantManagerPage - AppRoutes --> TenantDetailPage - AppRoutes --> AdminGatewayListPage + ' The App bootstraps the Core and APIs + AppBoot --> CoreSecurity : "Injects" + AppBoot --> GeneratedAPI : "Provides (Provider)" - ' Core services relationships - AuthService --> Enums - AuthService --> Models - AuthGuard --> AuthService - AuthGuard --> ThresholdPrefetch - RoleGuard --> AuthService - HomeRedirectGuard --> AuthService - AuthInterceptor --> AuthService - DashboardResolver --> AuthService + ' The Core defines the base models + CoreServices ..> CoreModels : "Uses" - ObfuscatedStreamManager --> MeasureModel - ThresholdPrefetch --> ThresholdService - ThresholdService --> ThresholdModel - MeasureBoundsEval --> MeasureModel - MeasureBoundsEval --> ThresholdModel + ' Security and Interceptors use Services (e.g., AuthService) + CoreSecurity --> CoreServices : "Verifies Permissions" - ' Generated API relationships - GatewayService --> MgmtApi - AlertService --> MgmtApi - CommandService --> MgmtApi - DecryptedMeasure --> DataApi - DecryptedMeasure --> DataApi - ObfuscatedMeasure --> DataApi - ObfuscatedMeasure --> ObfuscatedStreamManager + ' Core services communicate with generated APIs + CoreServices --> GeneratedAPI : "Manages Token / SSE" - ' Dashboard relationships - DashboardPage --> FilterPanel - DashboardPage --> TelemetryChart - DashboardPage --> TelemetryTable - DashboardPage --> DecryptedMeasure - DashboardPage --> ObfuscatedMeasure - DashboardPage --> ValidatedMeasure - ValidatedMeasure --> DecryptedMeasure - ValidatedMeasure --> MeasureBoundsEval - FilterPanel --> MultiSelect - FilterPanel --> RomeDatePipe + ' Feature modules rely on Core for state and APIs for data + Features --> CoreServices : "Uses global business logic" + Features --> GeneratedAPI : "Executes queries / commands" + Features ..> CoreModels : "Types data" - ' Gateways relationships - GatewayListPage --> GatewayCard - GatewayListPage --> GatewayService - GatewayDetailPage --> GatewayService - GatewayDetailPage --> CommandService - GatewayDetailPage --> RenameModal - GatewayDetailPage --> CommandModal - GatewayDetailPage --> DeleteConfirmModal - GatewayDetailPage --> TelemetryTable - GatewayCard --> StatusBadge - GatewayActions --> GatewayCard - GatewayService --> MgmtApi - CommandService --> MgmtApi + ' Feature modules use shared components and pipes + Features --> SharedUI : "Composes Views" + Features --> SharedUtils : "Formats dates/data" - ' Admin relationships - TenantManagerPage --> MgmtApi - TenantDetailPage --> MgmtApi - TenantDetailPage --> AuthService + ' Cross-dependencies (logical) between Features (optional for context) + FeatDashboard ..> FeatGateways : "Uses data for filters" + FeatDashboard ..> FeatSensors : "Uses data for filters" - ' Shared relationships - Sidebar --> ImpersonationBanner - Sidebar --> ImpersonationTag - Sidebar --> ProfileSection - Sidebar --> LogoutButton - ModalLayer --> DeleteConfirmModal - ModalLayer --> RenameModal - ModalLayer --> CommandModal - MultiSelect --> FilterPanel } -note top of AppConfig - Application bootstrap configuration - Registers Keycloak with PKCE S256, - AutoRefreshTokenService, UserActivityService, - HTTP client, router, OpenAPI providers. - Session timeout: 10 minutes. - No environment.ts files used. -end note - -note top of AuthService - Central authentication service - Implements SessionLifeCycle and - ImpersonationStatus interfaces - Manages Keycloak lifecycle -end note - -note top of ValidatedMeasure - Facade that wraps DecryptedMeasure - and adds bounds checking via - MeasureBoundsEvaluationService +note right of Core + The "Core" contains the app's Singleton logic: + Authentication (Keycloak), Impersonation, + SSE (Server-Sent Events) streams, and global + Threshold management. end note -note top of ObfuscatedStreamManager - Manages SSE connection to data-api - Uses @microsoft/fetch-event-source - Emits telemetry envelopes +note left of Features + "Features" are highly cohesive modules. + They contain the page logic (Smart Components) + and domain-specific services that orchestrate + calls to the generated APIs. end note -@enduml +@enduml \ No newline at end of file diff --git a/docs/13-pb/docest/specifica_tecnica_frontend/assets/01-app-architecture.svg b/docs/13-pb/docest/specifica_tecnica_frontend/assets/01-app-architecture.svg index 1d6bb8a1..4aebb6f9 100644 --- a/docs/13-pb/docest/specifica_tecnica_frontend/assets/01-app-architecture.svg +++ b/docs/13-pb/docest/specifica_tecnica_frontend/assets/01-app-architecture.svg @@ -1 +1 @@ -NOTIP Frontend - Application ArchitectureNOTIP Frontend - Application ArchitectureNOTIP Frontend (Angular 21 Standalone)Core InfrastructureGuardsInterceptorsModelsServicesResolversGenerated API ClientsFeaturesDashboardGatewaysAdminAlertsSensorsManagementShared ComponentsPipes & Utilsapp.config.tsAutoRefreshTokenServiceUserActivityServiceprovideKeycloak()provideHttpClient()provideRouter()provideMgmtApi('/api/mgmt')provideDataApi('/api/data')app.routes.tsroutes: Routes[]AppComponent<router-outlet>AuthGuardcanActivate(): Promise<boolean>RoleGuardcanActivate(): boolean | UrlTreeHomeRedirectGuardcanActivate(): UrlTreeauthInterceptorHttpInterceptorFnerrorInterceptorHttpInterceptorFnenums.tsUserRoleTenantStatusGatewayStatusCommandStatusAlertsTypegateway.tsGatewayObfuscatedGatewayAddGatewayParametersmeasure.tsTelemetryEnvelopeDecryptedEnvelopeCheckedEnvelopeObfuscatedEnvelopetenant.tsTenantCreateTenantParametersuser.tsViewUserCreatedUserUserParametersalert.tsAlertsAlertsConfigAlertsFilterthreshold.tsThresholdSensorThresholdTypeThresholdAuthServicekeycloak: KeycloakServiceinit(): Promise<boolean>login(): voidlogout(): voidgetToken(): Promise<string>getRole(): UserRolestartImpersonation(): voidstopImpersonation(): voidisImpersonating(): booleanObfuscatedStreamManagerServicefetchEventSourcesubject: Subject<ObfuscatedEnvelope>openStream(): voidcloseStream(): voidgetChannel(): ObservableThresholdPrefetchServicetimer: Observablestart(): voidinvalidate(): voidThresholdServicecache: Mapfetch(): voidset(): voidgetBounds(): ThresholdMeasureBoundsEvaluationServiceisOutOfBounds(value, thresholds): booleanDashboardResolverresolve(): {dataMode: 'clear' | 'obfuscated'}notip-management-api-openapiGatewaysServiceAdminTenantsServiceAdminGatewaysServiceAlertsServiceAuditLogServiceApiClientServiceCommandsServiceCostsServiceKeysServiceThresholdsServiceUsersServiceAuthServicenotip-data-api-openapiMeasuresServiceSensorsServiceAppServiceDataDashboardPageComponentdataMode: 'clear' | 'obfuscated'viewMode: 'stream' | 'query'FilterPanelComponentgatewayIds: string[]sensorTypes: string[]sensorIds: string[]from: stringto: stringTelemetryChartComponentchart: Chartdatasets: Dataset[]TelemetryTableComponentmeasures: CheckedEnvelope[]DecryptedMeasureServicecryptoSdk: DataApiServicequery(): Observable<CheckedQueryResPage>stream(): Observable<CheckedEnvelope>export(): Observable<Blob>ObfuscatedMeasureServicequery(): Observable<ObfuscatedQueryResPage>stream(): Observable<ObfuscatedEnvelope>ValidatedMeasureFacadeServicequery(): Observable<CheckedQueryResPage>stream(): Observable<CheckedEnvelope>GatewayListPageComponentGatewayDetailPageComponentgateway: Gatewaysensors: Sensor[]rename modalcommand modaldelete modalGatewayCardComponentGatewayActionsComponentGatewayRenameModalComponentCommandModalComponentmode: 'config' | 'firmware'GatewayServicelistSignal: Signal<Gateway[]>selectedGatewaySignal: Signal<Gateway>loadingSignal: Signal<boolean>loadList(): voidloadDetail(id): voidupdateName(id, name): voiddelete(id): voidCommandServicesendConfigCommand(gwId, freq, status): voidsendFirmwareCommand(gwId, ver, url): voidpollCommandStatus(commandId): ObservableTenantManagerPageComponentTenantDetailPageComponentAdminGatewayListPageComponentAdminGatewayFormComponentAdminGatewayTableComponentImpersonateButtonComponentTenantFormComponentTenantTableComponentTenantUserListComponentAlertListPageComponentAlertConfigPageComponentAlertServiceAlertConfigFormComponentAlertFilterPanelComponentSensorListPageComponentSensorDetailPageComponentUserListPageComponentUserFormComponentUserTableComponentThresholdSettingsPageComponentThresholdFormComponentThresholdTableComponentApiClientListPageComponentApiClientTableComponentAuditLogPageComponentAuditFilterPanelComponentAuditLogTableComponentCostDashboardPageComponentCostCardComponentSidebarComponentrole-based menu itemsimpersonation bannerprofile sectionlogout buttonModalLayerComponentbackdrop<ng-content>closeOnBackdropClickMultiSelectDropdownComponentoptions: SelectOption[]selected: string[]searchQuery: stringfilteredOptions: SelectOption[]StatusBadgeComponentstatus: stringcssClass: 'is-good' | 'is-warn' | 'is-bad' | 'is-neutral'DeleteConfirmModalComponentImpersonationBannerComponentImpersonationTagComponentProfileSectionComponentLogoutButtonComponentPlaceholderPageComponentRomeTimezoneDatePipe// Europe/Rome timezone conversiontransform(date: string): stringrome-timezone.tsconvertToRomeTimezone(date): stringformatDateTime(date): stringApplication bootstrap configurationRegisters Keycloak with PKCE S256,AutoRefreshTokenService, UserActivityService,HTTP client, router, OpenAPI providers.Session timeout: 10 minutes.No environment.ts files used.Central authentication serviceImplements SessionLifeCycle andImpersonationStatus interfacesManages Keycloak lifecycleFacade that wraps DecryptedMeasureand adds bounds checking viaMeasureBoundsEvaluationServiceManages SSE connection to data-apiUses @microsoft/fetch-event-sourceEmits telemetry envelopes \ No newline at end of file +NoTIP Frontend - Package Dependencies ArchitectureNoTIP Frontend - Package Dependencies ArchitectureNoTIP Frontend (Angular 21)App BootstrapGenerated API ClientsCore InfrastructureShared Components & UtilsFeature ModulesGlobal Configuration(Router, HTTP, Keycloak)MgmtApiMgmt API ClientDataApiData API ClientCoreServicesServices (Auth, Streams, Bounds)CoreSecuritySecurity (Guards, Interceptors)CoreModelsModels & EnumsSharedUIUI Components (Modals, Badges, Sidebar)SharedUtilsPipes & Utils (Timezone)FeatDashboardDashboardFeatGatewaysGatewaysFeatSensorsSensorsFeatAlertsAlertsFeatAdminSystem AdminFeatMgmtManagementThe "Core" contains the app's Singleton logic:Authentication (Keycloak), Impersonation,SSE (Server-Sent Events) streams, and globalThreshold management."Features" are highly cohesive modules.They contain the page logic (Smart Components)and domain-specific services that orchestratecalls to the generated APIs.InjectsProvides (Provider)UsesVerifies PermissionsManages Token / SSEUses global business logicExecutes queries / commandsTypes dataComposes ViewsFormats dates/dataUses data for filtersUses data for filters \ No newline at end of file diff --git a/docs/13-pb/docest/specifica_tecnica_frontend/assets/02-management.puml b/docs/13-pb/docest/specifica_tecnica_frontend/assets/02-management.puml new file mode 100644 index 00000000..fc093273 --- /dev/null +++ b/docs/13-pb/docest/specifica_tecnica_frontend/assets/02-management.puml @@ -0,0 +1,182 @@ +@startuml +title NoTIP Frontend - Management Architecture (Accurate) + +skinparam backgroundColor #FFFFFF +skinparam defaultFontSize 10 +skinparam defaultFontColor #000000 +skinparam nodesep 8 +skinparam ranksep 8 +skinparam linetype ortho +skinparam classAttributeIconSize 0 + +package "Management Feature" <> { + + package "Users Management" { + class "UserListPageComponent" as UserListPage { + -- + +users: Signal + +showCreateForm: Signal + +isLoading: Signal + +isSaving: Signal + +editingUserId: Signal + +deletingUserId: Signal + +editingUser: Computed + -- + +ngOnInit(): void + +requestEdit(userId: string): void + +openCreateUserForm(): void + +requestDelete(userId: string): void + +cancelDelete(): void + +confirmDelete(): void + +createUser(payload: CreateUserPayload): void + +updateUser(payload: UpdateUserPayload): void + +cancelEdit(): void + -loadUsers(): void + } + + class "UserService" as UserService { + -- + +getUsers(): Observable + +createUser(payload): Observable + +updateUser(id, payload): Observable + +deleteUsers(ids: string[]): Observable<{deleted: number}> + } + } + + package "Thresholds" { + class "ThresholdSettingsPageComponent" as ThresholdPage { + -- + +thresholds: Signal + +sensorTypeOptions: Signal + +sensorIdOptions: Signal + +showForm: Signal + +deletingThreshold: Signal + +isLoading: Signal + +isSaving: Signal + +canEditThresholds: boolean + -- + +ngOnInit(): void + +toggleForm(): void + +closeForm(): void + +saveTypeThreshold(payload: TypeThresholdPayload): void + +saveSensorThreshold(payload: SensorThresholdPayload): void + +deleteThreshold(payload: ThresholdDeletePayload): void + +cancelDeleteThreshold(): void + +confirmDeleteThreshold(): void + -loadThresholds(): void + -loadSensorOptions(): void + } + + class "ThresholdService" as ThresholdService { + -- + +fetchThresholds(): Observable + +setDefaultThreshold(type, min, max): Observable + +setSensorThreshold(id, type, min, max): Observable + +deleteSensorThreshold(id): Observable + +deleteTypeThreshold(type): Observable + } + } + + package "Audit Logs" { + class "AuditLogPageComponent" as AuditPage { + -- + +logs: Signal + +userIdOptions: Signal + +actionOptions: Signal + +from: Signal + +to: Signal + +userIds: Signal + +actions: Signal + +isLoading: Signal + +isExporting: Signal + -- + +ngOnInit(): void + +applyFilters(form: AuditFilterFormValue): void + +resetFilters(): void + +canExportLogs(): boolean + +onExportLogs(): void + -loadLogs(): void + -refreshFilterOptions(rows: Logs[]): void + -downloadAsCsv(rows: Logs[]): void + } + + class "AuditService" as AuditService { + -- + +getLogs(filters): Observable + } + } + + package "Costs" { + class "CostDashboardPageComponent" as CostPage { + -- + +costs: Signal + +isLoading: Signal + -- + +ngOnInit(): void + -loadCosts(): void + } + + class "CostsService" as CostsService { + -- + +getTenantCosts(): Observable + } + } + + package "API Clients" { + class "ApiClientListPageComponent" as ApiClientPage { + -- + +clients: Signal + +lastCreated: Signal + +showCreateForm: Signal + +isLoading: Signal + +isSaving: Signal + +deletingClientId: Signal + -- + +ngOnInit(): void + +toggleCreateForm(): void + +closeCreateForm(): void + +submitCreateForm(event, input): void + +createClient(name: string, input?): void + +requestDelete(clientId: string): void + +cancelDelete(): void + +confirmDelete(): void + -loadClients(): void + -resolveCreateClientErrorMessage(error): string + } + + class "ClientsService" as ClientService { + -- + +getClients(): Observable + +createClient(name: string): Observable + +deleteClient(id: string): Observable + } + } + + ' Relationships + UserListPage --> UserService + ThresholdPage --> ThresholdService + AuditPage --> AuditService + CostPage --> CostsService + ApiClientPage --> ClientService +} + +note top of UserListPage + Handles creation, update, and deletion of users. + Uses computed signal 'editingUser' to + determine if the form is in edit or create mode. +end note + +note right of ThresholdPage + Manages thresholds via distinct methods + for Type (global) and Sensor (specific). + Checks 'canEditThresholds' via AuthService + to prevent unauthorized modifications. +end note + +note bottom of AuditPage + Downloads logs directly in CSV format. + Extracts unique User IDs and Actions from + the fetched logs to populate the filter dropdowns. +end note + +@enduml \ No newline at end of file diff --git a/docs/13-pb/docest/specifica_tecnica_frontend/assets/02-management.svg b/docs/13-pb/docest/specifica_tecnica_frontend/assets/02-management.svg new file mode 100644 index 00000000..5a0c0e60 --- /dev/null +++ b/docs/13-pb/docest/specifica_tecnica_frontend/assets/02-management.svg @@ -0,0 +1 @@ +NoTIP Frontend - Management Architecture (Accurate)NoTIP Frontend - Management Architecture (Accurate)Management FeatureUsers ManagementThresholdsAudit LogsCostsAPI ClientsUserListPageComponent+users: Signal<ViewUser[]>+showCreateForm: Signal<boolean>+isLoading: Signal<boolean>+isSaving: Signal<boolean>+editingUserId: Signal<string | null>+deletingUserId: Signal<string | null>+editingUser: Computed<ViewUser | null>+ngOnInit(): void+requestEdit(userId: string): void+openCreateUserForm(): void+requestDelete(userId: string): void+cancelDelete(): void+confirmDelete(): void+createUser(payload: CreateUserPayload): void+updateUser(payload: UpdateUserPayload): void+cancelEdit(): void-loadUsers(): voidUserService+getUsers(): Observable<ViewUser[]>+createUser(payload): Observable<CreatedUser>+updateUser(id, payload): Observable<ViewUser>+deleteUsers(ids: string[]): Observable<{deleted: number}>ThresholdSettingsPageComponent+thresholds: Signal<ThresholdConfig[]>+sensorTypeOptions: Signal<string[]>+sensorIdOptions: Signal<string[]>+showForm: Signal<boolean>+deletingThreshold: Signal<ThresholdDeletePayload | null>+isLoading: Signal<boolean>+isSaving: Signal<boolean>+canEditThresholds: boolean+ngOnInit(): void+toggleForm(): void+closeForm(): void+saveTypeThreshold(payload: TypeThresholdPayload): void+saveSensorThreshold(payload: SensorThresholdPayload): void+deleteThreshold(payload: ThresholdDeletePayload): void+cancelDeleteThreshold(): void+confirmDeleteThreshold(): void-loadThresholds(): void-loadSensorOptions(): voidThresholdService+fetchThresholds(): Observable<ThresholdConfig[]>+setDefaultThreshold(type, min, max): Observable<void>+setSensorThreshold(id, type, min, max): Observable<void>+deleteSensorThreshold(id): Observable<void>+deleteTypeThreshold(type): Observable<void>AuditLogPageComponent+logs: Signal<Logs[]>+userIdOptions: Signal<string[]>+actionOptions: Signal<string[]>+from: Signal<string>+to: Signal<string>+userIds: Signal<string[]>+actions: Signal<string[]>+isLoading: Signal<boolean>+isExporting: Signal<boolean>+ngOnInit(): void+applyFilters(form: AuditFilterFormValue): void+resetFilters(): void+canExportLogs(): boolean+onExportLogs(): void-loadLogs(): void-refreshFilterOptions(rows: Logs[]): void-downloadAsCsv(rows: Logs[]): voidAuditService+getLogs(filters): Observable<Logs[]>CostDashboardPageComponent+costs: Signal<Costs | null>+isLoading: Signal<boolean>+ngOnInit(): void-loadCosts(): voidCostsService+getTenantCosts(): Observable<Costs>ApiClientListPageComponent+clients: Signal<SecretClient[]>+lastCreated: Signal<SecretClient | null>+showCreateForm: Signal<boolean>+isLoading: Signal<boolean>+isSaving: Signal<boolean>+deletingClientId: Signal<string | null>+ngOnInit(): void+toggleCreateForm(): void+closeCreateForm(): void+submitCreateForm(event, input): void+createClient(name: string, input?): void+requestDelete(clientId: string): void+cancelDelete(): void+confirmDelete(): void-loadClients(): void-resolveCreateClientErrorMessage(error): stringClientsService+getClients(): Observable<SecretClient[]>+createClient(name: string): Observable<SecretClient>+deleteClient(id: string): Observable<void>Handles creation, update, and deletion of users.Uses computed signal 'editingUser' todetermine if the form is in edit or create mode.Manages thresholds via distinct methodsfor Type (global) and Sensor (specific).Checks 'canEditThresholds' via AuthServiceto prevent unauthorized modifications.Downloads logs directly in CSV format.Extracts unique User IDs and Actions fromthe fetched logs to populate the filter dropdowns. \ No newline at end of file diff --git a/docs/13-pb/docest/specifica_tecnica_frontend/assets/03-admin.puml b/docs/13-pb/docest/specifica_tecnica_frontend/assets/03-admin.puml new file mode 100644 index 00000000..f2fea4bf --- /dev/null +++ b/docs/13-pb/docest/specifica_tecnica_frontend/assets/03-admin.puml @@ -0,0 +1,211 @@ +@startuml +title NoTIP Frontend - Admin Architecture + +skinparam backgroundColor #FFFFFF +skinparam defaultFontSize 10 +skinparam defaultFontColor #000000 +skinparam nodesep 8 +skinparam ranksep 8 +skinparam linetype ortho +skinparam classAttributeIconSize 0 +left to right direction + +package "Admin" <> { + class "TenantManagerPageComponent" as TenantManagerPage { + -- + +tenants: Signal + +showCreateForm: Signal + +isLoading: Signal + +isSaving: Signal + +editingTenantId: Signal + +deletingTenantId: Signal + +editingTenant: Computed + -- + +ngOnInit(): void + +requestEdit(tenantId: string): void + +openCreateForm(): void + +requestDelete(tenantId: string): void + +cancelDelete(): void + +confirmDelete(): void + +saveTenant(payload: CreateTenantParameters): void + +cancelEdit(): void + -loadTenants(): void + } + + class "TenantDetailPageComponent" as TenantDetailPage { + -- + +tenant: Signal + +isLoading: Signal + +activeTab: Signal<'users' | 'gateways'> + -- + +ngOnInit(): void + +setActiveTab(tab: 'users' | 'gateways'): void + -loadTenant(id: string): void + } + + class "AdminGatewayListPageComponent" as AdminGatewayListPage { + -- + +tenantId: InputSignal + +gateways: Signal + +isLoading: Signal + +showCreateForm: Signal + +isSaving: Signal + +editingGatewayId: Signal + +deletingGatewayId: Signal + +editingGateway: Computed + -- + +ngOnInit(): void + +ngOnChanges(): void + +requestEdit(gatewayId: string): void + +openCreateForm(): void + +requestDelete(gatewayId: string): void + +cancelDelete(): void + +confirmDelete(): void + +saveGateway(payload: AddGatewayParameters): void + +cancelEdit(): void + -loadGateways(): void + } + + class "TenantTableComponent" as TenantTable { + -- + +tenants: InputSignal + -- + +editRequest: OutputEmitterRef + +deleteRequest: OutputEmitterRef + +getStatusBadgeClass(status: string): string + } + + class "TenantFormComponent" as TenantForm { + -- + +tenant: InputSignal + +isSubmitting: InputSignal + -- + +save: OutputEmitterRef + +cancel: OutputEmitterRef + +onSubmit(): void + } + + class "TenantUserListComponent" as TenantUserList { + -- + +tenantId: InputSignal + +users: Signal + +isLoading: Signal + -- + +ngOnInit(): void + +ngOnChanges(): void + -loadUsers(): void + } + + class "ImpersonateButtonComponent" as ImpersonateButton { + -- + +targetUserId: InputSignal + +disabled: InputSignal + +isImpersonating: Computed + +isActionLoading: Signal + -- + +onImpersonateClick(): void + } + + class "AdminGatewayTableComponent" as AdminGatewayTable { + -- + +gateways: InputSignal + +tenantId: InputSignal + -- + +editRequest: OutputEmitterRef + +deleteRequest: OutputEmitterRef + +getStatusBadgeClass(status: string): string + } + + class "AdminGatewayFormComponent" as AdminGatewayForm { + -- + +gateway: InputSignal + +isSubmitting: InputSignal + -- + +save: OutputEmitterRef + +cancel: OutputEmitterRef + +onSubmit(): void + } + + package "Services" as Services { + class "TenantService" as TenantService { + -- + -mgmtApi: AdminTenantsService + -- + +getTenants(): Observable + +getTenant(id: string): Observable + +createTenant(payload: CreateTenantParameters): Observable + +updateTenant(id: string, payload: CreateTenantParameters): Observable + +deleteTenant(id: string): Observable + } + + class "AdminUserService" as AdminUserService { + -- + -mgmtApi: AdminTenantsService + -- + +getTenantUsers(tenantId: string): Observable + } + + class "AdminGatewayService" as AdminGatewayService { + -- + -mgmtApi: AdminGatewaysService + -- + +getGateways(tenantId: string): Observable + +addGateway(tenantId: string, payload: AddGatewayParameters): Observable + +updateGateway(gatewayId: string, payload: AddGatewayParameters): Observable + +deleteGateway(gatewayId: string): Observable + } + } + + ' Core external dependencies (Mocked for context) + class "AuthService (Core)" as AuthService + + ' Relationships - Tenant Manager + TenantManagerPage --> TenantTable + TenantManagerPage --> TenantForm + TenantManagerPage --> TenantService + + ' Relationships - Tenant Detail + TenantDetailPage --> TenantService + TenantDetailPage --> TenantUserList + TenantDetailPage --> AdminGatewayListPage + + ' Relationships - Subcomponents of Detail + TenantUserList --> AdminUserService + TenantUserList --> ImpersonateButton + ImpersonateButton --> AuthService : Injects AuthService\nto trigger startImpersonation() + + ' Relationships - Admin Gateways + AdminGatewayListPage --> AdminGatewayTable + AdminGatewayListPage --> AdminGatewayForm + AdminGatewayListPage --> AdminGatewayService +} + +note top of TenantManagerPage + Entry point for System Admins. + Handles CRUD operations for Tenants. + Relies on computed 'editingTenant' to switch + the TenantFormComponent mode. +end note + +note right of TenantDetailPage + Displays specific Tenant details. + Manages UI state via tabs to show + either the list of Users or the + list of Admin Gateways assigned. +end note + +note bottom of ImpersonateButton + Allows System Admins to impersonate + Tenant Users. Disables itself and shows + loading state while AuthService handles + the management-api PKCE exchange. +end note + +note bottom of AdminGatewayListPage + Embedded page component used within + TenantDetail via InputSignal 'tenantId'. + Handles the creation, assignment, and + deletion of gateways for a specific tenant. +end note + +@enduml \ No newline at end of file diff --git a/docs/13-pb/docest/specifica_tecnica_frontend/assets/03-admin.svg b/docs/13-pb/docest/specifica_tecnica_frontend/assets/03-admin.svg new file mode 100644 index 00000000..97e22570 --- /dev/null +++ b/docs/13-pb/docest/specifica_tecnica_frontend/assets/03-admin.svg @@ -0,0 +1 @@ +NoTIP Frontend - Admin ArchitectureNoTIP Frontend - Admin ArchitectureAdminServicesTenantManagerPageComponent+tenants: Signal<Tenant[]>+showCreateForm: Signal<boolean>+isLoading: Signal<boolean>+isSaving: Signal<boolean>+editingTenantId: Signal<string | null>+deletingTenantId: Signal<string | null>+editingTenant: Computed<Tenant | null>+ngOnInit(): void+requestEdit(tenantId: string): void+openCreateForm(): void+requestDelete(tenantId: string): void+cancelDelete(): void+confirmDelete(): void+saveTenant(payload: CreateTenantParameters): void+cancelEdit(): void-loadTenants(): voidTenantDetailPageComponent+tenant: Signal<Tenant | null>+isLoading: Signal<boolean>+activeTab: Signal<'users' | 'gateways'>+ngOnInit(): void+setActiveTab(tab: 'users' | 'gateways'): void-loadTenant(id: string): voidAdminGatewayListPageComponent+tenantId: InputSignal<string>+gateways: Signal<ObfuscatedGateway[]>+isLoading: Signal<boolean>+showCreateForm: Signal<boolean>+isSaving: Signal<boolean>+editingGatewayId: Signal<string | null>+deletingGatewayId: Signal<string | null>+editingGateway: Computed<ObfuscatedGateway | null>+ngOnInit(): void+ngOnChanges(): void+requestEdit(gatewayId: string): void+openCreateForm(): void+requestDelete(gatewayId: string): void+cancelDelete(): void+confirmDelete(): void+saveGateway(payload: AddGatewayParameters): void+cancelEdit(): void-loadGateways(): voidTenantTableComponent+tenants: InputSignal<Tenant[]>+editRequest: OutputEmitterRef<string>+deleteRequest: OutputEmitterRef<string>+getStatusBadgeClass(status: string): stringTenantFormComponent+tenant: InputSignal<Tenant | null>+isSubmitting: InputSignal<boolean>+save: OutputEmitterRef<CreateTenantParameters>+cancel: OutputEmitterRef<void>+onSubmit(): voidTenantUserListComponent+tenantId: InputSignal<string>+users: Signal<ViewUser[]>+isLoading: Signal<boolean>+ngOnInit(): void+ngOnChanges(): void-loadUsers(): voidImpersonateButtonComponent+targetUserId: InputSignal<string>+disabled: InputSignal<boolean>+isImpersonating: Computed<boolean>+isActionLoading: Signal<boolean>+onImpersonateClick(): voidAdminGatewayTableComponent+gateways: InputSignal<ObfuscatedGateway[]>+tenantId: InputSignal<string>+editRequest: OutputEmitterRef<string>+deleteRequest: OutputEmitterRef<string>+getStatusBadgeClass(status: string): stringAdminGatewayFormComponent+gateway: InputSignal<ObfuscatedGateway | null>+isSubmitting: InputSignal<boolean>+save: OutputEmitterRef<AddGatewayParameters>+cancel: OutputEmitterRef<void>+onSubmit(): voidAuthService (Core)TenantService-mgmtApi: AdminTenantsService+getTenants(): Observable<Tenant[]>+getTenant(id: string): Observable<Tenant>+createTenant(payload: CreateTenantParameters): Observable<Tenant>+updateTenant(id: string, payload: CreateTenantParameters): Observable<Tenant>+deleteTenant(id: string): Observable<void>AdminUserService-mgmtApi: AdminTenantsService+getTenantUsers(tenantId: string): Observable<ViewUser[]>AdminGatewayService-mgmtApi: AdminGatewaysService+getGateways(tenantId: string): Observable<ObfuscatedGateway[]>+addGateway(tenantId: string, payload: AddGatewayParameters): Observable<ObfuscatedGateway>+updateGateway(gatewayId: string, payload: AddGatewayParameters): Observable<ObfuscatedGateway>+deleteGateway(gatewayId: string): Observable<void>Entry point for System Admins.Handles CRUD operations for Tenants.Relies on computed 'editingTenant' to switchthe TenantFormComponent mode.Displays specific Tenant details.Manages UI state via tabs to showeither the list of Users or thelist of Admin Gateways assigned.Allows System Admins to impersonateTenant Users. Disables itself and showsloading state while AuthService handlesthe management-api PKCE exchange.Embedded page component used withinTenantDetail via InputSignal 'tenantId'.Handles the creation, assignment, anddeletion of gateways for a specific tenant.Injects AuthServiceto trigger startImpersonation() \ No newline at end of file diff --git a/docs/13-pb/docest/specifica_tecnica_frontend/assets/04-alerts.puml b/docs/13-pb/docest/specifica_tecnica_frontend/assets/04-alerts.puml new file mode 100644 index 00000000..3baf9514 --- /dev/null +++ b/docs/13-pb/docest/specifica_tecnica_frontend/assets/04-alerts.puml @@ -0,0 +1,164 @@ +@startuml +title NoTIP Frontend - Alerts Architecture + +skinparam backgroundColor #FFFFFF +skinparam defaultFontSize 10 +skinparam defaultFontColor #000000 +skinparam nodesep 8 +skinparam ranksep 8 +skinparam linetype ortho +skinparam classAttributeIconSize 0 + +package "Alerts Feature" <> { + + class "AlertListPageComponent" as AlertListPage { + -- + +alerts: Signal + +gatewayOptions: Signal + +isLoading: Signal + +errorMessage: Signal + +canEditAlertsConfig: boolean + +from: Signal + +to: Signal + +gatewayIds: Signal + -- + +ngOnInit(): void + +applyFilter(form: AlertFilterFormValue): void + +resetFilter(): void + -loadAlerts(): void + -loadGatewayOptions(): void + -normalizeList(values: string[]): string[] + -toIsoOrNow(value: string): string + -toDatetimeLocal(value: Date): string + } + + class "AlertConfigPageComponent" as AlertConfigPage { + -- + +config: Signal + +availableGatewayIds: Signal + +showForm: Signal + +isLoading: Signal + +isSaving: Signal + +errorMessage: Signal + +infoMessage: Signal + +canEditConfig: boolean + -- + +ngOnInit(): void + +toggleForm(): void + +closeForm(): void + +saveDefault(payload: DefaultTimeoutPayload): void + +saveGateway(payload: GatewayTimeoutPayload): void + +deleteGateway(gatewayId: string): void + -loadConfig(): void + -loadAvailableGatewayIds(): void + } + + class "AlertFilterPanelComponent" as AlertFilterPanel { + -- + +from: InputSignal + +to: InputSignal + +gatewayIds: InputSignal + +gatewayOptions: InputSignal + +isLoading: InputSignal + +selectedGatewayIds: Signal + -- + +filterSubmitted: OutputEmitterRef + +resetRequested: OutputEmitterRef + -- + <> (uses effect) + +submit(event: Event, from: string, to: string): void + +onGatewaySelectionChanged(gatewayIds: string[]): void + +reset(): void + -normalizeList(values: string[]): string[] + } + + class "AlertConfigFormComponent" as AlertConfigForm { + -- + +defaultTimeoutMs: InputSignal + +gatewayOverrides: InputSignal + +availableGatewayIds: InputSignal + +isSaving: InputSignal + +selectedGatewayCurrentTimeoutMs: Signal + -- + +defaultSubmitted: OutputEmitterRef + +gatewaySubmitted: OutputEmitterRef + +gatewayDeleteRequested: OutputEmitterRef + +cancelRequested: OutputEmitterRef + -- + +submitDefault(event: Event, timeoutRaw: string): void + +submitGateway(event: Event, gatewayId: string, timeoutRaw: string): void + +onGatewaySelectionChanged(gatewayId: string, timeoutInput: HTMLInputElement): void + +deleteGatewayOverride(gatewayId: string): void + +cancel(): void + -resolveCurrentTimeout(gatewayId: string): number + } + + package "Alerts Services" as Services { + class "AlertService" as AlertService { + -- + -alertsApi: AlertsApiService + -- + +getAlertsConfig(): Observable + +setDefaultConfig(timeoutMs: number): Observable + +sendGatewayConfig(gatewayId: string, timeoutMs: number): Observable + +getAlerts(af: AlertsFilter): Observable + +deleteGatewayConfig(gatewayId: string): Observable + -fetchAlerts(from: string, to: string, gatewayId?: string): Observable + -normalizeGatewayIds(values?: string[]): string[] + -mergeAlerts(rows: Alerts[]): Alerts[] + -toAlerts(rows: Record[]): Alerts[] + -toAlertsType(value: unknown): AlertsType + -asRecord(value: unknown): Record + -asNumber(value: unknown, fallback: number): number + -asString(value: unknown): string + -toAlertDetails(details: unknown, message: unknown): AlertDetails + } + } + + ' Internal Feature Relationships + AlertListPage --> AlertFilterPanel + AlertListPage --> AlertService + + AlertConfigPage --> AlertConfigForm + AlertConfigPage --> AlertService +} + +package "External Dependencies" <> { + class "GatewayService" as GatewayService + class "AuthService" as AuthService + class "MultiSelectDropdownComponent" as MultiSelectDropdown + class "ModalLayerComponent" as ModalLayer +} + +' External Links +AlertListPage --> GatewayService +AlertListPage --> AuthService + +AlertConfigPage --> GatewayService +AlertConfigPage --> AuthService +AlertConfigPage --> ModalLayer + +AlertFilterPanel --> MultiSelectDropdown + +note top of AlertListPage + Main view for displaying offline gateway alerts. + Manages date range and multi-gateway filtering. + Uses Rome timezone utilities for input values. +end note + +note left of AlertConfigPage + Manages default and per-gateway timeout configs. + Uses `canEditConfig` based on the user's role + (tenant_admin) to guard UI interaction. +end note + +note bottom of AlertService + Handles API communication for Alerts and Configs. + If multiple gateways are selected in getAlerts(), + it uses forkJoin to fetch them in parallel and + merges/sorts the results via mergeAlerts(). + Contains robust JSON parsing logic for parsing + both CamelCase and SnakeCase AlertDetails. +end note + +@enduml \ No newline at end of file diff --git a/docs/13-pb/docest/specifica_tecnica_frontend/assets/04-alerts.svg b/docs/13-pb/docest/specifica_tecnica_frontend/assets/04-alerts.svg new file mode 100644 index 00000000..d43956c2 --- /dev/null +++ b/docs/13-pb/docest/specifica_tecnica_frontend/assets/04-alerts.svg @@ -0,0 +1 @@ +NoTIP Frontend - Alerts ArchitectureNoTIP Frontend - Alerts ArchitectureAlerts FeatureAlerts ServicesExternal DependenciesAlertListPageComponent+alerts: Signal<Alerts[]>+gatewayOptions: Signal<string[]>+isLoading: Signal<boolean>+errorMessage: Signal<string | null>+canEditAlertsConfig: boolean+from: Signal<string>+to: Signal<string>+gatewayIds: Signal<string[]>+ngOnInit(): void+applyFilter(form: AlertFilterFormValue): void+resetFilter(): void-loadAlerts(): void-loadGatewayOptions(): void-normalizeList(values: string[]): string[]-toIsoOrNow(value: string): string-toDatetimeLocal(value: Date): stringAlertConfigPageComponent+config: Signal<AlertsConfig | null>+availableGatewayIds: Signal<string[]>+showForm: Signal<boolean>+isLoading: Signal<boolean>+isSaving: Signal<boolean>+errorMessage: Signal<string | null>+infoMessage: Signal<string | null>+canEditConfig: boolean+ngOnInit(): void+toggleForm(): void+closeForm(): void+saveDefault(payload: DefaultTimeoutPayload): void+saveGateway(payload: GatewayTimeoutPayload): void+deleteGateway(gatewayId: string): void-loadConfig(): void-loadAvailableGatewayIds(): voidAlertFilterPanelComponent+from: InputSignal<string>+to: InputSignal<string>+gatewayIds: InputSignal<string[]>+gatewayOptions: InputSignal<string[]>+isLoading: InputSignal<boolean>+selectedGatewayIds: Signal<string[]>+filterSubmitted: OutputEmitterRef<AlertFilterFormValue>+resetRequested: OutputEmitterRef<void>«constructor» (uses effect)+submit(event: Event, from: string, to: string): void+onGatewaySelectionChanged(gatewayIds: string[]): void+reset(): void-normalizeList(values: string[]): string[]AlertConfigFormComponent+defaultTimeoutMs: InputSignal<number>+gatewayOverrides: InputSignal<GatewayOverride[]>+availableGatewayIds: InputSignal<string[]>+isSaving: InputSignal<boolean>+selectedGatewayCurrentTimeoutMs: Signal<number | null>+defaultSubmitted: OutputEmitterRef<DefaultTimeoutPayload>+gatewaySubmitted: OutputEmitterRef<GatewayTimeoutPayload>+gatewayDeleteRequested: OutputEmitterRef<string>+cancelRequested: OutputEmitterRef<void>+submitDefault(event: Event, timeoutRaw: string): void+submitGateway(event: Event, gatewayId: string, timeoutRaw: string): void+onGatewaySelectionChanged(gatewayId: string, timeoutInput: HTMLInputElement): void+deleteGatewayOverride(gatewayId: string): void+cancel(): void-resolveCurrentTimeout(gatewayId: string): numberAlertService-alertsApi: AlertsApiService+getAlertsConfig(): Observable<AlertsConfig>+setDefaultConfig(timeoutMs: number): Observable<DefaultAlertsConfig>+sendGatewayConfig(gatewayId: string, timeoutMs: number): Observable<GatewayAlertsConfig>+getAlerts(af: AlertsFilter): Observable<Alerts[]>+deleteGatewayConfig(gatewayId: string): Observable<void>-fetchAlerts(from: string, to: string, gatewayId?: string): Observable<Alerts[]>-normalizeGatewayIds(values?: string[]): string[]-mergeAlerts(rows: Alerts[]): Alerts[]-toAlerts(rows: Record<string, unknown>[]): Alerts[]-toAlertsType(value: unknown): AlertsType-asRecord(value: unknown): Record<string, unknown>-asNumber(value: unknown, fallback: number): number-asString(value: unknown): string-toAlertDetails(details: unknown, message: unknown): AlertDetailsGatewayServiceAuthServiceMultiSelectDropdownComponentModalLayerComponentMain view for displaying offline gateway alerts.Manages date range and multi-gateway filtering.Uses Rome timezone utilities for input values.Manages default and per-gateway timeout configs.Uses `canEditConfig` based on the user's role(tenant_admin) to guard UI interaction.Handles API communication for Alerts and Configs.If multiple gateways are selected in getAlerts(),it uses forkJoin to fetch them in parallel andmerges/sorts the results via mergeAlerts().Contains robust JSON parsing logic for parsingboth CamelCase and SnakeCase AlertDetails. \ No newline at end of file diff --git a/docs/13-pb/docest/specifica_tecnica_frontend/assets/05-core-services.puml b/docs/13-pb/docest/specifica_tecnica_frontend/assets/05-core-services.puml index 4e9b6f95..13c03895 100644 --- a/docs/13-pb/docest/specifica_tecnica_frontend/assets/05-core-services.puml +++ b/docs/13-pb/docest/specifica_tecnica_frontend/assets/05-core-services.puml @@ -1,5 +1,5 @@ @startuml -title NOTIP Frontend - Core Services Architecture +title NoTIP Frontend - Core Services Architecture skinparam backgroundColor #FFFFFF skinparam defaultFontSize 10 @@ -7,6 +7,7 @@ skinparam defaultFontColor #000000 skinparam nodesep 8 skinparam ranksep 8 skinparam linetype ortho +skinparam classAttributeIconSize 0 package "Core Services" <> { class "AuthService" as AuthService { diff --git a/docs/13-pb/docest/specifica_tecnica_frontend/assets/05-core-services.svg b/docs/13-pb/docest/specifica_tecnica_frontend/assets/05-core-services.svg index 43ca25aa..5644209b 100644 --- a/docs/13-pb/docest/specifica_tecnica_frontend/assets/05-core-services.svg +++ b/docs/13-pb/docest/specifica_tecnica_frontend/assets/05-core-services.svg @@ -1 +1 @@ -NOTIP Frontend - Core Services ArchitectureNOTIP Frontend - Core Services ArchitectureCore ServicesAuthServiceSTORAGE_KEY: stringkeycloak: Keycloak (via inject)keycloakEventSignal: SignalauthApi: AuthApiService (via inject)logoutSubject: Subject<void>impersonatingSignal: Signal<boolean>impersonationTokenSignal: Signal<string | null>impersonationPayloadSignal: Signal<JwtPayload | null>logout$: Observable<void>isImpersonating: Signal<boolean>init(): Promise<boolean>login(): voidopenProfile(): voidopenPasswordChange(): voidlogout(): voidgetToken(): Promise<string>getUsername(): Promise<string>getRole(): UserRolegetTenantId(): stringgetUserId(): stringsetImpersonating(value: boolean): voidstartImpersonation(targetUserId: string): Observable<string>stopImpersonation(): voidsaveImpersonationContext(...): voidclearImpersonationStorage(): voidparseStoredImpersonation(raw: string): StoredImpersonation | nullloadImpersonating(): booleanloadImpersonationToken(): string | nullloadImpersonationPayload(): JwtPayload | nullcollectRoles(payload: JwtPayload): Set<string>clearImpersonationContext(): voiddecodeTokenPayload(token: string): JwtPayload | nullnormalizeBase64(value: string): stringasJwtPayload(value: unknown): JwtPayload | nulldecodeJwtPayload(): JwtPayloadformatDisplayName(value: string): stringSessionLifeCyclelogout$: Observable<void>logout(): voidImpersonationStatusisImpersonating: Signal<boolean>ObfuscatedStreamManagerServiceauth: AuthService (via inject)abortController: AbortController | nullchannel: ObservableChannel<TelemetryEnvelope> | nullstreamSessionId: numberopenStream(sp: StreamParameters): Observable<TelemetryEnvelope>closeStream(): voidstartStream(...): Promise<void>buildQuery(sp): URLSearchParamsparseTelemetryEnvelope(raw): TelemetryEnvelopehandleMalformedMessage(...): voidisActiveSession(...): booleantoError(value: unknown, fallbackMessage: string): ErrorisTokenExpiredEvent(value): booleanisTelemetryEnvelope(value): booleanThresholdPrefetchServiceREFRESH_INTERVAL_MS: numberthresholdService: ThresholdService (via inject)authService: AuthService (via inject)refreshSub: Subscription | nullstart(): voidstop(): voidshouldStart(): booleanThresholdServicethresholdsApi: ThresholdsService (via inject)cache: ThresholdConfig[]fetchThresholds(): Observable<ThresholdConfig[]>getCached(): ThresholdConfig[]invalidateCache(): voidrefreshThresholds(): Observable<ThresholdConfig[]>setDefaultThreshold(...): Observable<void>setSensorThreshold(...): Observable<void>deleteSensorThreshold(sensorId: string): Observable<void>deleteTypeThreshold(sensorType: string): Observable<void>getSensorThreshold(sensorId: string): ThresholdConfig | undefinedgetTypeThreshold(sensorType: string): ThresholdConfig | undefinedresolveBounds(...): { min, max }toThresholds(rows): ThresholdConfig[]normalizeType(type: unknown): 'sensorId' | 'sensorType' | nulltoNullableNumber(value: unknown): number | nulltoNonEmptyString(value: unknown): string | nullMeasureBoundsEvaluationServicethresholds: ThresholdService (via inject)evaluate(envelope: DecryptedEnvelope): booleanUserRoleThresholdConfigDecryptedEnvelopeTelemetryEnvelopeImplements both SessionLifeCycle andImpersonationStatus interfaces.Provided as SESSION_LIFECYCLE andIMPERSONATION_STATUS tokens in app.config.ts.Uses Angular inject() pattern.Signal-based state for impersonation.sessionStorage for impersonation token persistence.startImpersonation() calls management-api/auth/impersonate and returns Observable<string>.Manages SSE connection lifecycle:- Creates AbortController per session- Tracks streamSessionId to prevent stale subs- Parses SSE messages into TelemetryEnvelope- Handles token expiry, malformed messages- Emits envelopes via ObservableChannel- Uses @microsoft/fetch-event-source- Detects application 'token_expired' eventsStarts immediately after AuthGuardvalidates user. Runs every 5 minutes.Skips for system_admin or missing tenant.Invalidates cache on logout.Error-resilient: keeps scheduler alive.Evaluates telemetry values againstconfigured thresholds. Priority order:1. Match by sensorId (exact)2. Match by sensorType (fallback)3. No match -> return false (in bounds) \ No newline at end of file +NoTIP Frontend - Core Services ArchitectureNoTIP Frontend - Core Services ArchitectureCore ServicesAuthService-STORAGE_KEY: string-keycloak: Keycloak (via inject)-keycloakEventSignal: Signal-authApi: AuthApiService (via inject)-logoutSubject: Subject<void>-impersonatingSignal: Signal<boolean>-impersonationTokenSignal: Signal<string | null>-impersonationPayloadSignal: Signal<JwtPayload | null>+logout$: Observable<void>+isImpersonating: Signal<boolean>+init(): Promise<boolean>+login(): void+openProfile(): void+openPasswordChange(): void+logout(): void+getToken(): Promise<string>+getUsername(): Promise<string>+getRole(): UserRole+getTenantId(): string+getUserId(): string+setImpersonating(value: boolean): void+startImpersonation(targetUserId: string): Observable<string>+stopImpersonation(): void-saveImpersonationContext(...): void-clearImpersonationStorage(): void-parseStoredImpersonation(raw: string): StoredImpersonation | null-loadImpersonating(): boolean-loadImpersonationToken(): string | null-loadImpersonationPayload(): JwtPayload | null-collectRoles(payload: JwtPayload): Set<string>-clearImpersonationContext(): void-decodeTokenPayload(token: string): JwtPayload | null-normalizeBase64(value: string): string-asJwtPayload(value: unknown): JwtPayload | null-decodeJwtPayload(): JwtPayload-formatDisplayName(value: string): stringSessionLifeCycle+logout$: Observable<void>+logout(): voidImpersonationStatus+isImpersonating: Signal<boolean>ObfuscatedStreamManagerService-auth: AuthService (via inject)-abortController: AbortController | null-channel: ObservableChannel<TelemetryEnvelope> | null-streamSessionId: number+openStream(sp: StreamParameters): Observable<TelemetryEnvelope>+closeStream(): void-startStream(...): Promise<void>-buildQuery(sp): URLSearchParams-parseTelemetryEnvelope(raw): TelemetryEnvelope-handleMalformedMessage(...): void-isActiveSession(...): boolean-toError(value: unknown, fallbackMessage: string): Error-isTokenExpiredEvent(value): boolean-isTelemetryEnvelope(value): booleanThresholdPrefetchService-REFRESH_INTERVAL_MS: number-thresholdService: ThresholdService (via inject)-authService: AuthService (via inject)-refreshSub: Subscription | null+start(): void+stop(): void-shouldStart(): booleanThresholdService-thresholdsApi: ThresholdsService (via inject)-cache: ThresholdConfig[]+fetchThresholds(): Observable<ThresholdConfig[]>+getCached(): ThresholdConfig[]+invalidateCache(): void+refreshThresholds(): Observable<ThresholdConfig[]>+setDefaultThreshold(...): Observable<void>+setSensorThreshold(...): Observable<void>+deleteSensorThreshold(sensorId: string): Observable<void>+deleteTypeThreshold(sensorType: string): Observable<void>-getSensorThreshold(sensorId: string): ThresholdConfig | undefined-getTypeThreshold(sensorType: string): ThresholdConfig | undefined-resolveBounds(...): { min, max }-toThresholds(rows): ThresholdConfig[]-normalizeType(type: unknown): 'sensorId' | 'sensorType' | null-toNullableNumber(value: unknown): number | null-toNonEmptyString(value: unknown): string | nullMeasureBoundsEvaluationService-thresholds: ThresholdService (via inject)+evaluate(envelope: DecryptedEnvelope): booleanUserRoleThresholdConfigDecryptedEnvelopeTelemetryEnvelopeImplements both SessionLifeCycle andImpersonationStatus interfaces.Provided as SESSION_LIFECYCLE andIMPERSONATION_STATUS tokens in app.config.ts.Uses Angular inject() pattern.Signal-based state for impersonation.sessionStorage for impersonation token persistence.startImpersonation() calls management-api/auth/impersonate and returns Observable<string>.Manages SSE connection lifecycle:- Creates AbortController per session- Tracks streamSessionId to prevent stale subs- Parses SSE messages into TelemetryEnvelope- Handles token expiry, malformed messages- Emits envelopes via ObservableChannel- Uses @microsoft/fetch-event-source- Detects application 'token_expired' eventsStarts immediately after AuthGuardvalidates user. Runs every 5 minutes.Skips for system_admin or missing tenant.Invalidates cache on logout.Error-resilient: keeps scheduler alive.Evaluates telemetry values againstconfigured thresholds. Priority order:1. Match by sensorId (exact)2. Match by sensorType (fallback)3. No match -> return false (in bounds) \ No newline at end of file diff --git a/docs/13-pb/docest/specifica_tecnica_frontend/assets/06-sensors.puml b/docs/13-pb/docest/specifica_tecnica_frontend/assets/06-sensors.puml new file mode 100644 index 00000000..5cd40668 --- /dev/null +++ b/docs/13-pb/docest/specifica_tecnica_frontend/assets/06-sensors.puml @@ -0,0 +1,123 @@ +@startuml +title NoTIP Frontend - Sensors Architecture + +skinparam backgroundColor #FFFFFF +skinparam defaultFontSize 10 +skinparam defaultFontColor #000000 +skinparam nodesep 8 +skinparam ranksep 8 +skinparam linetype ortho +skinparam classAttributeIconSize 0 +left to right direction + +package "Sensors Feature" <> { + class "SensorListPageComponent" as SensorListPage { + -- + +sensors: Signal + +isLoading: Signal + +errorMessage: Signal + +gatewayFilters: Signal + +typeFilters: Signal + +gatewayOptions: Computed + +sensorTypeOptions: Computed + +filteredSensors: Computed + -- + +ngOnInit(): void + +onGatewayFilterChanged(gatewayIds: string[]): void + +onTypeFilterChanged(sensorTypes: string[]): void + +onClearFilters(): void + +openSensorDetail(sensorId: string): void + -loadSensors(): void + -normalizeList(values: string[]): string[] + -uniqueSorted(values: string[]): string[] + } + + class "SensorDetailPageComponent" as SensorDetailPage { + -- + -{static} QUERY_PAGE_SIZE = 20 + -{static} STREAM_PAGE_SIZE = 20 + -{static} DEFAULT_QUERY_WINDOW_HOURS = 24 + -streamSubscription: Subscription | null + -telemetryRunId: number + +sensorId: Signal + +sensor: Signal + +errorMessage: Signal + +telemetry: Signal> + +isTelemetryLoading: Signal + -- + +ngOnInit(): void + +ngOnDestroy(): void + -loadSensor(sensorId: string): void + -startTelemetryStream(sensorId: string): void + -loadHistoryAndAttachStream(queryFilters, streamFilters, runId): Promise + -openObfuscatedStream(filters: StreamParameters, runId: number): void + -openValidatedStream(filters: StreamParameters, runId: number): void + -stopTelemetryStream(): void + -defaultQueryWindow(): Pick + -takeLastRows(rows: Array): Array + -isCurrentTelemetryRun(runId: number): boolean + } + + package "Sensors Services" as Services { + class "SensorService" as SensorService { + -- + -sensorApi: DataApiSensorsService + -- + +getAllSensors(refreshMs?: number): Observable + +getGatewaySensors(id: string, refreshMs?: number): Observable + -withRefresh(refreshMs: number, fetchFn: () => Observable): Observable + -fetchSensors(gatewayId?: string): Observable + -toSensor(row: SensorDto): Sensor + } + } + + ' Internal Feature Relationships + SensorListPage --> SensorService + SensorDetailPage --> SensorService +} + +package "External Dependencies" <> { + class "AuthService" as AuthService + class "ObfuscatedMeasureService" as ObfMeasureService + class "ValidatedMeasureFacadeService" as ValMeasureFacade + class "TelemetryChartComponent" as TelemetryChart + class "TelemetryTableComponent" as TelemetryTable + class "MultiSelectDropdownComponent" as MultiSelectDropdown + class "RomeDateTimePipe" as RomeDatePipe +} + +' External Links +SensorListPage --> MultiSelectDropdown +SensorListPage --> RomeDatePipe + +SensorDetailPage --> AuthService +SensorDetailPage --> ObfMeasureService +SensorDetailPage --> ValMeasureFacade +SensorDetailPage --> TelemetryChart +SensorDetailPage --> TelemetryTable +SensorDetailPage --> RomeDatePipe + +note top of SensorListPage + Displays all sensors for the current tenant. + Uses frontend-computed signals to derive + unique 'gatewayOptions' and 'sensorTypeOptions' + for the MultiSelectDropdowns and applies + local filtering via 'filteredSensors'. +end note + +note right of SensorDetailPage + Displays sensor metadata and real-time telemetry. + Depending on AuthService.isImpersonating(), + fetches history (query) and starts SSE stream + via Obfuscated or Validated measure services. + Safeguards overlapping requests with 'telemetryRunId'. +end note + +note bottom of SensorService + Communicates with the Data API. + Supports automatic data polling using RxJS + 'timer(0, refreshMs).pipe(switchMap(...))'. + Default refresh rate is 10 seconds. +end note + +@enduml \ No newline at end of file diff --git a/docs/13-pb/docest/specifica_tecnica_frontend/assets/06-sensors.svg b/docs/13-pb/docest/specifica_tecnica_frontend/assets/06-sensors.svg new file mode 100644 index 00000000..1ca96a1e --- /dev/null +++ b/docs/13-pb/docest/specifica_tecnica_frontend/assets/06-sensors.svg @@ -0,0 +1 @@ +NoTIP Frontend - Sensors ArchitectureNoTIP Frontend - Sensors ArchitectureSensors FeatureSensors ServicesExternal DependenciesSensorListPageComponent+sensors: Signal<Sensor[]>+isLoading: Signal<boolean>+errorMessage: Signal<string | null>+gatewayFilters: Signal<string[]>+typeFilters: Signal<string[]>+gatewayOptions: Computed<string[]>+sensorTypeOptions: Computed<string[]>+filteredSensors: Computed<Sensor[]>+ngOnInit(): void+onGatewayFilterChanged(gatewayIds: string[]): void+onTypeFilterChanged(sensorTypes: string[]): void+onClearFilters(): void+openSensorDetail(sensorId: string): void-loadSensors(): void-normalizeList(values: string[]): string[]-uniqueSorted(values: string[]): string[]SensorDetailPageComponent-QUERY_PAGE_SIZE = 20-STREAM_PAGE_SIZE = 20-DEFAULT_QUERY_WINDOW_HOURS = 24-streamSubscription: Subscription | null-telemetryRunId: number+sensorId: Signal<string>+sensor: Signal<Sensor | null>+errorMessage: Signal<string | null>+telemetry: Signal<Array<CheckedEnvelope | ObfuscatedEnvelope>>+isTelemetryLoading: Signal<boolean>+ngOnInit(): void+ngOnDestroy(): void-loadSensor(sensorId: string): void-startTelemetryStream(sensorId: string): void-loadHistoryAndAttachStream(queryFilters, streamFilters, runId): Promise<void>-openObfuscatedStream(filters: StreamParameters, runId: number): void-openValidatedStream(filters: StreamParameters, runId: number): void-stopTelemetryStream(): void-defaultQueryWindow(): Pick<QueryParameters, 'from' | 'to'>-takeLastRows(rows: Array): Array<CheckedEnvelope | ObfuscatedEnvelope>-isCurrentTelemetryRun(runId: number): booleanSensorService-sensorApi: DataApiSensorsService+getAllSensors(refreshMs?: number): Observable<Sensor[]>+getGatewaySensors(id: string, refreshMs?: number): Observable<Sensor[]>-withRefresh(refreshMs: number, fetchFn: () => Observable<Sensor[]>): Observable<Sensor[]>-fetchSensors(gatewayId?: string): Observable<Sensor[]>-toSensor(row: SensorDto): SensorAuthServiceObfuscatedMeasureServiceValidatedMeasureFacadeServiceTelemetryChartComponentTelemetryTableComponentMultiSelectDropdownComponentRomeDateTimePipeDisplays all sensors for the current tenant.Uses frontend-computed signals to deriveunique 'gatewayOptions' and 'sensorTypeOptions'for the MultiSelectDropdowns and applieslocal filtering via 'filteredSensors'.Displays sensor metadata and real-time telemetry.Depending on AuthService.isImpersonating(),fetches history (query) and starts SSE streamvia Obfuscated or Validated measure services.Safeguards overlapping requests with 'telemetryRunId'.Communicates with the Data API.Supports automatic data polling using RxJSDefault refresh rate is 10 seconds. \ No newline at end of file diff --git a/docs/13-pb/docest/specifica_tecnica_frontend/assets/07-dashboard.puml b/docs/13-pb/docest/specifica_tecnica_frontend/assets/07-dashboard.puml index 55eb4bb6..0253aacd 100644 --- a/docs/13-pb/docest/specifica_tecnica_frontend/assets/07-dashboard.puml +++ b/docs/13-pb/docest/specifica_tecnica_frontend/assets/07-dashboard.puml @@ -1,5 +1,5 @@ @startuml -title NOTIP Frontend - Dashboard Architecture +title NoTIP Frontend - Dashboard Architecture skinparam backgroundColor #FFFFFF skinparam defaultFontSize 10 @@ -7,7 +7,8 @@ skinparam defaultFontColor #000000 skinparam nodesep 8 skinparam ranksep 8 skinparam linetype ortho - +skinparam classAttributeIconSize 0 +left to right direction package "Dashboard Feature" <> { class "DataDashboardPageComponent" as DashboardPage { diff --git a/docs/13-pb/docest/specifica_tecnica_frontend/assets/07-dashboard.svg b/docs/13-pb/docest/specifica_tecnica_frontend/assets/07-dashboard.svg index 42c08949..2a2a1789 100644 --- a/docs/13-pb/docest/specifica_tecnica_frontend/assets/07-dashboard.svg +++ b/docs/13-pb/docest/specifica_tecnica_frontend/assets/07-dashboard.svg @@ -1 +1 @@ -NOTIP Frontend - Dashboard ArchitectureNOTIP Frontend - Dashboard ArchitectureDashboard FeatureDashboard ServicesDataDashboardPageComponentdataMode: Signal<'clear' | 'obfuscated'>activeView: Signal<'stream' | 'query'>gatewayOptions: Signal<string[]>sensorTypeOptions: Signal<string[]>sensorOptions: Signal<string[]>isExporting: Signal<boolean>ngOnInit(): voidonFiltersApplied(filters): voidonFiltersCleared(): voidsetActiveView(view): voidonExportQuery(): Promise<void>onNextQueryPage(): voiddownloadAsCsv(rows): voidFilterPanelComponentsensorCatalog: InputSignal<DashboardSensorCatalogEntry[]>selectedFilters: Signal<MultiSelectState>queryFromRaw: Signal<string>queryToRaw: Signal<string>applyFilters(event): voidclearFilters(): voidenforceMaxQueryWindow(): voidnormalizeSelection(state): MultiSelectStateTelemetryChartComponentchartInstance: Chart | undefineduniqueCheckedSensorIds: Computed<string[]>«constructor» (uses effect)toChartData(rows): { labels, datasets }formatTimestamp(value): stringTelemetryTableComponentmeasures: InputSignal<Array<CheckedEnvelope | ObfuscatedEnvelope>>formatValue(row): stringrowClass(row): stringBoundsEvalRomeDatePipeChartJsDecryptedMeasureServicesdk: CryptoSdkactiveStreamAbortController: AbortController | nullquery(params): Observable<QueryResPage>openStream(params): Observable<DecryptedEnvelope>export(params): Observable<DecryptedEnvelope>closeStream(): voidObfuscatedMeasureServicemsm: ObfuscatedStreamManagerServicemeasuresApi: MeasuresServicequery(qp): Observable<ObfuscatedQueryResPage>openStream(sp): Observable<ObfuscatedEnvelope[]>closeStream(): voidValidatedMeasureFacadeServicedecryptedMeasureService: DecryptedMeasureServicemeasureBoundsEvaluationService: MeasureBoundsEvaluationServicequery(params): Observable<CheckedQueryResPage>openStream(params): Observable<CheckedEnvelope>export(params): Observable<CheckedEnvelope>closeStream(): voidMain dashboard page. Resolver setsdataMode based on impersonation status.Stream mode: real-time SSE, 20-row capQuery mode: paginated with cursor, 24h limitExport: disabled in obfuscated modeMulti-select dropdowns with search.Cross-filter dependency: gatewayselection updates sensor catalogs.24h time window enforcementwith automatic clamping.Rome timezone datetime conversion.Chart.js line chart. One dataset persensor with distinct color.Cap at 120 points per dataset.Italian locale timestamp formatting.Responsive update via Angular effect().Facade that wraps DecryptedMeasureand adds isOutOfBounds evaluation.Transforms DecryptedEnvelope intoCheckedEnvelope by evaluating valueagainst cached thresholds.injects MeasureBoundsEvaluationServicetoRomeDateTimeInputfromRomeDateTimeInputToIso \ No newline at end of file +NoTIP Frontend - Dashboard ArchitectureNoTIP Frontend - Dashboard ArchitectureDashboard FeatureDashboard ServicesDataDashboardPageComponent+dataMode: Signal<'clear' | 'obfuscated'>+activeView: Signal<'stream' | 'query'>+gatewayOptions: Signal<string[]>+sensorTypeOptions: Signal<string[]>+sensorOptions: Signal<string[]>+isExporting: Signal<boolean>+ngOnInit(): void+onFiltersApplied(filters): void+onFiltersCleared(): void+setActiveView(view): void+onExportQuery(): Promise<void>+onNextQueryPage(): void-downloadAsCsv(rows): voidFilterPanelComponent+sensorCatalog: InputSignal<DashboardSensorCatalogEntry[]>+selectedFilters: Signal<MultiSelectState>+queryFromRaw: Signal<string>+queryToRaw: Signal<string>+applyFilters(event): void+clearFilters(): void-enforceMaxQueryWindow(): void-normalizeSelection(state): MultiSelectStateTelemetryChartComponent-chartInstance: Chart | undefined+uniqueCheckedSensorIds: Computed<string[]>«constructor» (uses effect)-toChartData(rows): { labels, datasets }-formatTimestamp(value): stringTelemetryTableComponent+measures: InputSignal<Array<CheckedEnvelope | ObfuscatedEnvelope>>+formatValue(row): string+rowClass(row): stringBoundsEvalRomeDatePipeChartJsDecryptedMeasureService-sdk: CryptoSdk-activeStreamAbortController: AbortController | null+query(params): Observable<QueryResPage>+openStream(params): Observable<DecryptedEnvelope>+export(params): Observable<DecryptedEnvelope>+closeStream(): voidObfuscatedMeasureService-msm: ObfuscatedStreamManagerService-measuresApi: MeasuresService+query(qp): Observable<ObfuscatedQueryResPage>+openStream(sp): Observable<ObfuscatedEnvelope[]>+closeStream(): voidValidatedMeasureFacadeService-decryptedMeasureService: DecryptedMeasureService-measureBoundsEvaluationService: MeasureBoundsEvaluationService+query(params): Observable<CheckedQueryResPage>+openStream(params): Observable<CheckedEnvelope>+export(params): Observable<CheckedEnvelope>+closeStream(): voidMain dashboard page. Resolver setsdataMode based on impersonation status.Stream mode: real-time SSE, 20-row capQuery mode: paginated with cursor, 24h limitExport: disabled in obfuscated modeMulti-select dropdowns with search.Cross-filter dependency: gatewayselection updates sensor catalogs.24h time window enforcementwith automatic clamping.Rome timezone datetime conversion.Chart.js line chart. One dataset persensor with distinct color.Cap at 120 points per dataset.Italian locale timestamp formatting.Responsive update via Angular effect().Facade that wraps DecryptedMeasureand adds isOutOfBounds evaluation.Transforms DecryptedEnvelope intoCheckedEnvelope by evaluating valueagainst cached thresholds.injects MeasureBoundsEvaluationServicetoRomeDateTimeInputfromRomeDateTimeInputToIso \ No newline at end of file diff --git a/docs/13-pb/docest/specifica_tecnica_frontend/assets/09-gateways.puml b/docs/13-pb/docest/specifica_tecnica_frontend/assets/09-gateways.puml index b50412ce..4e40eeca 100644 --- a/docs/13-pb/docest/specifica_tecnica_frontend/assets/09-gateways.puml +++ b/docs/13-pb/docest/specifica_tecnica_frontend/assets/09-gateways.puml @@ -1,5 +1,5 @@ @startuml -title NOTIP Frontend - Gateway Feature Architecture +title NoTIP Frontend - Gateway Feature Architecture skinparam backgroundColor #FFFFFF skinparam defaultFontSize 10 @@ -7,6 +7,9 @@ skinparam defaultFontColor #000000 skinparam nodesep 8 skinparam ranksep 8 skinparam linetype ortho +skinparam classAttributeIconSize 0 +left to right direction + package "Shared Components" <> { class "DeleteConfirmModalComponent" as DeleteModal { diff --git a/docs/13-pb/docest/specifica_tecnica_frontend/assets/09-gateways.svg b/docs/13-pb/docest/specifica_tecnica_frontend/assets/09-gateways.svg index e8f0f462..a168f9d6 100644 --- a/docs/13-pb/docest/specifica_tecnica_frontend/assets/09-gateways.svg +++ b/docs/13-pb/docest/specifica_tecnica_frontend/assets/09-gateways.svg @@ -1 +1 @@ -NOTIP Frontend - Gateway Feature ArchitectureNOTIP Frontend - Gateway Feature ArchitectureShared ComponentsCore & Other FeaturesGateway FeatureGateway ServicesDeleteConfirmModalComponentopen: input<boolean>title: input<string>message: input<string>busy: input<boolean>onConfirm(): voidonCancel(): voidModalLayerComponentStatusBadgeComponentAuthServiceSensorServiceObfuscatedMeasureServiceValidatedMeasureFacadeServiceTelemetryTableComponentGatewayListPageComponentgateways: Signal<Gateway[]>isLoading: Signal<boolean>selectedGatewayId: Signal<string | null>errorMessage: Signal<string | null>ngOnInit(): voidonGatewaySelected(gatewayId: string): voidloadGateways(): voidGatewayDetailPageComponentgateway: Signal<Gateway | null>sensors: Signal<Sensor[]>telemetry: Signal<Array<CheckedEnvelope | ObfuscatedEnvelope>>canManage: booleanisBusy: Signal<boolean>ngOnInit(): voidngOnDestroy(): voidsubmitRename(nextNameRaw: string): voidsubmitConfigCommand(config: GatewayConfig): voidsubmitFirmwareCommand(firmware: GatewayFirmware): voidconfirmDeleteGateway(): voidGatewayCardComponentgateway: input<Gateway>selected: input<boolean>canOpenDetail: input<boolean>onOpenDetail(): voidGatewayActionsComponentcanManage: input<boolean>showRename: input<boolean>isBusy: input<boolean>onRename(): voidonConfigure(): voidonFirmware(): voidonDelete(): voidGatewayRenameModalComponentopen: input<boolean>currentName: input<string>busy: input<boolean>submit(event: Event, nextNameRaw: string): voidclose(): voidCommandModalComponentopen: input<boolean>mode: input<CommandModalMode>busy: input<boolean>initialSendFrequencyMs: input<number | null>initialStatus: input<CmdGatewayStatus | null>submitConfig(event: Event, frequencyRaw: string, statusRaw: string): voidsubmitFirmware(event: Event, version: string, downloadUrl: string): voidclose(): voidGatewayServicelistSignal: Signal<Gateway[]>selectedGatewaySignal: Signal<Gateway | null>loadingSignal: Signal<boolean>gatewaysApi: GatewaysService (generated)getGateways(): Observable<Gateway[]>getGatewayDetail(gatewayId: string): Observable<Gateway>updateGatewayName(gatewayId: string, name: string): Observable<GatewayUpdateResult>deleteGateway(gatewayId: string): Observable<string>list(): Signal<Gateway[]>selectedGateway(): Signal<Gateway | null>isLoading(): Signal<boolean>toGatewayStatus(status: unknown): GatewayStatustoGateway(dto: GatewayResponseDto): GatewayCommandServicecommandsApi: CommandsService (generated)sendConfig(id: string, config: GatewayConfig): Observable<CommandStatusUpdate>sendFirmware(id: string, firmware: GatewayFirmware): Observable<CommandStatusUpdate>pollStatus(gwId: string, cmdId: string, intervalMs: number): Observable<CommandStatusUpdate>toCommandStatus(status: string): CommandStatusMaintains internal signals for list,selected gateway, and loading state.Exposes them as read-only Signal<T>via public getters. Normalizes statusfrom API (handles uppercase, camelCase,paused/offline variants).Sends config/firmware commandsthen polls status every 2s with5-minute timeout. Maps 304 to cache,404/503 to timeout. Filters outinvalid fields from payload.Rejects commands with disallowed status.Most complex page in the application.Combines: gateway summary, rename,command sending (config + firmware),delete confirmation, sensor list,and live telemetry stream.Relies on external core services forimpersonation checks and telemetry. \ No newline at end of file +NoTIP Frontend - Gateway Feature ArchitectureNoTIP Frontend - Gateway Feature ArchitectureShared ComponentsCore & Other FeaturesGateway FeatureGateway ServicesDeleteConfirmModalComponent-open: input<boolean>-title: input<string>-message: input<string>-busy: input<boolean>+onConfirm(): void+onCancel(): voidModalLayerComponentStatusBadgeComponentAuthServiceSensorServiceObfuscatedMeasureServiceValidatedMeasureFacadeServiceTelemetryTableComponentGatewayListPageComponent-gateways: Signal<Gateway[]>-isLoading: Signal<boolean>-selectedGatewayId: Signal<string | null>-errorMessage: Signal<string | null>+ngOnInit(): void+onGatewaySelected(gatewayId: string): void-loadGateways(): voidGatewayDetailPageComponent-gateway: Signal<Gateway | null>-sensors: Signal<Sensor[]>-telemetry: Signal<Array<CheckedEnvelope | ObfuscatedEnvelope>>-canManage: boolean-isBusy: Signal<boolean>+ngOnInit(): void+ngOnDestroy(): void+submitRename(nextNameRaw: string): void+submitConfigCommand(config: GatewayConfig): void+submitFirmwareCommand(firmware: GatewayFirmware): void+confirmDeleteGateway(): voidGatewayCardComponent-gateway: input<Gateway>-selected: input<boolean>-canOpenDetail: input<boolean>+onOpenDetail(): voidGatewayActionsComponent-canManage: input<boolean>-showRename: input<boolean>-isBusy: input<boolean>+onRename(): void+onConfigure(): void+onFirmware(): void+onDelete(): voidGatewayRenameModalComponent-open: input<boolean>-currentName: input<string>-busy: input<boolean>+submit(event: Event, nextNameRaw: string): void+close(): voidCommandModalComponent-open: input<boolean>-mode: input<CommandModalMode>-busy: input<boolean>-initialSendFrequencyMs: input<number | null>-initialStatus: input<CmdGatewayStatus | null>+submitConfig(event: Event, frequencyRaw: string, statusRaw: string): void+submitFirmware(event: Event, version: string, downloadUrl: string): void+close(): voidGatewayService-listSignal: Signal<Gateway[]>-selectedGatewaySignal: Signal<Gateway | null>-loadingSignal: Signal<boolean>-gatewaysApi: GatewaysService (generated)+getGateways(): Observable<Gateway[]>+getGatewayDetail(gatewayId: string): Observable<Gateway>+updateGatewayName(gatewayId: string, name: string): Observable<GatewayUpdateResult>+deleteGateway(gatewayId: string): Observable<string>+list(): Signal<Gateway[]>+selectedGateway(): Signal<Gateway | null>+isLoading(): Signal<boolean>-toGatewayStatus(status: unknown): GatewayStatus-toGateway(dto: GatewayResponseDto): GatewayCommandService-commandsApi: CommandsService (generated)+sendConfig(id: string, config: GatewayConfig): Observable<CommandStatusUpdate>+sendFirmware(id: string, firmware: GatewayFirmware): Observable<CommandStatusUpdate>+pollStatus(gwId: string, cmdId: string, intervalMs: number): Observable<CommandStatusUpdate>-toCommandStatus(status: string): CommandStatusMaintains internal signals for list,selected gateway, and loading state.Exposes them as read-only Signal<T>via public getters. Normalizes statusfrom API (handles uppercase, camelCase,paused/offline variants).Sends config/firmware commandsthen polls status every 2s with5-minute timeout. Maps 304 to cache,404/503 to timeout. Filters outinvalid fields from payload.Rejects commands with disallowed status.Most complex page in the application.Combines: gateway summary, rename,command sending (config + firmware),delete confirmation, sensor list,and live telemetry stream.Relies on external core services forimpersonation checks and telemetry. \ No newline at end of file diff --git a/docs/13-pb/docest/specifica_tecnica_frontend/assets/10-shared-components.puml b/docs/13-pb/docest/specifica_tecnica_frontend/assets/10-shared-components.puml index 6b4a5190..cb0b203d 100644 --- a/docs/13-pb/docest/specifica_tecnica_frontend/assets/10-shared-components.puml +++ b/docs/13-pb/docest/specifica_tecnica_frontend/assets/10-shared-components.puml @@ -1,5 +1,5 @@ @startuml -title NOTIP Frontend - Shared Components +title NoTIP Frontend - Shared Components skinparam backgroundColor #FFFFFF skinparam defaultFontSize 10 @@ -7,6 +7,7 @@ skinparam defaultFontColor #000000 skinparam nodesep 8 skinparam ranksep 8 skinparam linetype ortho +skinparam classAttributeIconSize 0 package "Shared Components" <> { @@ -112,21 +113,6 @@ package "Shared Components" <> { +onClick(): void } - class "ImpersonationBannerComponent" as ImpBanner { - -- Inputs (Signals) -- - +active: Signal - +impersonatedUserId: Signal - -- - // Defined in shared, but not used by Sidebar - } - - class "PlaceholderPageComponent" as PlaceholderPage { - -- Properties -- - +title: string - -- - // Scaffold page for features in development - } - ' Usage relationships derived from imports Sidebar --> ImpTag Sidebar --> ProfileSection diff --git a/docs/13-pb/docest/specifica_tecnica_frontend/assets/10-shared-components.svg b/docs/13-pb/docest/specifica_tecnica_frontend/assets/10-shared-components.svg index 0c5db506..5fe49bc6 100644 --- a/docs/13-pb/docest/specifica_tecnica_frontend/assets/10-shared-components.svg +++ b/docs/13-pb/docest/specifica_tecnica_frontend/assets/10-shared-components.svg @@ -1 +1 @@ -NOTIP Frontend - Shared ComponentsNOTIP Frontend - Shared ComponentsShared ComponentsSidebarComponentisImpersonating: Signal<boolean>username: Signal<string>role: Signal<UserRole>Inputs (Signals)logoutRequested: OutputEmitterRef<void>impersonationStopRequested: OutputEmitterRef<void>profileRequested: OutputEmitterRef<void>passwordChangeRequested: OutputEmitterRef<void>OutputsmenuItems(): MenuItem[]emitLogout(): voidemitImpersonationStopRequested(): voidemitProfileRequested(): voidemitPasswordChangeRequested(): voidcurrentRole(): UserRoleModalLayerComponentopen: Signal<boolean>closeOnBackdrop: Signal<boolean>Inputs (Signals)backdropClosed: OutputEmitterRef<void>OutputsonHostClick(event: MouseEvent): voidMultiSelectDropdownComponentoptions: Signal<string[]>selected: Signal<string[]>placeholder: Signal<string>searchPlaceholder: Signal<string>noResultsLabel: Signal<string>disabled: Signal<boolean>Inputs (Signals)selectedChange: OutputEmitterRef<string[]>OutputsisOpen: WritableSignal<boolean>searchTerm: WritableSignal<string>State (Signals)onDocumentClick(event: MouseEvent): voidonEscape(): voidtoggleMenu(): voidupdateSearch(value: string): voidtoggleOption(value: string): voidisSelected(value: string): booleanselectionLabel(): stringfilteredOptions(): string[]normalizedOptions(): string[]closeMenu(): voidStatusBadgeComponentstatus: Signal<string>Inputs (Signals)badgeClass(): stringDeleteConfirmModalComponentopen: Signal<boolean>title: Signal<string>message: Signal<string>busy: Signal<boolean>Inputs (Signals)confirmed: OutputEmitterRef<void>cancelled: OutputEmitterRef<void>OutputsonConfirm(): voidonCancel(): voidProfileSectionComponentusername: Signal<string> «required»role: Signal<string> «required»showProfileLink: Signal<boolean>Inputs (Signals)profileRequested: OutputEmitterRef<void>passwordChangeRequested: OutputEmitterRef<void>OutputsemitProfileRequested(event: Event): voidemitPasswordChangeRequested(event: Event): voidImpersonationTagComponent// Static component showing// "OBFUSCATED MODE" warningLogoutButtonComponentclicked: OutputEmitterRef<void>OutputsonClick(): voidImpersonationBannerComponentactive: Signal<boolean>impersonatedUserId: Signal<string | null>Inputs (Signals)// Defined in shared, but not used by SidebarPlaceholderPageComponenttitle: stringProperties// Scaffold page for features in developmentMain navigation sidebar. Shows/hidesmenu items based on user role(tenant_user, tenant_admin, system_admin).During impersonation: shows ImpersonationTagand "Exit impersonation" button.Generic modal overlay with backdrop.Uses HTML <dialog> element.Listens to @HostListener clicks to detectwhen the dialog backdrop is clicked.Reusable multi-select dropdown withintegrated search. Opens/closes viadocument click and Escape key.Fully driven by Angular Signals.Colored badge component mappingstatus strings to CSS classes:- is-good: online, ack- is-warn: paused, provisioning, queued- is-bad: offline, nack, expired, timeout, error- is-neutral: default \ No newline at end of file +NoTIP Frontend - Shared ComponentsNoTIP Frontend - Shared ComponentsShared ComponentsSidebarComponent+isImpersonating: Signal<boolean>+username: Signal<string>+role: Signal<UserRole>Inputs (Signals)+logoutRequested: OutputEmitterRef<void>+impersonationStopRequested: OutputEmitterRef<void>+profileRequested: OutputEmitterRef<void>+passwordChangeRequested: OutputEmitterRef<void>Outputs+menuItems(): MenuItem[]+emitLogout(): void+emitImpersonationStopRequested(): void+emitProfileRequested(): void+emitPasswordChangeRequested(): void-currentRole(): UserRoleModalLayerComponent+open: Signal<boolean>+closeOnBackdrop: Signal<boolean>Inputs (Signals)+backdropClosed: OutputEmitterRef<void>Outputs+onHostClick(event: MouseEvent): voidMultiSelectDropdownComponent+options: Signal<string[]>+selected: Signal<string[]>+placeholder: Signal<string>+searchPlaceholder: Signal<string>+noResultsLabel: Signal<string>+disabled: Signal<boolean>Inputs (Signals)+selectedChange: OutputEmitterRef<string[]>Outputs+isOpen: WritableSignal<boolean>+searchTerm: WritableSignal<string>State (Signals)+onDocumentClick(event: MouseEvent): void+onEscape(): void+toggleMenu(): void+updateSearch(value: string): void+toggleOption(value: string): void+isSelected(value: string): boolean+selectionLabel(): string+filteredOptions(): string[]-normalizedOptions(): string[]-closeMenu(): voidStatusBadgeComponent+status: Signal<string>Inputs (Signals)+badgeClass(): stringDeleteConfirmModalComponent+open: Signal<boolean>+title: Signal<string>+message: Signal<string>+busy: Signal<boolean>Inputs (Signals)+confirmed: OutputEmitterRef<void>+cancelled: OutputEmitterRef<void>Outputs+onConfirm(): void+onCancel(): voidProfileSectionComponent+username: Signal<string> «required»+role: Signal<string> «required»+showProfileLink: Signal<boolean>Inputs (Signals)+profileRequested: OutputEmitterRef<void>+passwordChangeRequested: OutputEmitterRef<void>Outputs+emitProfileRequested(event: Event): void+emitPasswordChangeRequested(event: Event): voidImpersonationTagComponent// Static component showing// "OBFUSCATED MODE" warningLogoutButtonComponent+clicked: OutputEmitterRef<void>Outputs+onClick(): voidMain navigation sidebar. Shows/hidesmenu items based on user role(tenant_user, tenant_admin, system_admin).During impersonation: shows ImpersonationTagand "Exit impersonation" button.Generic modal overlay with backdrop.Uses HTML <dialog> element.Listens to @HostListener clicks to detectwhen the dialog backdrop is clicked.Reusable multi-select dropdown withintegrated search. Opens/closes viadocument click and Escape key.Fully driven by Angular Signals.Colored badge component mappingstatus strings to CSS classes:- is-good: online, ack- is-warn: paused, provisioning, queued- is-bad: offline, nack, expired, timeout, error- is-neutral: default \ No newline at end of file diff --git a/docs/13-pb/docest/specifica_tecnica_frontend/specifica_tecnica_frontend.meta.yaml b/docs/13-pb/docest/specifica_tecnica_frontend/specifica_tecnica_frontend.meta.yaml index c51a56a2..1bcdd985 100644 --- a/docs/13-pb/docest/specifica_tecnica_frontend/specifica_tecnica_frontend.meta.yaml +++ b/docs/13-pb/docest/specifica_tecnica_frontend/specifica_tecnica_frontend.meta.yaml @@ -1,5 +1,12 @@ title: Specifica tecnica - Frontend WebApp changelog: + - version: "1.1.0" + date: "2026-04-17" + authors: + - Leonardo Preo + verifier: Alessandro Mazzariol + description: > + Aggiustamenti post meeting con Prof. Cardin: modifica descrizione architettura - version: "1.0.0" date: "2026-04-13" approver: Leonardo Preo diff --git a/docs/13-pb/docest/specifica_tecnica_frontend/specifica_tecnica_frontend.typ b/docs/13-pb/docest/specifica_tecnica_frontend/specifica_tecnica_frontend.typ index 7164f37f..c6778dfd 100644 --- a/docs/13-pb/docest/specifica_tecnica_frontend/specifica_tecnica_frontend.typ +++ b/docs/13-pb/docest/specifica_tecnica_frontend/specifica_tecnica_frontend.typ @@ -93,13 +93,28 @@ = Architettura logica - L'applicazione adotta una *Layered Feature-Based Architecture* con componenti standalone Angular, senza NgModules. La - struttura è organizzata in tre layer orizzontali con responsabilità distinte e non sovrapposte — Core, Features e - Shared — combinati con una decomposizione verticale per dominio funzionale all'interno del layer Features. Le - dipendenze scorrono in direzione unidirezionale: Features e Shared dipendono da Core, mentre nessun layer dipende da - un layer superiore (Unidirectional Dependency Rule). Lo state management è basato su *segnali Angular* (signal(), - computed(), effect()) per lo stato locale dei componenti e dei servizi di feature, combinati con *RxJS* per la - gestione di flussi asincroni (SSE, HTTP) e la comunicazione inter-componente tramite Subject. + L’applicazione adotta un'architettura *Model-View* (nello specifico Model-View-ViewModel), tipica dell'ecosistema + Angular, implementata attraverso componenti standalone senza l'uso di NgModules. Questo paradigma architetturale + separa nettamente la logica di business dalla presentazione, assegnando responsabilità precise: + + - *Model:* Rappresenta i dati di dominio e la logica applicativa. È implementato attraverso i Service, le interfacce e + le classi. Il Model è responsabile della gestione dei flussi di dati asincroni e della comunicazione con il backend + (es. chiamate HTTP e connessioni SSE), avvalendosi in modo massiccio della libreria RxJS. + - *View:* Costituisce l'interfaccia utente (template HTML e fogli di stile CSS). È un layer puramente presentazionale, + reattivo e completamente disaccoppiato dalla logica complessa di recupero o manipolazione dei dati. + - *ViewModel / Controller:* I componenti standalone agiscono da ponte (intermediari) tra il Model e la View. + Interrogano i servizi per ottenere i dati e gestiscono lo stato locale avvalendosi dello state management basato sui + segnali di Angular (signal(), computed(), effect()). Questo permette di esporre alla View uno stato reattivo + facilmente consumabile e di gestire l'interazione dell'utente (Subject e comunicazioni inter-componente). + + A livello organizzativo e di file system, l'applicazione supporta questa architettura Model-View suddividendo il + codice in tre aree orizzontali principali (Core, Features e Shared). Questo garantisce che la logica di business + (Model, contenuta soprattutto nel Core e nei servizi Feature) sia ben isolata dalla presentazione, mantenendo + dipendenze unidirezionali e impedendo che la View conosca i dettagli di implementazione dei dati. + + Lo state management è basato su *segnali Angular* (signal(), computed(), effect()) per lo stato locale dei componenti + e dei servizi di feature, combinati con *RxJS* per la gestione di flussi asincroni (SSE, HTTP) e la comunicazione + inter-componente tramite Subject. == Layout delle cartelle @@ -237,11 +252,9 @@ │ │ ├── multi-select-dropdown/ # Dropdown multi-selezione con ricerca │ │ ├── status-badge/ # Badge colorati per stato │ │ ├── delete-confirm-modal/ # Modale conferma cancellazione - │ │ ├── impersonation-banner/ # Banner modalità impersonazione │ │ ├── impersonation-tag/ # Tag "OBFUSCATED MODE" │ │ ├── logout-button/ # Bottone logout │ │ ├── profile-section/ # Sezione profilo utente - │ │ └── placeholder-page/ # Pagina scaffold generica │ ├── pipes/ │ │ └── rome-date-time.pipe.ts # Pipe per fuso orario Roma │ └── utils/ @@ -310,7 +323,7 @@ caption: [Architettura del frontend `notip-frontend`], )[ #align(center)[ - #image("./assets/01-app-architecture.svg", width: 115%) + #image("assets/01-app-architecture.svg") ] ] @@ -881,7 +894,7 @@ caption: [Diagramma dei servizi core], )[ #align(center)[ - #image("./assets/05-core-services.svg", width: 115%) + #image("assets/05-core-services.svg") ] ] @@ -927,14 +940,6 @@ - *Query mode*: query paginata con cursore composito `(time, sensorId)`. Supporta filtri per gatewayIds, sensorTypes, sensorIds, e range temporale (con limite finestra 24h). - #figure( - caption: [Diagramma della dashboard], - )[ - #align(center)[ - #image("./assets/07-dashboard.svg", width: 115%) - ] - ] - == FilterPanelComponent `FilterPanelComponent` (`src/app/features/dashboard/components/filter-panel/`) fornisce i controlli di filtro: @@ -990,6 +995,14 @@ composito; - *Export*: `ValidatedMeasureFacadeService.export()` in modalità clear, con limite finestra 24h. + #figure( + caption: [Diagramma della dashboard], + )[ + #align(center)[ + #image("./assets/07-dashboard.svg") + ] + ] + = Feature: Gateway La gestione dei gateway consente agli utenti tenant di visualizzare, rinominare, configurare e inviare comandi ai @@ -1114,7 +1127,7 @@ caption: [Diagramma della feature Gateway], )[ #align(center)[ - #image("./assets/09-gateways.svg", width: 115%) + #image("assets/09-gateways.svg") ] ] @@ -1237,6 +1250,14 @@ ) ] + #figure( + caption: [Diagramma della feature Admin], + )[ + #align(center)[ + #image("assets/03-admin.svg") + ] + ] + = Feature: Alerts La gestione degli alert consente di configurare e consultare gli alert di gateway offline. @@ -1304,6 +1325,14 @@ ) ] + #figure( + caption: [Diagramma della feature Alerts], + )[ + #align(center)[ + #image("assets/04-alerts.svg") + ] + ] + = Feature: Sensors == SensorListPageComponent @@ -1347,6 +1376,14 @@ ) ] + #figure( + caption: [Diagramma della feature Sensors], + )[ + #align(center)[ + #image("assets/06-sensors.svg") + ] + ] + = Feature: Management (Tenant Admin) Le funzionalità di gestione tenant sono accessibili ai `tenant_admin`. @@ -1459,6 +1496,14 @@ ) ] + #figure( + caption: [Diagramma della feature Management], + )[ + #align(center)[ + #image("assets/02-management.svg") + ] + ] + = Componenti shared I componenti shared sono riutilizzati trasversalmente nell'applicazione. @@ -1491,9 +1536,6 @@ [Modale di conferma cancellazione con titolo e messaggio personalizzabili. Bottone confirm (pericolo, rosso) e cancel (ghost). Stato busy durante l'operazione.], - [ImpersonationBannerComponent], - [Banner condizionale visualizzato durante l'impersonazione. Mostra ID utente target (fallback: "unknown").], - [ImpersonationTagComponent], [Tag statico "OBFUSCATED MODE" visualizzato in contesti di dati offuscati.], [ProfileSectionComponent], @@ -1501,10 +1543,6 @@ password".], [LogoutButtonComponent], [Bottone logout che emette evento click. Delega il logout ad AuthService.], - - [PlaceholderPageComponent], - [Pagina scaffold generica per feature in sviluppo. Legge il titolo dai `data['title']` della rotta, con fallback a - "NoTIP".], ) ] @@ -1512,7 +1550,7 @@ caption: [Diagramma dei componenti shared], )[ #align(center)[ - #image("./assets/10-shared-components.svg", width: 115%) + #image("./assets/10-shared-components.svg") ] ]