From 47bffd629fceee609ef1686f66aa7a7cdb52678a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Jasi=C5=84ski?= Date: Fri, 1 May 2026 09:07:26 +0200 Subject: [PATCH] feat: add application preloader, identity roles, and resilient database initialization with automated seeding --- scratch/check_db.cs | 44 +++++++++++ .../Persistence/DbInitializer.cs | 72 ++++++++++++++++++ src/NexusReader.Maui/Main.razor | 6 +- src/NexusReader.Maui/wwwroot/index.html | 7 +- .../Layout/MainLayout.razor | 35 +++++---- src/NexusReader.UI.Shared/wwwroot/app.css | 55 ++++++++++++- src/NexusReader.Web.New/Components/App.razor | 27 ++++++- src/NexusReader.Web.New/Program.cs | 34 ++++++++- src/NexusReader.Web.New/appsettings.json | 3 +- src/NexusReader.Web.New/nexus.db-shm | Bin 32768 -> 0 bytes src/NexusReader.Web.New/nexus.db-wal | Bin 37112 -> 0 bytes 11 files changed, 262 insertions(+), 21 deletions(-) create mode 100644 scratch/check_db.cs create mode 100644 src/NexusReader.Infrastructure/Persistence/DbInitializer.cs delete mode 100644 src/NexusReader.Web.New/nexus.db-shm delete mode 100644 src/NexusReader.Web.New/nexus.db-wal diff --git a/scratch/check_db.cs b/scratch/check_db.cs new file mode 100644 index 0000000..f4fa88f --- /dev/null +++ b/scratch/check_db.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.EntityFrameworkCore; +using NexusReader.Infrastructure.Persistence; +using Microsoft.Extensions.Configuration; +using NexusReader.Domain.Entities; +using System; +using System.Linq; +using System.Threading.Tasks; + +var configuration = new ConfigurationBuilder() + .AddJsonFile("src/NexusReader.Web.New/appsettings.json") + .Build(); + +var services = new ServiceCollection(); +var pgConnectionString = configuration.GetConnectionString("PostgresConnection"); +if (!string.IsNullOrEmpty(pgConnectionString)) +{ + services.AddDbContext(options => options.UseNpgsql(pgConnectionString)); +} +else +{ + services.AddDbContext(options => options.UseSqlite(configuration.GetConnectionString("SqliteConnection"))); +} + +var serviceProvider = services.BuildServiceProvider(); +using var scope = serviceProvider.CreateScope(); +var dbContext = scope.ServiceProvider.GetRequiredService(); + +try +{ + var user = await dbContext.Users.FirstOrDefaultAsync(u => u.Email == "admin@nexus.com"); + if (user == null) + { + Console.WriteLine("User admin@nexus.com NOT FOUND in database."); + } + else + { + Console.WriteLine($"User found: {user.Email}, Id: {user.Id}, EmailConfirmed: {user.EmailConfirmed}"); + } +} +catch (Exception ex) +{ + Console.WriteLine($"Error accessing database: {ex.Message}"); +} diff --git a/src/NexusReader.Infrastructure/Persistence/DbInitializer.cs b/src/NexusReader.Infrastructure/Persistence/DbInitializer.cs new file mode 100644 index 0000000..1170bf3 --- /dev/null +++ b/src/NexusReader.Infrastructure/Persistence/DbInitializer.cs @@ -0,0 +1,72 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using NexusReader.Domain.Entities; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace NexusReader.Infrastructure.Persistence; + +public static class DbInitializer +{ + public static async Task SeedAsync(IServiceProvider serviceProvider) + { + using var scope = serviceProvider.CreateScope(); + var userManager = scope.ServiceProvider.GetRequiredService>(); + var roleManager = scope.ServiceProvider.GetRequiredService>(); + + try + { + Console.WriteLine("[Seeder] Starting database seeding..."); + + // Seed Roles + string[] roleNames = { "Admin", "User" }; + foreach (var roleName in roleNames) + { + var roleExist = await roleManager.RoleExistsAsync(roleName); + if (!roleExist) + { + await roleManager.CreateAsync(new IdentityRole(roleName)); + Console.WriteLine($"[Seeder] Created role: {roleName}"); + } + } + + // Seed Admin User + var adminEmail = "admin@nexus.com"; + var adminUser = await userManager.FindByEmailAsync(adminEmail); + + if (adminUser == null) + { + adminUser = new NexusUser + { + UserName = adminEmail, + Email = adminEmail, + EmailConfirmed = true, + CurrentPlan = "Enterprise", + AITokenLimit = 1000000, + TenantId = Guid.NewGuid() + }; + + var createPowerUser = await userManager.CreateAsync(adminUser, "Admin123!"); + if (createPowerUser.Succeeded) + { + await userManager.AddToRoleAsync(adminUser, "Admin"); + Console.WriteLine($"[Seeder] Admin user created successfully: {adminEmail}"); + } + else + { + var errors = string.Join(", ", createPowerUser.Errors.Select(e => e.Description)); + Console.WriteLine($"[Seeder] Failed to create admin user: {errors}"); + } + } + else + { + Console.WriteLine("[Seeder] Admin user already exists."); + } + } + catch (Exception ex) + { + Console.WriteLine($"[Seeder] Critical error during seeding: {ex.Message}"); + } + } +} diff --git a/src/NexusReader.Maui/Main.razor b/src/NexusReader.Maui/Main.razor index 021c022..89cac43 100644 --- a/src/NexusReader.Maui/Main.razor +++ b/src/NexusReader.Maui/Main.razor @@ -3,7 +3,11 @@ - + + + + + diff --git a/src/NexusReader.Maui/wwwroot/index.html b/src/NexusReader.Maui/wwwroot/index.html index 544600a..e0a77ce 100644 --- a/src/NexusReader.Maui/wwwroot/index.html +++ b/src/NexusReader.Maui/wwwroot/index.html @@ -11,7 +11,12 @@ -
Loading...
+
+
+
+
Nexus Reader
+
+
An unhandled error has occurred. diff --git a/src/NexusReader.UI.Shared/Layout/MainLayout.razor b/src/NexusReader.UI.Shared/Layout/MainLayout.razor index a53e1e3..ab44b8e 100644 --- a/src/NexusReader.UI.Shared/Layout/MainLayout.razor +++ b/src/NexusReader.UI.Shared/Layout/MainLayout.razor @@ -11,18 +11,18 @@ @inject NavigationManager NavigationManager @implements IDisposable -
-
-
- @Body -
- -
+ + +
+
+
+ @Body +
+ +
- + - -
@@ -46,9 +46,18 @@
-
-
-
+
+ + +
+
+
Weryfikacja...
+
+
+ + @Body + +
An unhandled error has occurred. diff --git a/src/NexusReader.UI.Shared/wwwroot/app.css b/src/NexusReader.UI.Shared/wwwroot/app.css index 723e410..df8d6f2 100644 --- a/src/NexusReader.UI.Shared/wwwroot/app.css +++ b/src/NexusReader.UI.Shared/wwwroot/app.css @@ -99,4 +99,57 @@ h1:focus { color: white; margin: 1rem; border-radius: 8px; -} \ No newline at end of file +} + +/* Preloader Styles */ +#app-preloader, .app-preloader { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: radial-gradient(circle at center, #1a1a1a 0%, var(--nexus-bg) 100%); + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + z-index: 9999; + transition: opacity 0.8s ease, visibility 0.8s; +} + +#app-preloader.loaded { + opacity: 0; + visibility: hidden; +} + +.preloader-spinner { + width: 80px; + height: 80px; + border: 3px solid rgba(0, 255, 153, 0.1); + border-top: 3px solid var(--nexus-neon); + border-radius: 50%; + animation: spin 1s cubic-bezier(0.4, 0, 0.2, 1) infinite; + filter: drop-shadow(0 0 10px var(--nexus-neon)); + margin-bottom: 20px; +} + +.preloader-text { + color: var(--nexus-neon); + font-family: var(--nexus-font-sans); + letter-spacing: 4px; + text-transform: uppercase; + font-size: 0.8rem; + font-weight: 500; + animation: pulse 2s infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +@keyframes pulse { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.5; transform: scale(0.95); } +} + \ No newline at end of file diff --git a/src/NexusReader.Web.New/Components/App.razor b/src/NexusReader.Web.New/Components/App.razor index faf4c1e..266dea5 100644 --- a/src/NexusReader.Web.New/Components/App.razor +++ b/src/NexusReader.Web.New/Components/App.razor @@ -5,7 +5,6 @@ - @@ -14,9 +13,35 @@ +
+
+
Nexus Reader
+
+ + diff --git a/src/NexusReader.Web.New/Program.cs b/src/NexusReader.Web.New/Program.cs index c61d952..80a67c6 100644 --- a/src/NexusReader.Web.New/Program.cs +++ b/src/NexusReader.Web.New/Program.cs @@ -81,6 +81,7 @@ builder.Services.AddAuthentication(options => }); builder.Services.AddIdentityApiEndpoints() + .AddRoles() .AddEntityFrameworkStores(); builder.Services.ConfigureApplicationCookie(options => @@ -113,11 +114,38 @@ builder.Services.Configure(options => var app = builder.Build(); -// Ensure Database is initialized +// Ensure Database is initialized and seeded using (var scope = app.Services.CreateScope()) { - var dbContext = scope.ServiceProvider.GetRequiredService(); - await dbContext.Database.MigrateAsync(); + var services = scope.ServiceProvider; + var logger = services.GetRequiredService>(); + var dbContext = services.GetRequiredService(); + + int maxRetries = 5; + int delayMs = 2000; + + for (int i = 0; i < maxRetries; i++) + { + try + { + logger.LogInformation("Próba połączenia z bazą danych (próba {Attempt}/{MaxRetries})...", i + 1, maxRetries); + await dbContext.Database.MigrateAsync(); + await DbInitializer.SeedAsync(services); + logger.LogInformation("Baza danych zainicjowana pomyślnie."); + break; + } + catch (Npgsql.NpgsqlException ex) when (i < maxRetries - 1) + { + logger.LogWarning("Błąd połączenia z bazą danych: {Message}. Ponowna próba za {Delay}ms...", ex.Message, delayMs); + await Task.Delay(delayMs); + delayMs *= 2; // Exponential backoff + } + catch (Exception ex) + { + logger.LogCritical(ex, "Krytyczny błąd podczas inicjalizacji bazy danych."); + throw; + } + } } // Configure the HTTP request pipeline. diff --git a/src/NexusReader.Web.New/appsettings.json b/src/NexusReader.Web.New/appsettings.json index f60ca83..747b806 100644 --- a/src/NexusReader.Web.New/appsettings.json +++ b/src/NexusReader.Web.New/appsettings.json @@ -26,5 +26,6 @@ "Model": "gemini-2.5-flash-lite", "MaxOutputTokens": 8192 } - } + }, + "ApiBaseUrl": "http://localhost:5000" } diff --git a/src/NexusReader.Web.New/nexus.db-shm b/src/NexusReader.Web.New/nexus.db-shm deleted file mode 100644 index ea5e4ded85790ca710b4b7a68292b6584e84ac7f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI)KWYL&6bIn(Pc-Rlgax%vlQJjB0fL1^DhZ_W0v^E|SX+va(lU1u!AqpkH?js> zo4~H$58e(lEW7i*1H5AYl5*Nnjg)#%v5um4^X123G+jJTzE|(-&DYOt{Q35}{Vi^M zKR$KNV?2*nDUbh__;>0@4a&Oghq2H1$>&0V009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5Fk)Dfkt|$ zTTA*OK!5-N0t5&UAV7dX?FCvf!!S=T6ax1r(7nHwbxfcX$kSzo009C72oNAZfB*pk t1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oR{YKr?l!-N3)y{si?VC!PQR diff --git a/src/NexusReader.Web.New/nexus.db-wal b/src/NexusReader.Web.New/nexus.db-wal deleted file mode 100644 index cc93d51ec767f59c53cf65710d678580953adffe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37112 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