Files
Nexus.Reader/src/NexusReader.Infrastructure/DependencyInjection.cs
T
Antigravity c94e8f0acb feat(creator): overhaul Creator flow, editor duplication, and staging setup (#83)
This pull request completely overhauls the Creator editor flow, resolves the editor duplication race condition, aligns layout/styling themes in light and dark mode, and adds Docker staging setups.

### Key Changes
1. **Creator Flow Polish**: Redesigned the editor canvas to prevent double scrolling by delegating overflow to the editor canvas layer, updated styles to a premium aesthetic.
2. **Race Condition Prevention**: Resolved Crepe editor duplication when loading or switching chapters by tracking state via shared window maps (`window.editorCache`, `window.editorStates`) and checking `_lastInitializedEditorId` synchronously in Blazor.
3. **Theme Synchronization**: Integrated explicit theme initialization (`ThemeService.InitializeAsync()`) and anchored CSS isolation selectors to correctly sync with Light (Soft Sepia) and Deep Dark theme preferences.
4. **Staging Automation**: Created staging docker configurations with `--nexus-only` flag to allow iterative development without resetting PG/Neo4j database containers.

---------

Co-authored-by: Marek Jasiński <jasins.marek@gmail.com>
Reviewed-on: #83
Co-authored-by: Antigravity <antigravity@google.com>
Co-committed-by: Antigravity <antigravity@google.com>
2026-06-15 17:15:42 +00:00

166 lines
7.2 KiB
C#

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.AI;
using NexusReader.Application.Common;
using GeminiDotnet;
using GeminiDotnet.Extensions.AI;
using NexusReader.Data.Persistence;
using NexusReader.Application.Abstractions.Messaging;
using NexusReader.Application.Abstractions.Persistence;
using NexusReader.Application.Abstractions.Services;
using NexusReader.Infrastructure.Persistence;
using NexusReader.Infrastructure.RealTime;
using NexusReader.Infrastructure.Services;
using NexusReader.Infrastructure.Configuration;
using Polly;
using Polly.Retry;
using NexusReader.Domain.Entities;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Authorization;
using NexusReader.Application.Security.Authorization;
using Qdrant.Client;
using Neo4j.Driver;
using Hangfire;
using Hangfire.PostgreSql;
namespace NexusReader.Infrastructure;
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
var pgConnectionString = configuration.GetConnectionString("PostgresConnection");
if (!string.IsNullOrEmpty(pgConnectionString))
{
services.AddDbContextFactory<AppDbContext>(options =>
options.UseNpgsql(pgConnectionString),
ServiceLifetime.Scoped);
// Also register a scoped DbContext for repositories that need it
services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(pgConnectionString));
}
else
{
var sqliteConnectionString = configuration.GetConnectionString("SqliteConnection") ?? "Data Source=nexus.db";
services.AddDbContextFactory<AppDbContext>(options =>
options.UseSqlite(sqliteConnectionString),
ServiceLifetime.Scoped);
services.AddDbContext<AppDbContext>(options =>
options.UseSqlite(sqliteConnectionString));
}
// Qdrant Client registration
var qdrantUrl = configuration.GetConnectionString("QdrantConnection") ?? "http://localhost:6334";
var qdrantApiKey = configuration["Qdrant:ApiKey"];
services.AddSingleton<QdrantClient>(sp =>
{
if (!string.IsNullOrEmpty(qdrantApiKey))
{
return new QdrantClient(new Uri(qdrantUrl), apiKey: qdrantApiKey);
}
return new QdrantClient(new Uri(qdrantUrl));
});
// Neo4j Driver registration (supports optional authentication)
var neo4jUrl = configuration.GetConnectionString("Neo4jConnection") ?? "bolt://localhost:7687";
var neo4jUser = configuration["Neo4j:Username"];
var neo4jPass = configuration["Neo4j:Password"];
var neo4jAuth = !string.IsNullOrEmpty(neo4jUser)
? AuthTokens.Basic(neo4jUser, neo4jPass ?? string.Empty)
: AuthTokens.None;
services.AddSingleton<IDriver>(sp => GraphDatabase.Driver(neo4jUrl, neo4jAuth));
// Hangfire registration
if (!string.IsNullOrEmpty(pgConnectionString))
{
services.AddHangfire(config => config
.UseRecommendedSerializerSettings()
.UsePostgreSqlStorage(options => options.UseNpgsqlConnection(pgConnectionString)));
services.AddHangfireServer();
}
services.Configure<AiSettings>(configuration.GetSection(AiSettings.SectionName));
services.Configure<StripeSettings>(configuration.GetSection(StripeSettings.SectionName));
services.Configure<RagMonetizationOptions>(configuration.GetSection(RagMonetizationOptions.SectionName));
services.Configure<HtmlSanitizerSettings>(configuration.GetSection(HtmlSanitizerSettings.SectionName));
var aiSettings = configuration.GetSection(AiSettings.SectionName).Get<AiSettings>() ?? new AiSettings();
if (string.IsNullOrWhiteSpace(aiSettings.ApiKey) || aiSettings.ApiKey == "PLACEHOLDER")
{
Console.WriteLine("[Infrastructure] WARNING: AI API Key is missing or placeholder!");
}
services.AddResiliencePipeline("ai-retry", builder =>
{
builder.AddRetry(new RetryStrategyOptions
{
ShouldHandle = new PredicateBuilder().Handle<Exception>(ex =>
ex.Message.Contains("429") ||
ex.Message.Contains("Too Many Requests") ||
ex.Message.Contains("quota") ||
ex.Message.Contains("503") ||
ex.Message.Contains("ServiceUnavailable") ||
ex.Message.Contains("demand")),
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
MaxRetryAttempts = aiSettings.RetryAttempts,
Delay = TimeSpan.FromSeconds(2)
});
});
services.AddChatClient(new GeminiChatClient(new GeminiClientOptions
{
ApiKey = aiSettings.ApiKey,
ModelId = aiSettings.Model
}));
services.AddEmbeddingGenerator(new GeminiEmbeddingGenerator(new GeminiClientOptions
{
ApiKey = aiSettings.ApiKey,
ModelId = aiSettings.EmbeddingModel ?? "gemini-embedding-001"
}));
// Application-layer service implementations
services.AddScoped<IKnowledgeService, KnowledgeService>();
services.AddTransient<IEpubReader, EpubReaderService>();
services.AddTransient<IEpubMetadataExtractor, EpubMetadataExtractor>();
services.AddTransient<IEpubExtractor, EpubExtractor>();
// Fix #4: Scoped instead of Singleton — BookStorageService uses path resolution
// that is environment-specific and incompatible with Singleton lifetime in MAUI.
services.AddScoped<IBookStorageService, BookStorageService>();
services.AddScoped<IStorageService, LocalStorageService>();
services.AddSingleton<ISanitizerService, HtmlSanitizerService>();
// Fix #1: Ebook repository (scoped, matches AppDbContext lifetime)
services.AddScoped<IEbookRepository, EbookRepository>();
services.AddScoped<IQuizResultRepository, QuizResultRepository>();
services.AddScoped<IConceptsMapReadRepository, ConceptsMapReadRepository>();
services.AddScoped<IUserLibraryStore, UserLibraryStore>();
services.AddScoped<IUserReadingStateStore, UserReadingStateStore>();
services.AddScoped<IVectorSearchStore, VectorSearchStore>();
// Fix #2: SignalR broadcaster (scoped, wraps IHubContext which is itself a singleton wrapper)
services.AddScoped<ISyncBroadcaster, SignalRSyncBroadcaster>();
services.AddAuthorizationCore(options =>
{
options.AddPolicy("ProUser", policy => policy.Requirements.Add(new ProUserRequirement()));
});
services.AddScoped<IAuthorizationHandler, ProUserHandler>();
services.AddScoped<IInfrastructureMarker, InfrastructureMarker>();
return services;
}
public static System.Reflection.Assembly Assembly => typeof(DependencyInjection).Assembly;
}
public interface IInfrastructureMarker { }
internal class InfrastructureMarker : IInfrastructureMarker { }