feat(ui): Hub Navigation, Profile Dashboard and Auth Stability Fixes (#31)

This PR implements the Hub Navigation system and the Profile Dashboard, while resolving critical session synchronization issues.

### Key Changes
- **Hub Navigation**: Introduced `MainHubLayout` with a premium glassmorphism sidebar, providing access to Dashboard, Library, Concepts Map, and Profile.
- **Profile Dashboard**: Implemented a high-fidelity Profile page (#27) with learning metrics, AI token usage tracking, and system rank visualization.
- **Stability Fixes**:
    - Resolved an infinite network loop on the `/profile` page by implementing request deduplication and in-memory caching in `IdentityService`.
    - Added environment-aware guards to prevent illegal JavaScript interop calls during server-side prerendering.
    - Implemented automatic session invalidation on `401 Unauthorized` responses to handle stale authentication states gracefully.
- **Reader Integration**: Added a "Return to Dashboard" option in the reader toolbar (#26).

Closes #26
Closes #27

Reviewed-on: #31
Co-authored-by: Marek Jasiński <jasins.marek@gmail.com>
Co-committed-by: Marek Jasiński <jasins.marek@gmail.com>
This commit was merged in pull request #31.
This commit is contained in:
2026-05-10 17:36:35 +00:00
committed by Marek Jaisński
parent 34794db209
commit 2e23a032d3
56 changed files with 4292 additions and 481 deletions
@@ -4,15 +4,24 @@ using FluentResults;
using NexusReader.Application.Abstractions.Services;
using NexusReader.Application.Queries.Reader;
using VersOne.Epub;
using Microsoft.EntityFrameworkCore;
using NexusReader.Data.Persistence;
using NexusReader.Domain.Entities;
namespace NexusReader.Infrastructure.Services;
public class EpubService : IEpubService
{
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
private const string EpubPath = "wwwroot/assets/book.epub";
private const int WordThreshold = 1000;
public async Task<Result<ReaderPageViewModel>> GetEpubContentAsync(int chapterIndex)
public EpubService(IDbContextFactory<AppDbContext> dbContextFactory)
{
_dbContextFactory = dbContextFactory;
}
public async Task<Result<ReaderPageViewModel>> GetEpubContentAsync(int chapterIndex, string? userId = null)
{
try
{
@@ -100,7 +109,29 @@ public class EpubService : IEpubService
blocks.Add(CreateAiTrigger($"trigger-{blockCounter++}"));
}
return Result.Ok(new ReaderPageViewModel(blocks, chapterIndex, readingOrder.Count, chapterTitle));
// Find the EbookId from DB for this file AND this user
using var context = await _dbContextFactory.CreateDbContextAsync();
var ebook = await context.Ebooks
.Where(e => e.FilePath.Contains("book.epub") && (userId == null || e.UserId == userId))
.FirstOrDefaultAsync();
// Auto-provision if not found for this user (convenience for dev)
if (ebook == null && !string.IsNullOrEmpty(userId))
{
var author = await context.Authors.FirstOrDefaultAsync() ?? new Author { Name = "Unknown Author" };
ebook = new Ebook
{
Title = "Lives of the Most Excellent Painters, Sculptors, and Architects",
FilePath = "wwwroot/assets/book.epub",
UserId = userId,
Author = author,
TenantId = "global"
};
context.Ebooks.Add(ebook);
await context.SaveChangesAsync();
}
return Result.Ok(new ReaderPageViewModel(blocks, chapterIndex, readingOrder.Count, chapterTitle, ebook?.Id ?? Guid.Empty));
}
catch (Exception ex)
{