From 5a6c951641e9b3e4ce9a0d8d810614120406331c Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 22 Oct 2026 04:55:44 +0200 Subject: [PATCH 1/3] docs: codify C# functional style, low branching, try-catch avoidance, and Clean/Vertical Slice/SOLID/DRY balance in GEMINI.md --- GEMINI.md | 164 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 108 insertions(+), 56 deletions(-) diff --git a/GEMINI.md b/GEMINI.md index 55a6552..62fcaf4 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -955,64 +955,116 @@ PostFundManagement.Infrastructure PostFundManagement.Domain └── Migrations/ └── Abstractions/ # IEntity, IAggregateRoot, IService ``` -### 16.2 C# 13 & .NET 10 Coding Conventions +### 16.2 Architectural Balance: Clean Architecture, Vertical Slices, SOLID & DRY -1. **Primary Constructors & Modern Types**: - * Services and endpoints use primary constructors: - ```csharp - public sealed class ProjectService( - IDbContextFactory contextFactory, - HashService hashService, - ITenantContextService tenantContext) : IProjectService - ``` - * Extensive use of expression-bodied members (`=>`) for concise method definitions. - * Nullable reference types (`enable`) strictly enforced. -2. **Functional Result Pattern (`FluentResults`)**: - * Domain and Application services **never throw exceptions for predictable business validation failures**. - * Methods return `Task` or `Task>`. - * Standard error objects in `PostFundManagement.Domain.Errors`: - * `ValidationError`: Mandatory field missing, format invalid, or constraint violation. - * `NotFoundError`: Entity ID not found in database. - * `UpdateError`: Mutation failed or produced zero affected rows. - * `CreateError`: Entity creation failed during persistence. - * `IdDecodeError`: HashID token corrupted, invalid salt, or tampering detected. -3. **Unified Exception-to-Result Wrapping**: - * Service methods wrap their implementation inside `Result.Try`: - ```csharp - public async Task> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default) => - await Result.Try((Func>>)(async () => - { - // Execution logic... - })); - ``` - * Catches unhandled exceptions and converts them safely into `Result.Fail` without crashing the thread or leaking raw stack traces. -4. **Pattern-Matching Endpoint Handlers**: - * Minimal API endpoints map `Result` directly to HTTP status codes via C# switch expressions: - ```csharp - await service.UpdateProjectStatusAsync(...) switch - { - { IsSuccess: true } => Results.Ok(), - var r when r.HasError() => Results.Forbid(), - var r when r.HasError() => Results.NotFound(r.Errors.ToProblems()), - var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), - var r => Results.InternalServerError(r.Errors.ToProblems()) - } - ``` -5. **High-Concurrency DbContext Management**: - * Services inject `IDbContextFactory` rather than scoped `ApplicationDbContext`. - * Creates short-lived contexts using `await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);`. - * Avoids thread concurrency issues in parallel mediator requests and background Quartz jobs. -6. **Query & Mutation Optimizations**: - * Queries use `.AsNoTracking()` to eliminate EF Core tracking overhead. - * Targeted updates leverage `.ExecuteUpdateAsync(...)` for in-database bulk updates without loading full entities into memory. -7. **Tiered Hybrid Caching**: - * Implements `Microsoft.Extensions.Caching.Hybrid` alongside StackExchange Redis. - * Local L1 memory cache: 30-minute expiration. - * Distributed L2 Redis cache: 2-minute expiration (`InstanceName = "PFM-API_"`). +The PFM backend strikes a deliberate, pragmatic balance between **Clean Architecture**, **Vertical Slice Architecture**, **SOLID principles**, and the **DRY (Don't Repeat Yourself)** principle: + +1. **Clean Architecture with Vertical Slices**: + * **Domain Layer**: Completely isolated aggregate roots, entities, taxonomies (`Enums`), domain errors (`IError`), and abstractions. It has no dependencies on database technologies or presentation frameworks. + * **Infrastructure Layer**: Encapsulates EF Core configurations, PostgreSQL mappings, and data protection. + * **Application & Api Layers by Vertical Slice**: Features are organized cleanly into domain feature slices (`Projects`, `Grants`, `Execution`, `Governance`, `Evidence`, `Communications`, `Users`). Each slice groups its commands/queries, records (`Records.cs`), core service (`[Slice]Service`), hybrid cache decorator (`[Slice]CacheService`), and Minimal API endpoints (`[Action]Endpoint.cs`), preserving strong cohesion without code bloat. + +2. **Strict Adherence to SOLID Design Principles**: + * **Single Responsibility Principle (SRP)**: + * `[Slice]Service`: Implements pure business logic, database transactions, and entity mapping. + * `[Slice]CacheService`: Implements read caching and mutation eviction via `HybridCache`. + * `LoggingInterceptor`: Exclusively manages structured logging upon failure. + * `AuditInterceptor`: Exclusively manages regulatory audit logging upon success (`BR-007`). + * `[Action]Endpoint`: Exclusively handles route binding, OpenAPI documentation, and HTTP mapping. + * **Open/Closed Principle (OCP)**: New cross-cutting behaviors (auditing, metrics, tracing) and endpoints are added via decorators, interceptors, and `IEndpoint` implementations without altering existing service logic. + * **Liskov Substitution Principle (LSP)**: `[Slice]CacheService` transparently decorates and substitutes `[Slice]Service` across all consumers via `I[Slice]Service`. + * **Interface Segregation Principle (ISP)**: Lean, dedicated service interfaces per domain slice rather than bloated monolithic service contracts. + * **Dependency Inversion Principle (DIP)**: Endpoints and services depend exclusively on abstractions (`IDbContextFactory`, `ITenantContextService`, `HashService`, service interfaces) rather than concrete implementations. + +3. **DRY Principle via AOP & Functional Utilities**: + * **Zero Cross-Cutting Boilerplate**: Developers never write repetitive logging or auditing code in service methods. Attributes `[Log]` and `[Audit]` handle these concerns declaratively via Castle DynamicProxy. + * **Unified Error & Problem Details**: Domain error records (`ValidationError`, `NotFoundError`, `UpdateError`, `CreateError`, `IdDecodeError`) and extension methods (`ToProblems()`, `ToEndpointName()`) prevent repeated error payload formatting. + * **Shared HashID & Tenant Context**: Centralized `HashService` and `ITenantContextService` guarantee single-point security and tenant isolation enforcement. --- -### 16.3 Logging Strategy Using `LoggingInterceptor` & `[Log]` +### 16.3 C# 13 & .NET 10 Coding Style: Functional Results & Syntax Conventions + +> [!IMPORTANT] +> **Mandatory Assistant Rule (Never Regress or Undo This Approach)**: +> Antigravity / Gemini must strictly adhere to the established functional, low-branching C# style. When adding, modifying, or refactoring backend code: +> - **DO NOT introduce imperative `try-catch` blocks** for control flow or routine domain logic. +> - **DO NOT introduce nested `if-else` return branches**. +> - **DO NOT throw exceptions** for predictable domain validation or mutation failures. +> - **Always use FluentResults** (`Result.Try`, `Result.OkIf`, `Result.FailIf`) and pattern-matching switch expressions on endpoints. + +#### 1. Extensive Avoidance of `try-catch` Blocks +* Imperative `try-catch` blocks are strictly avoided across all application services, endpoints, and domain logic. +* Methods wrap their entire execution functionally inside `Result.Try`: + ```csharp + public async Task> GetProjectAsync(string projectIdHash, CancellationToken cancellationToken = default) => + await Result.Try((Func>>)(async () => + { + // Execution logic without any try-catch... + })); + ``` +* Unexpected runtime exceptions are automatically caught by `Result.Try` and converted into a `Result.Fail` with exception metadata, allowing `LoggingInterceptor` to record errors without leaking raw exceptions or crashing threads. + +#### 2. Minimal Code Branching & Elimination of Nested `if-else` +* Traditional branching constructs (`if-else-if-else`) on returns are eliminated in favor of clean guard clauses and FluentResults combinators: + * **Concise Guard Clauses**: Early validation failure returns: + ```csharp + if (!await context.Users.AnyAsync(u => u.Id == userId, cancellationToken)) + return Result.Fail(NotFoundError.New); + ``` + * **`Result.OkIf` & `Result.FailIf`**: Replace conditional return statements: + ```csharp + // Mutation success check: + return Result.OkIf(changedTotal > 0, UpdateError.Create("No changes detected, update cancelled.")); + + // Existence check with mapper transformation: + return Result.FailIf(project is null, NotFoundError.Create("Project not found")) + .ToResult(project?.Map()!); + ``` + * **Chained Functional Evaluations**: Guard against invalid decodes or missing foreign keys with immediate failure returns instead of deeply indented blocks. + +#### 3. Primary Constructors & Expression-Bodied Members +* Services and endpoints use primary constructors to eliminate constructor assignment boilerplate: + ```csharp + public sealed class ProjectService( + IDbContextFactory contextFactory, + HashService hashService, + ITenantContextService tenantContext) : IProjectService + ``` +* Methods leverage expression bodies (`=>`) to delegate directly to `await Result.Try(...)` or endpoint mappings. +* Nullable reference types (`enable`) are strictly enforced. + +#### 4. Pattern-Matching Endpoint Handlers +* Minimal API endpoints map `Result` directly to HTTP status codes via C# switch expressions, completely eliminating endpoint `if-else` branching: + ```csharp + await service.UpdateProjectStatusAsync(request, context.GetUserId(), cancellationToken) switch + { + { IsSuccess: true } => Results.Ok(), + var r when r.HasError() => Results.Forbid(), + var r when r.HasError() => Results.NotFound(r.Errors.ToProblems()), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r when r.HasError() => Results.BadRequest(r.Errors.ToProblems()), + var r => Results.InternalServerError(r.Errors.ToProblems()) + } + ``` + +#### 5. High-Concurrency DbContext Management +* Services inject `IDbContextFactory` rather than scoped `ApplicationDbContext`. +* Creates short-lived contexts using `await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);`. +* Avoids thread concurrency issues in parallel mediator requests and background Quartz jobs. + +#### 6. Query & Mutation Optimizations +* Queries use `.AsNoTracking()` to eliminate EF Core tracking overhead. +* Targeted updates leverage `.ExecuteUpdateAsync(...)` for in-database bulk updates without loading full entities into memory. + +#### 7. Tiered Hybrid Caching +* Implements `Microsoft.Extensions.Caching.Hybrid` alongside StackExchange Redis. +* Local L1 memory cache: 30-minute expiration. +* Distributed L2 Redis cache: 2-minute expiration (`InstanceName = "PFM-API_"`). + +--- + +### 16.4 Logging Strategy Using `LoggingInterceptor` & `[Log]` The application uses Aspect-Oriented Programming (AOP) via **Castle DynamicProxy** and `Castle.Core.AsyncInterceptor` to decouple logging from core domain logic. @@ -1044,7 +1096,7 @@ The application uses Aspect-Oriented Programming (AOP) via **Castle DynamicProxy --- -### 16.4 Audit Strategy Using `AuditInterceptor`, `[Audit]` & `AuditService` +### 16.5 Audit Strategy Using `AuditInterceptor`, `[Audit]` & `AuditService` The audit trail is a strict regulatory requirement (`BR-007`, `NFR-008`, `AT-12`). Rather than writing manual audit code in every handler, the platform automates audit logging via `AuditInterceptor`. @@ -1074,7 +1126,7 @@ The audit trail is a strict regulatory requirement (`BR-007`, `NFR-008`, `AT-12` --- -### 16.5 AOP Service Registration Pipeline +### 16.6 AOP Service Registration Pipeline All application services are registered through a unified decorator-and-proxy composition in [`PostFundManagement.Application/Extensions/ServiceExtensionTools.cs`](file:///c:/Users/Admin/source/repos/PostFundManagement/PostFundManagement.Application/Extensions/ServiceExtensionTools.cs) and [`Services.cs`](file:///c:/Users/Admin/source/repos/PostFundManagement/PostFundManagement.Application/Extensions/Services.cs): -- 2.47.3 From 87f597c091aefe9e869910589a3ea16e4bfd6298 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 22 Oct 2026 05:01:10 +0200 Subject: [PATCH 2/3] docs: formalize C# formatting, brace discipline, FluentResults vs ternary, grouping, and clean namespace rules in GEMINI.md --- GEMINI.md | 144 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 107 insertions(+), 37 deletions(-) diff --git a/GEMINI.md b/GEMINI.md index 62fcaf4..cd26b55 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -983,15 +983,20 @@ The PFM backend strikes a deliberate, pragmatic balance between **Clean Architec --- -### 16.3 C# 13 & .NET 10 Coding Style: Functional Results & Syntax Conventions +### 16.3 C# 13 & .NET 10 Coding Style: Functional Results, Formatting & Syntax Rules > [!IMPORTANT] -> **Mandatory Assistant Rule (Never Regress or Undo This Approach)**: -> Antigravity / Gemini must strictly adhere to the established functional, low-branching C# style. When adding, modifying, or refactoring backend code: +> **Authoritative Coding Style Rules (Never Regress or Undo This Approach)**: +> Antigravity / Gemini must strictly adhere to the established C# functional idioms, layout, and syntax conventions across all projects. When authoring, refactoring, or extending backend code: > - **DO NOT introduce imperative `try-catch` blocks** for control flow or routine domain logic. > - **DO NOT introduce nested `if-else` return branches**. -> - **DO NOT throw exceptions** for predictable domain validation or mutation failures. -> - **Always use FluentResults** (`Result.Try`, `Result.OkIf`, `Result.FailIf`) and pattern-matching switch expressions on endpoints. +> - **DO NOT use braces `{}` on single-line `if` statements** — braces are reserved exclusively for blocks with more than one line of code. +> - **Prefer FluentResults over ternary operators** (`Result.FailIf(...).ToResult(...)` over `condition ? Result.Fail(...) : Result.Ok(...)`). +> - **Prefer expression-bodied members (`=>`) and switch expressions** over statement bodies and traditional switch statements. +> - **Never split method parameters one-per-line** in method or constructor definitions. +> - **Group related variable declarations and function calls** without empty blank lines between them. +> - **Never leave unused `using` namespaces** in `.cs` files. +> - **Pragmatically avoid magic strings** using constants, enums, or domain errors without obsessive over-engineering. #### 1. Extensive Avoidance of `try-catch` Blocks * Imperative `try-catch` blocks are strictly avoided across all application services, endpoints, and domain logic. @@ -1005,37 +1010,51 @@ The PFM backend strikes a deliberate, pragmatic balance between **Clean Architec ``` * Unexpected runtime exceptions are automatically caught by `Result.Try` and converted into a `Result.Fail` with exception metadata, allowing `LoggingInterceptor` to record errors without leaking raw exceptions or crashing threads. -#### 2. Minimal Code Branching & Elimination of Nested `if-else` -* Traditional branching constructs (`if-else-if-else`) on returns are eliminated in favor of clean guard clauses and FluentResults combinators: - * **Concise Guard Clauses**: Early validation failure returns: - ```csharp - if (!await context.Users.AnyAsync(u => u.Id == userId, cancellationToken)) - return Result.Fail(NotFoundError.New); - ``` - * **`Result.OkIf` & `Result.FailIf`**: Replace conditional return statements: - ```csharp - // Mutation success check: - return Result.OkIf(changedTotal > 0, UpdateError.Create("No changes detected, update cancelled.")); - - // Existence check with mapper transformation: - return Result.FailIf(project is null, NotFoundError.Create("Project not found")) - .ToResult(project?.Map()!); - ``` - * **Chained Functional Evaluations**: Guard against invalid decodes or missing foreign keys with immediate failure returns instead of deeply indented blocks. - -#### 3. Primary Constructors & Expression-Bodied Members -* Services and endpoints use primary constructors to eliminate constructor assignment boilerplate: +#### 2. Minimal Branching, Guard Clauses & If-Statement Brace Discipline +* **No Braces for Single-Line `if`**: Braces `{}` are strictly omitted when the `if` body consists of a single statement/line: ```csharp - public sealed class ProjectService( - IDbContextFactory contextFactory, - HashService hashService, - ITenantContextService tenantContext) : IProjectService - ``` -* Methods leverage expression bodies (`=>`) to delegate directly to `await Result.Try(...)` or endpoint mappings. -* Nullable reference types (`enable`) are strictly enforced. + if (!await context.Users.AnyAsync(u => u.Id == userId, cancellationToken)) + return Result.Fail(NotFoundError.New); -#### 4. Pattern-Matching Endpoint Handlers -* Minimal API endpoints map `Result` directly to HTTP status codes via C# switch expressions, completely eliminating endpoint `if-else` branching: + if (decodeResult.IsFailed) + return Result.Fail(IdDecodeError.Create().CausedBy(decodeResult.Errors)); + ``` +* **Braces Only for Multi-Line Blocks**: Braces `{}` are used **only** when the `if` block encompasses more than one line of code: + ```csharp + if (string.IsNullOrWhiteSpace(cached)) + { + var result = await service.GetActivitiesAsync(projectIdHash, cancellationToken); + if (result.IsFailed) return result; + + await cache.SetStringAsync(cacheKey, result.Value.ToJson(), cacheOptions, cancellationToken); + return result.Value; + } + ``` + +#### 3. FluentResults Monadic Chaining over Ternary Operators +* Whenever processing or transforming a response, **always choose FluentResults methods over ternary operators (`? :`)**: + ```csharp + // Preferred (Fluent monadic style): + return Result.FailIf(project is null, NotFoundError.Create("Project not found")) + .ToResult(project?.Map()!); + + return Result.OkIf(changedTotal > 0, UpdateError.Create("No changes detected, update cancelled.")); + + // Avoid (Ternary branching): + // return project is null ? Result.Fail(...) : Result.Ok(project.Map()); + ``` + +#### 4. Expression-Bodied Members & Switch Expressions +* Methods, mappers, getters, and endpoint registrations strongly prefer expression bodies (`=>`): + ```csharp + public static Activity Map(this Entities.Activity entity) => new() + { + Id = entity.Id, + Title = entity.Title, + // ... + }; + ``` +* Minimal API endpoints map `Result` directly to HTTP status codes via C# pattern-matching switch expressions: ```csharp await service.UpdateProjectStatusAsync(request, context.GetUserId(), cancellationToken) switch { @@ -1048,16 +1067,67 @@ The PFM backend strikes a deliberate, pragmatic balance between **Clean Architec } ``` -#### 5. High-Concurrency DbContext Management +#### 5. Compact Method & Constructor Definitions +* Method signatures are kept compact and readable on one line or cleanly wrapped; **parameters are never split into one line per variable**: + ```csharp + // Preferred: + public async Task UpdateProjectStatusAsync(UpdateProjectStatusRequest request, Guid userId, CancellationToken cancellationToken = default) => + + public sealed class ProjectService(IDbContextFactory contextFactory, HashService hashService, + ITenantContextService tenantContext) : IProjectService + + // Avoid: + // public async Task UpdateProjectStatusAsync( + // UpdateProjectStatusRequest request, + // Guid userId, + // CancellationToken cancellationToken = default) + ``` + +#### 6. Bracket Placement (Allman Style) +* Opening braces `{` are placed on a new line for type declarations, multi-line blocks, lambda bodies in `Result.Try`, and object initializers: + ```csharp + public sealed class ProjectService(...) : IProjectService + { + public async Task MethodAsync(...) => + await Result.Try((Func>)(async () => + { + // ... + })); + } + ``` + +#### 7. Dense Grouping of Related Declarations & Calls +* Related variable declarations are grouped together without blank lines between them. +* Related sequential invocations are kept adjacent without extraneous empty lines: + ```csharp + var name = request.Name.Humanize(LetterCasing.Title).Trim(); + var description = request.Description.Humanize(LetterCasing.Sentence).Trim(); + + var cacheKey = HashService.ToMd5Hash($"project-{id}").Value; + var cached = await cache.GetStringAsync(cacheKey, cancellationToken); + ``` + +#### 8. Clean Namespaces & Pragmatic String Handling +* **Zero Unused Namespaces**: Every `.cs` file maintains clean, minimal `using` directives without stale or unused imports. File-scoped namespaces (`namespace PostFundManagement...;`) are standard. +* **Pragmatic Magic String Avoidance**: Avoid magic strings for business-critical values (use constants in `Constants.cs`, `EndpointTags`, domain enums, or typed domain errors), but remain pragmatic (e.g. human-readable error messages and route patterns need not be over-abstracted into single-use constants). + +#### 9. Functional Programming Orientation +* While C# is multi-paradigm, the codebase leans intentionally into functional programming concepts: + * Immutability via positional records (`public sealed record UpdateProjectStatusRequest(...)`). + * Pure extension mapping functions (`entity.Map()`). + * Declarative LINQ queries and pipeline data transformations. + * Monadic error handling (`Result`, `Result.Try`, `CausedBy`). + +#### 10. High-Concurrency DbContext Management * Services inject `IDbContextFactory` rather than scoped `ApplicationDbContext`. * Creates short-lived contexts using `await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);`. * Avoids thread concurrency issues in parallel mediator requests and background Quartz jobs. -#### 6. Query & Mutation Optimizations +#### 11. Query & Mutation Optimizations * Queries use `.AsNoTracking()` to eliminate EF Core tracking overhead. * Targeted updates leverage `.ExecuteUpdateAsync(...)` for in-database bulk updates without loading full entities into memory. -#### 7. Tiered Hybrid Caching +#### 12. Tiered Hybrid Caching * Implements `Microsoft.Extensions.Caching.Hybrid` alongside StackExchange Redis. * Local L1 memory cache: 30-minute expiration. * Distributed L2 Redis cache: 2-minute expiration (`InstanceName = "PFM-API_"`). -- 2.47.3 From c24e809b88d7f14caf8ed844d5def552f92c6050 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 22 Oct 2026 06:23:22 +0200 Subject: [PATCH 3/3] Applied UI rendering fixes --- .../src/auth/AuthProvider.tsx | 4 +- .../auth/AccountActivationModal.tsx | 11 +- .../auth/PartnerAccessDeniedModal.tsx | 11 +- .../components/auth/SessionExpiredModal.tsx | 11 +- .../src/components/layout/AppLayout.tsx | 25 +++-- .../src/components/ui/ErrorBoundary.tsx | 104 ++++++++++++------ .../src/components/ui/Modal.tsx | 22 +++- .../components/users/UserSecurityModal.tsx | 66 ++++++----- PostFundManagement.ConsoleUI/src/index.css | 21 +++- .../src/pages/SettingsPage.tsx | 97 ++++++++-------- .../src/pages/TenantManagementPage.tsx | 43 ++++---- 11 files changed, 261 insertions(+), 154 deletions(-) diff --git a/PostFundManagement.ConsoleUI/src/auth/AuthProvider.tsx b/PostFundManagement.ConsoleUI/src/auth/AuthProvider.tsx index 7b77dda..91ebb38 100644 --- a/PostFundManagement.ConsoleUI/src/auth/AuthProvider.tsx +++ b/PostFundManagement.ConsoleUI/src/auth/AuthProvider.tsx @@ -64,7 +64,7 @@ interface AuthContextType { authError: string | null; } -const AuthContext = createContext(undefined); +export const AuthContext = createContext(undefined); export const LOCAL_STORAGE_TOKEN_KEY = 'pfm_access_token'; export const LOCAL_STORAGE_USER_KEY = 'pfm_auth_user'; @@ -415,7 +415,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => const urlParams = new URLSearchParams(window.location.search); if (urlParams.has('code') && urlParams.has('state')) { const mgr = getUserManager(); - const oidcUser: OidcUser = await mgr.signinCallback(); + const oidcUser: OidcUser | undefined = await mgr.signinCallback(); if (oidcUser && oidcUser.access_token) { const token = oidcUser.access_token; diff --git a/PostFundManagement.ConsoleUI/src/components/auth/AccountActivationModal.tsx b/PostFundManagement.ConsoleUI/src/components/auth/AccountActivationModal.tsx index d96813f..f91766b 100644 --- a/PostFundManagement.ConsoleUI/src/components/auth/AccountActivationModal.tsx +++ b/PostFundManagement.ConsoleUI/src/components/auth/AccountActivationModal.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import { createPortal } from 'react-dom'; import { useNavigate } from 'react-router-dom'; import { ShieldAlert, @@ -113,16 +114,16 @@ export const AccountActivationModal: React.FC = ({ navigate('/'); }; - return ( + const modalContent = (
{/* Non-dismissable Card Container: No X button, no escape dismiss, no backdrop click */}
e.stopPropagation()} > {/* Amber accent top bar */} @@ -278,4 +279,8 @@ export const AccountActivationModal: React.FC = ({
); + + return typeof document !== 'undefined' + ? createPortal(modalContent, document.body) + : modalContent; }; diff --git a/PostFundManagement.ConsoleUI/src/components/auth/PartnerAccessDeniedModal.tsx b/PostFundManagement.ConsoleUI/src/components/auth/PartnerAccessDeniedModal.tsx index c7c04d8..ce7eb0a 100644 --- a/PostFundManagement.ConsoleUI/src/components/auth/PartnerAccessDeniedModal.tsx +++ b/PostFundManagement.ConsoleUI/src/components/auth/PartnerAccessDeniedModal.tsx @@ -1,4 +1,5 @@ import React, { useEffect } from 'react'; +import { createPortal } from 'react-dom'; import { useNavigate } from 'react-router-dom'; import { ShieldAlert, @@ -54,15 +55,15 @@ export const PartnerAccessDeniedModal: React.FC = navigate('/', { replace: true }); }; - return ( + const modalContent = (
-
+
{/* Glow Header Accent */}
@@ -171,4 +172,8 @@ export const PartnerAccessDeniedModal: React.FC =
); + + return typeof document !== 'undefined' + ? createPortal(modalContent, document.body) + : modalContent; }; diff --git a/PostFundManagement.ConsoleUI/src/components/auth/SessionExpiredModal.tsx b/PostFundManagement.ConsoleUI/src/components/auth/SessionExpiredModal.tsx index 7523ca8..7681a5a 100644 --- a/PostFundManagement.ConsoleUI/src/components/auth/SessionExpiredModal.tsx +++ b/PostFundManagement.ConsoleUI/src/components/auth/SessionExpiredModal.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState, useRef } from 'react'; +import { createPortal } from 'react-dom'; import { ShieldAlert, Clock, ArrowRight, X, AlertTriangle, LogOut } from 'lucide-react'; import { clearSessionAndRedirectToLanding, dismissSubordinateModals } from '../../services/sessionExpiration'; @@ -83,10 +84,10 @@ export const SessionExpiredModal: React.FC = ({ const secondsRemaining = Math.max(0, Math.ceil(timeLeftMs / 1000)); const progressPercent = Math.max(0, Math.min(100, (timeLeftMs / totalDurationMs) * 100)); - return ( + const modalContent = (
setIsPaused(true)} onMouseLeave={() => setIsPaused(false)} role="dialog" @@ -95,7 +96,7 @@ export const SessionExpiredModal: React.FC = ({ >
{/* Subtle decorative top accent bar */}
@@ -183,4 +184,8 @@ export const SessionExpiredModal: React.FC = ({
); + + return typeof document !== 'undefined' + ? createPortal(modalContent, document.body) + : modalContent; }; diff --git a/PostFundManagement.ConsoleUI/src/components/layout/AppLayout.tsx b/PostFundManagement.ConsoleUI/src/components/layout/AppLayout.tsx index 80b0c2d..c34d60a 100644 --- a/PostFundManagement.ConsoleUI/src/components/layout/AppLayout.tsx +++ b/PostFundManagement.ConsoleUI/src/components/layout/AppLayout.tsx @@ -581,18 +581,21 @@ export const AppLayout: React.FC = () => { {/* Main Content Area */} -
-
-
-
+
+ {/* Ambient Glows & Dot Grid - Isolated inside overflow-hidden to prevent phantom page scrollbars */} +
+
+
+
+
-
+
diff --git a/PostFundManagement.ConsoleUI/src/components/ui/ErrorBoundary.tsx b/PostFundManagement.ConsoleUI/src/components/ui/ErrorBoundary.tsx index 03706f6..0a68b5e 100644 --- a/PostFundManagement.ConsoleUI/src/components/ui/ErrorBoundary.tsx +++ b/PostFundManagement.ConsoleUI/src/components/ui/ErrorBoundary.tsx @@ -1,5 +1,8 @@ -import React, { Component, ErrorInfo, ReactNode } from 'react'; +import React, { Component, ErrorInfo, ReactNode, useContext } from 'react'; +import { Link } from 'react-router-dom'; import { AlertTriangle, RefreshCw, Home } from 'lucide-react'; +import { AuthContext } from '../../auth/AuthProvider'; +import { evaluateUserPermission } from '../../auth/rbac'; interface Props { children: ReactNode; @@ -11,6 +14,70 @@ interface State { error: Error | null; } +interface FallbackContentProps { + fallbackTitle?: string; + error: Error | null; + onReset: () => void; +} + +const ErrorBoundaryFallbackContent: React.FC = ({ + fallbackTitle, + error, + onReset, +}) => { + const auth = useContext(AuthContext); + const roles = auth?.roles || []; + const claims = auth?.claims || {}; + const isUnrestricted = + roles.some((r) => r.toLowerCase().trim() === 'admin') || + claims?.unrestricted === true || + claims?.unrestricted === 'all'; + + const canAccessDashboard = isUnrestricted || evaluateUserPermission(roles, claims, 'dashboard', 'view'); + + return ( +
+
+
+ +
+

+ {fallbackTitle || 'Display Module Encountered an Issue'} +

+

+ {canAccessDashboard + ? 'An unexpected display error occurred while rendering this view. You can reload the view or return to the executive dashboard.' + : 'An unexpected display error occurred while rendering this view. Please try reloading the view.'} +

+ {error?.message && ( +
+ {error.message} +
+ )} +
+ + {canAccessDashboard && ( + + + Dashboard + + )} +
+
+
+ ); +}; + export class ErrorBoundary extends React.Component { declare props: Props; declare state: State; @@ -38,36 +105,11 @@ export class ErrorBoundary extends React.Component { public render() { if (this.state.hasError) { return ( -
-
-
- -
-

- {this.props.fallbackTitle || 'Display Module Encountered an Issue'} -

-

- An unexpected display error occurred while rendering this view. You can reload the view or return to the executive dashboard. -

-
- - - - Dashboard - -
-
-
+ ); } diff --git a/PostFundManagement.ConsoleUI/src/components/ui/Modal.tsx b/PostFundManagement.ConsoleUI/src/components/ui/Modal.tsx index fded33c..343d4a9 100644 --- a/PostFundManagement.ConsoleUI/src/components/ui/Modal.tsx +++ b/PostFundManagement.ConsoleUI/src/components/ui/Modal.tsx @@ -1,4 +1,5 @@ import React, { ReactNode, useEffect } from 'react'; +import { createPortal } from 'react-dom'; import { X } from 'lucide-react'; import { HelpTooltip } from './HelpTooltip'; @@ -60,13 +61,18 @@ export const Modal: React.FC = ({ }; const maxWidthClass = maxWidthClassMap[effectiveSize] || 'max-w-lg'; - return ( -
+ const modalContent = ( +
e.stopPropagation()} > -
+

{title}

@@ -82,14 +88,20 @@ export const Modal: React.FC = ({ {subtitle &&

{subtitle}

}
-
{children}
+
{children}
); + + return typeof document !== 'undefined' + ? createPortal(modalContent, document.body) + : modalContent; }; diff --git a/PostFundManagement.ConsoleUI/src/components/users/UserSecurityModal.tsx b/PostFundManagement.ConsoleUI/src/components/users/UserSecurityModal.tsx index 2a1b48e..a55975b 100644 --- a/PostFundManagement.ConsoleUI/src/components/users/UserSecurityModal.tsx +++ b/PostFundManagement.ConsoleUI/src/components/users/UserSecurityModal.tsx @@ -210,24 +210,6 @@ export const UserSecurityModal: React.FC = ({ onError: (err: any) => toastError('Status Update Failed', err), }); - if (!user) return null; - - const effectiveUser = freshUser || user; - const decodedId = effectiveUser?.idNumber ? decodeIdNumberHex(effectiveUser.idNumber) : null; - const assignedRoleIds = new Set( - (Array.isArray(userRoles) ? userRoles : []).map((r) => r?.id || (r as any)?.Id).filter(Boolean) - ); - const availableRoles = (Array.isArray(systemRoles) ? systemRoles : []).filter((r) => { - if (!r) return false; - const rId = r.id || (r as any).Id; - const rName = r.name || (r as any).Name || ''; - if (rId && assignedRoleIds.has(rId)) return false; - if (hasTenantSelfHelpAccess && (rName.toLowerCase() === 'admin' || String(rId) === '1')) { - return false; - } - return true; - }); - const assignableClaims = React.useMemo(() => { const claimMap = new Map(); if (Array.isArray(systemClaims)) { @@ -255,6 +237,30 @@ export const UserSecurityModal: React.FC = ({ return Array.from(claimMap.values()); }, [systemClaims, hasTenantSelfHelpAccess]); + const assignedRoleIds = React.useMemo(() => { + return new Set( + (Array.isArray(userRoles) ? userRoles : []).map((r) => r?.id || (r as any)?.Id).filter(Boolean) + ); + }, [userRoles]); + + const availableRoles = React.useMemo(() => { + return (Array.isArray(systemRoles) ? systemRoles : []).filter((r) => { + if (!r) return false; + const rId = r.id || (r as any).Id; + const rName = r.name || (r as any).Name || ''; + if (rId && assignedRoleIds.has(rId)) return false; + if (hasTenantSelfHelpAccess && (rName.toLowerCase() === 'admin' || String(rId) === '1')) { + return false; + } + return true; + }); + }, [systemRoles, assignedRoleIds, hasTenantSelfHelpAccess]); + + if (!isOpen || !user) return null; + + const effectiveUser = freshUser || user; + const decodedId = effectiveUser?.idNumber ? decodeIdNumberHex(effectiveUser.idNumber) : null; + return ( = ({ }`} > - Roles ({userRoles.length}) + Roles ({(Array.isArray(userRoles) ? userRoles : []).length})
- {effectiveUser.status === ActivityStatus.Active ? ( + {effectiveUser?.status === ActivityStatus.Active ? (