- Ridotto consumo RAM: limiti log, pulizia e compattazione dati aste, timer periodico di cleanup - UI più fluida: cache locale aste, throttling aggiornamenti, refresh log solo se necessario - Nuovo sistema Ticker Loop: timing configurabile, strategie solo vicino alla scadenza, feedback puntate tardive - Migliorato layout e splitter, log visivo, gestione cache HTML - Aggiornata UI impostazioni e fix vari per performance e thread-safety
599 lines
23 KiB
C#
599 lines
23 KiB
C#
using AutoBidder.Services;
|
|
using AutoBidder.Data;
|
|
using AutoBidder.Models;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.AspNetCore.DataProtection;
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
// FORCE ASPNETCORE_URLS to prevent any override
|
|
// Questo garantisce che il container ascolti SEMPRE sulla porta configurata
|
|
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ASPNETCORE_URLS")))
|
|
{
|
|
builder.WebHost.UseUrls("http://+:8080");
|
|
}
|
|
else
|
|
{
|
|
builder.WebHost.UseUrls(Environment.GetEnvironmentVariable("ASPNETCORE_URLS")!);
|
|
}
|
|
|
|
// Configura Kestrel solo per HTTPS opzionale
|
|
// HTTP è gestito da ASPNETCORE_URLS (default: http://+:8080 nel Dockerfile)
|
|
var enableHttps = builder.Configuration.GetValue<bool>("Kestrel:EnableHttps", false);
|
|
|
|
if (enableHttps)
|
|
{
|
|
builder.WebHost.ConfigureKestrel(options =>
|
|
{
|
|
try
|
|
{
|
|
// In produzione, cerca il certificato da configurazione
|
|
var certPath = builder.Configuration["Kestrel:Certificates:Default:Path"];
|
|
var certPassword = builder.Configuration["Kestrel:Certificates:Default:Password"];
|
|
|
|
if (!string.IsNullOrEmpty(certPath) && File.Exists(certPath))
|
|
{
|
|
options.ListenAnyIP(8443, listenOptions =>
|
|
{
|
|
listenOptions.UseHttps(certPath, certPassword);
|
|
Console.WriteLine($"[Kestrel] HTTPS enabled with certificate: {certPath}");
|
|
});
|
|
}
|
|
else if (builder.Environment.IsDevelopment())
|
|
{
|
|
// Certificato di sviluppo SOLO in ambiente Development
|
|
options.ListenAnyIP(5001, listenOptions =>
|
|
{
|
|
listenOptions.UseHttps();
|
|
Console.WriteLine("[Kestrel] HTTPS enabled with development certificate");
|
|
});
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("[Kestrel] HTTPS requested but no certificate found");
|
|
Console.WriteLine("[Kestrel] Running in HTTP-only mode");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[Kestrel] Failed to enable HTTPS: {ex.Message}");
|
|
Console.WriteLine("[Kestrel] Running in HTTP-only mode");
|
|
}
|
|
});
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("[Kestrel] HTTPS disabled - running in HTTP-only mode");
|
|
Console.WriteLine("[Kestrel] Use a reverse proxy (nginx/traefik) for SSL termination");
|
|
Console.WriteLine($"[Kestrel] Listening on: {Environment.GetEnvironmentVariable("ASPNETCORE_URLS") ?? "http://+:8080"}");
|
|
}
|
|
|
|
// Add services to the container
|
|
builder.Services.AddRazorPages();
|
|
builder.Services.AddServerSideBlazor();
|
|
|
|
// ============================================
|
|
// CONFIGURAZIONE DATABASE - Path configurabile via DATA_PATH
|
|
// ============================================
|
|
|
|
// Determina il path base per tutti i database e dati persistenti
|
|
// DATA_PATH può essere impostato nel docker-compose per usare un volume persistente
|
|
var dataBasePath = Environment.GetEnvironmentVariable("DATA_PATH");
|
|
if (string.IsNullOrEmpty(dataBasePath))
|
|
{
|
|
// Fallback: usa directory relativa all'applicazione
|
|
dataBasePath = Path.Combine(AppContext.BaseDirectory, "Data");
|
|
}
|
|
|
|
// Crea directory se non esiste
|
|
if (!Directory.Exists(dataBasePath))
|
|
{
|
|
Directory.CreateDirectory(dataBasePath);
|
|
}
|
|
|
|
Console.WriteLine($"[Startup] Data path: {dataBasePath}");
|
|
|
|
// Configura Data Protection per evitare CryptographicException
|
|
var dataProtectionPath = Path.Combine(dataBasePath, "DataProtection-Keys");
|
|
|
|
if (!Directory.Exists(dataProtectionPath))
|
|
{
|
|
Directory.CreateDirectory(dataProtectionPath);
|
|
}
|
|
|
|
builder.Services.AddDataProtection()
|
|
.PersistKeysToFileSystem(new DirectoryInfo(dataProtectionPath))
|
|
.SetApplicationName("AutoBidder");
|
|
|
|
// Database per Identity (SQLite)
|
|
var identityDbPath = Path.Combine(dataBasePath, "identity.db");
|
|
|
|
builder.Services.AddDbContext<ApplicationDbContext>(options =>
|
|
{
|
|
options.UseSqlite($"Data Source={identityDbPath}");
|
|
});
|
|
|
|
// ASP.NET Core Identity
|
|
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
|
|
{
|
|
// Password settings (SICUREZZA FORTE)
|
|
options.Password.RequireDigit = true;
|
|
options.Password.RequireLowercase = true;
|
|
options.Password.RequireUppercase = true;
|
|
options.Password.RequireNonAlphanumeric = true;
|
|
options.Password.RequiredLength = 12;
|
|
options.Password.RequiredUniqueChars = 4;
|
|
|
|
// Lockout settings (protezione brute-force)
|
|
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
|
|
options.Lockout.MaxFailedAccessAttempts = 5;
|
|
options.Lockout.AllowedForNewUsers = true;
|
|
|
|
// User settings
|
|
options.User.RequireUniqueEmail = false;
|
|
options.SignIn.RequireConfirmedAccount = false;
|
|
})
|
|
.AddEntityFrameworkStores<ApplicationDbContext>()
|
|
.AddDefaultTokenProviders();
|
|
|
|
// Cookie configuration (SICUREZZA TAILSCALE)
|
|
builder.Services.ConfigureApplicationCookie(options =>
|
|
{
|
|
options.Cookie.Name = "AutoBidder.Auth";
|
|
options.Cookie.HttpOnly = true;
|
|
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; // HTTP su Tailscale OK
|
|
options.Cookie.SameSite = SameSiteMode.Lax;
|
|
options.ExpireTimeSpan = TimeSpan.FromDays(7);
|
|
options.SlidingExpiration = true;
|
|
|
|
// Redirect per autenticazione (Razor Pages)
|
|
options.LoginPath = "/Account/Login";
|
|
options.LogoutPath = "/Account/Logout";
|
|
options.AccessDeniedPath = "/Account/Login";
|
|
});
|
|
|
|
// Authorization
|
|
builder.Services.AddAuthorization();
|
|
builder.Services.AddCascadingAuthenticationState();
|
|
|
|
// Configura HTTPS Redirection per produzione
|
|
if (!builder.Environment.IsDevelopment())
|
|
{
|
|
builder.Services.AddHsts(options =>
|
|
{
|
|
options.MaxAge = TimeSpan.FromDays(365);
|
|
options.IncludeSubDomains = true;
|
|
options.Preload = true;
|
|
});
|
|
}
|
|
|
|
// Registra servizi applicazione come Singleton per condividere stato
|
|
var htmlCacheService = new HtmlCacheService(
|
|
maxConcurrentRequests: 3,
|
|
requestsPerSecond: 5,
|
|
cacheExpiration: TimeSpan.FromMinutes(5),
|
|
maxRetries: 2
|
|
);
|
|
|
|
var bidStrategyService = new BidStrategyService();
|
|
var auctionMonitor = new AuctionMonitor(bidStrategyService);
|
|
htmlCacheService.OnLog += (msg) => Console.WriteLine(msg);
|
|
|
|
builder.Services.AddSingleton(bidStrategyService);
|
|
builder.Services.AddSingleton(auctionMonitor);
|
|
builder.Services.AddSingleton(htmlCacheService);
|
|
builder.Services.AddSingleton(sp => new SessionService(auctionMonitor.GetApiClient()));
|
|
builder.Services.AddSingleton<DatabaseService>();
|
|
builder.Services.AddSingleton<ApplicationStateService>();
|
|
builder.Services.AddSingleton<BidooBrowserService>();
|
|
builder.Services.AddScoped<StatsService>();
|
|
builder.Services.AddScoped<AuctionStateService>();
|
|
|
|
// Configura SignalR per real-time updates
|
|
builder.Services.AddSignalR(options =>
|
|
{
|
|
options.MaximumReceiveMessageSize = 102400; // 100KB
|
|
options.EnableDetailedErrors = true;
|
|
});
|
|
|
|
var app = builder.Build();
|
|
|
|
// ============================================
|
|
// INIZIALIZZAZIONE DATABASE IDENTITY
|
|
// ============================================
|
|
using (var scope = app.Services.CreateScope())
|
|
{
|
|
try
|
|
{
|
|
var identityDb = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
|
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
|
|
|
|
// Crea database Identity
|
|
await identityDb.Database.EnsureCreatedAsync();
|
|
Console.WriteLine("[Identity] Database initialized");
|
|
|
|
// Crea utente admin se non esiste
|
|
var adminUsername = Environment.GetEnvironmentVariable("ADMIN_USERNAME") ?? "admin";
|
|
var adminPassword = Environment.GetEnvironmentVariable("ADMIN_PASSWORD");
|
|
|
|
// Password di default se non configurata (stessa per debug e container)
|
|
if (string.IsNullOrEmpty(adminPassword))
|
|
{
|
|
adminPassword = "Admin@Password123!";
|
|
}
|
|
|
|
var existingAdmin = await userManager.FindByNameAsync(adminUsername);
|
|
if (existingAdmin == null)
|
|
{
|
|
var adminUser = new ApplicationUser
|
|
{
|
|
UserName = adminUsername,
|
|
Email = $"{adminUsername}@autobidder.local",
|
|
EmailConfirmed = true,
|
|
IsActive = true,
|
|
CreatedAt = DateTime.UtcNow
|
|
};
|
|
|
|
var result = await userManager.CreateAsync(adminUser, adminPassword);
|
|
if (result.Succeeded)
|
|
{
|
|
Console.WriteLine($"[Identity] Admin user created: {adminUsername}");
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine($"[Identity] Failed to create admin user: {string.Join(", ", result.Errors.Select(e => e.Description))}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine($"[Identity] Admin user exists: {adminUsername}");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[Identity] Initialization error: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
// ??? NUOVO: Inizializza DatabaseService
|
|
using (var scope = app.Services.CreateScope())
|
|
{
|
|
var databaseService = scope.ServiceProvider.GetRequiredService<DatabaseService>();
|
|
|
|
try
|
|
{
|
|
Console.WriteLine("[DB] Initializing main database...");
|
|
await databaseService.InitializeDatabaseAsync();
|
|
|
|
var dbInfo = await databaseService.GetDatabaseInfoAsync();
|
|
Console.WriteLine($"[DB] Database initialized successfully:");
|
|
Console.WriteLine($"[DB] Path: {dbInfo.Path}");
|
|
Console.WriteLine($"[DB] Size: {dbInfo.SizeFormatted}");
|
|
Console.WriteLine($"[DB] Version: {dbInfo.Version}");
|
|
Console.WriteLine($"[DB] Auctions: {dbInfo.AuctionsCount}");
|
|
Console.WriteLine($"[DB] Bid History: {dbInfo.BidHistoryCount}");
|
|
Console.WriteLine($"[DB] Product Stats: {dbInfo.ProductStatsCount}");
|
|
|
|
// Verifica salute database
|
|
var isHealthy = await databaseService.CheckDatabaseHealthAsync();
|
|
Console.WriteLine($"[DB] Database health check: {(isHealthy ? "OK" : "FAILED")}");
|
|
|
|
// 🔥 MANUTENZIONE AUTOMATICA DATABASE
|
|
var settings = AutoBidder.Utilities.SettingsManager.Load();
|
|
|
|
if (settings.DatabaseAutoCleanupDuplicates)
|
|
{
|
|
Console.WriteLine("[DB] Checking for duplicate records...");
|
|
var duplicateCount = await databaseService.CountDuplicateAuctionResultsAsync();
|
|
if (duplicateCount > 0)
|
|
{
|
|
Console.WriteLine($"[DB] Found {duplicateCount} duplicates - removing...");
|
|
var removed = await databaseService.RemoveDuplicateAuctionResultsAsync();
|
|
Console.WriteLine($"[DB] ✓ Removed {removed} duplicate auction results");
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("[DB] ✓ No duplicates found");
|
|
}
|
|
}
|
|
|
|
if (settings.DatabaseAutoCleanupIncomplete)
|
|
{
|
|
Console.WriteLine("[DB] Checking for incomplete records...");
|
|
var incompleteCount = await databaseService.CountIncompleteAuctionResultsAsync();
|
|
if (incompleteCount > 0)
|
|
{
|
|
Console.WriteLine($"[DB] Found {incompleteCount} incomplete records - removing...");
|
|
var removed = await databaseService.RemoveIncompleteAuctionResultsAsync();
|
|
Console.WriteLine($"[DB] ✓ Removed {removed} incomplete auction results");
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("[DB] ✓ No incomplete records found");
|
|
}
|
|
}
|
|
|
|
if (settings.DatabaseMaxRetentionDays > 0)
|
|
{
|
|
Console.WriteLine($"[DB] Checking for records older than {settings.DatabaseMaxRetentionDays} days...");
|
|
var oldCount = await databaseService.RemoveOldAuctionResultsAsync(settings.DatabaseMaxRetentionDays);
|
|
if (oldCount > 0)
|
|
{
|
|
Console.WriteLine($"[DB] ✓ Removed {oldCount} old auction results");
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine($"[DB] ✓ No old records to remove");
|
|
}
|
|
}
|
|
|
|
// 🆕 Esegui diagnostica completa se ci sono problemi o se richiesto
|
|
var runDiagnostics = Environment.GetEnvironmentVariable("DB_DIAGNOSTICS")?.ToLower() == "true";
|
|
if (!isHealthy || runDiagnostics)
|
|
{
|
|
Console.WriteLine("[DB] Running full diagnostics...");
|
|
await databaseService.RunDatabaseDiagnosticsAsync();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[DB ERROR] Failed to initialize database: {ex.Message}");
|
|
Console.WriteLine($"[DB ERROR] Stack trace: {ex.StackTrace}");
|
|
|
|
// In caso di errore, esegui sempre la diagnostica
|
|
try
|
|
{
|
|
await databaseService.RunDatabaseDiagnosticsAsync();
|
|
}
|
|
catch
|
|
{
|
|
// Ignora errori nella diagnostica stessa
|
|
}
|
|
}
|
|
}
|
|
|
|
// ?? NUOVO: Collega evento OnAuctionCompleted per salvare statistiche
|
|
{
|
|
var dbService = app.Services.GetRequiredService<DatabaseService>();
|
|
|
|
|
|
|
|
auctionMonitor.OnAuctionCompleted += async (auction, state, won) =>
|
|
{
|
|
try
|
|
{
|
|
Console.WriteLine($"");
|
|
Console.WriteLine($"╔════════════════════════════════════════════════════════════════");
|
|
Console.WriteLine($"║ [EVENTO] Asta Terminata - Salvataggio Statistiche");
|
|
Console.WriteLine($"╠════════════════════════════════════════════════════════════════");
|
|
Console.WriteLine($"║ Asta: {auction.Name}");
|
|
Console.WriteLine($"║ ID: {auction.AuctionId}");
|
|
Console.WriteLine($"║ Stato: {(won ? "✓ VINTA" : "✗ PERSA")}");
|
|
Console.WriteLine($"╚════════════════════════════════════════════════════════════════");
|
|
Console.WriteLine($"");
|
|
|
|
// Crea un nuovo scope per StatsService (è Scoped)
|
|
using var scope = app.Services.CreateScope();
|
|
var statsService = scope.ServiceProvider.GetRequiredService<StatsService>();
|
|
|
|
await statsService.RecordAuctionCompletedAsync(auction, state, won);
|
|
|
|
// ✅ CORRETTO: Log di successo SOLO se non ci sono eccezioni
|
|
Console.WriteLine($"");
|
|
Console.WriteLine($"[EVENTO] ✓ Asta salvata con successo nel database");
|
|
Console.WriteLine($"");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"");
|
|
Console.WriteLine($"[EVENTO ERROR] ✗ Errore durante salvataggio statistiche:");
|
|
Console.WriteLine($"[EVENTO ERROR] {ex.Message}");
|
|
Console.WriteLine($"[EVENTO ERROR] Stack: {ex.StackTrace}");
|
|
Console.WriteLine($"");
|
|
}
|
|
};
|
|
|
|
Console.WriteLine("[STARTUP] OnAuctionCompleted event handler registered");
|
|
}
|
|
|
|
// ? NUOVO: Ripristina aste salvate e riprendi monitoraggio se configurato
|
|
using (var scope = app.Services.CreateScope())
|
|
{
|
|
try
|
|
{
|
|
Console.WriteLine("[STARTUP] Loading saved auctions...");
|
|
|
|
// Carica impostazioni
|
|
var settings = AutoBidder.Utilities.SettingsManager.Load();
|
|
Console.WriteLine($"[STARTUP] Remember auction states: {settings.RememberAuctionStates}");
|
|
Console.WriteLine($"[STARTUP] Default start on load: {settings.DefaultStartAuctionsOnLoad}");
|
|
|
|
// Carica aste salvate
|
|
var savedAuctions = AutoBidder.Utilities.PersistenceManager.LoadAuctions();
|
|
Console.WriteLine($"[STARTUP] Found {savedAuctions.Count} saved auctions");
|
|
|
|
if (savedAuctions.Count > 0)
|
|
{
|
|
var monitor = scope.ServiceProvider.GetRequiredService<AuctionMonitor>();
|
|
var appState = scope.ServiceProvider.GetRequiredService<ApplicationStateService>();
|
|
|
|
// Aggiungi tutte le aste al monitor E a ApplicationStateService
|
|
foreach (var auction in savedAuctions)
|
|
{
|
|
monitor.AddAuction(auction);
|
|
Console.WriteLine($"[STARTUP] Loaded auction: {auction.Name} (ID: {auction.AuctionId})");
|
|
}
|
|
|
|
// Popola ApplicationStateService con le aste caricate
|
|
appState.SetAuctions(savedAuctions);
|
|
Console.WriteLine($"[STARTUP] Populated ApplicationStateService with {savedAuctions.Count} auctions");
|
|
|
|
// Gestisci comportamento di avvio
|
|
if (settings.RememberAuctionStates)
|
|
{
|
|
// Modalità "Ricorda Stato": mantiene lo stato salvato di ogni asta
|
|
// 🔥 FIX CRITICO: Avvia monitor anche per aste in pausa (IsActive=true)
|
|
var activeAuctions = savedAuctions.Where(a => a.IsActive).ToList();
|
|
var resumeAuctions = savedAuctions.Where(a => a.IsActive && !a.IsPaused).ToList();
|
|
var pausedAuctions = savedAuctions.Where(a => a.IsActive && a.IsPaused).ToList();
|
|
|
|
if (activeAuctions.Any())
|
|
{
|
|
Console.WriteLine($"[STARTUP] Starting monitor for {activeAuctions.Count} active auctions ({resumeAuctions.Count} active, {pausedAuctions.Count} paused)");
|
|
monitor.Start();
|
|
appState.IsMonitoringActive = true;
|
|
|
|
if (pausedAuctions.Any())
|
|
{
|
|
appState.AddLog($"[STARTUP] Ripristinate {resumeAuctions.Count} aste attive + {pausedAuctions.Count} in pausa (polling attivo)");
|
|
}
|
|
else
|
|
{
|
|
appState.AddLog($"[STARTUP] Ripristinato stato salvato: {resumeAuctions.Count} aste attive");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("[STARTUP] No active auctions to resume");
|
|
appState.AddLog("[STARTUP] Nessuna asta attiva salvata");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Modalità "Default": applica DefaultStartAuctionsOnLoad a tutte le aste
|
|
switch (settings.DefaultStartAuctionsOnLoad)
|
|
{
|
|
case "Active":
|
|
// Avvia tutte le aste
|
|
Console.WriteLine("[STARTUP] Starting all auctions (Active mode)");
|
|
foreach (var auction in savedAuctions)
|
|
{
|
|
auction.IsActive = true;
|
|
auction.IsPaused = false;
|
|
}
|
|
monitor.Start();
|
|
appState.IsMonitoringActive = true;
|
|
appState.AddLog($"[AUTO-START] Avviate automaticamente {savedAuctions.Count} aste");
|
|
break;
|
|
|
|
case "Paused":
|
|
// Mette in pausa tutte le aste
|
|
Console.WriteLine("[STARTUP] Starting in paused mode");
|
|
foreach (var auction in savedAuctions)
|
|
{
|
|
auction.IsActive = true;
|
|
auction.IsPaused = true;
|
|
}
|
|
monitor.Start();
|
|
appState.IsMonitoringActive = true;
|
|
appState.AddLog($"[AUTO-START] Aste in pausa: {savedAuctions.Count}");
|
|
break;
|
|
|
|
case "Stopped":
|
|
default:
|
|
// Ferma tutte le aste (default)
|
|
Console.WriteLine("[STARTUP] Starting in stopped mode");
|
|
foreach (var auction in savedAuctions)
|
|
{
|
|
auction.IsActive = false;
|
|
auction.IsPaused = false;
|
|
}
|
|
appState.AddLog($"[STARTUP] Aste fermate all'avvio: {savedAuctions.Count}");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("[STARTUP] No saved auctions found");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[STARTUP ERROR] Failed to load auctions: {ex.Message}");
|
|
Console.WriteLine($"[STARTUP ERROR] Stack trace: {ex.StackTrace}");
|
|
}
|
|
}
|
|
|
|
// Configure the HTTP request pipeline
|
|
if (!app.Environment.IsDevelopment())
|
|
{
|
|
app.UseExceptionHandler("/Error");
|
|
|
|
// Abilita HSTS solo se HTTPS è attivo
|
|
if (enableHttps)
|
|
{
|
|
app.UseHsts();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
app.UseDeveloperExceptionPage();
|
|
}
|
|
|
|
// Abilita HTTPS redirection solo se HTTPS è configurato
|
|
if (enableHttps)
|
|
{
|
|
app.UseHttpsRedirection();
|
|
}
|
|
|
|
app.UseStaticFiles();
|
|
app.UseRouting();
|
|
|
|
// ============================================
|
|
// MIDDLEWARE AUTENTICAZIONE E AUTORIZZAZIONE
|
|
// ============================================
|
|
app.UseAuthentication();
|
|
app.UseAuthorization();
|
|
|
|
app.MapRazorPages(); // ? AGGIUNTO: abilita Razor Pages (Login, Logout)
|
|
app.MapBlazorHub();
|
|
app.MapFallbackToPage("/_Host");
|
|
|
|
// ?????????????????????????????????????????????????????????????????
|
|
// TIMER PULIZIA MEMORIA PERIODICA
|
|
// ?????????????????????????????????????????????????????????????????
|
|
|
|
// Timer per pulizia periodica della memoria (ogni 5 minuti)
|
|
var memoryCleanupTimer = new System.Threading.Timer(async _ =>
|
|
{
|
|
try
|
|
{
|
|
using var scope = app.Services.CreateScope();
|
|
var appState = scope.ServiceProvider.GetRequiredService<ApplicationStateService>();
|
|
var htmlCache = scope.ServiceProvider.GetRequiredService<HtmlCacheService>();
|
|
|
|
// Pulisci cache HTML scaduta
|
|
htmlCache.CleanExpiredCache();
|
|
|
|
// Compatta dati aste completate
|
|
appState.CleanupCompletedAuctions();
|
|
|
|
// Forza garbage collection leggera
|
|
GC.Collect(1, GCCollectionMode.Optimized, false);
|
|
|
|
// Log statistiche memoria
|
|
var stats = appState.GetMemoryStats();
|
|
var memoryMB = GC.GetTotalMemory(false) / 1024.0 / 1024.0;
|
|
Console.WriteLine($"[MEMORY] Cleanup: {stats.AuctionsCount} aste, " +
|
|
$"{stats.TotalBidHistoryEntries} bid history, " +
|
|
$"{stats.TotalRecentBidsEntries} recent bids, " +
|
|
$"{stats.GlobalLogEntries} global log, " +
|
|
$"RAM: {memoryMB:F1}MB");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[MEMORY ERROR] Cleanup failed: {ex.Message}");
|
|
}
|
|
}, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
|
|
|
|
// Assicura che il timer venga disposto quando l'app si chiude
|
|
app.Lifetime.ApplicationStopping.Register(() =>
|
|
{
|
|
Console.WriteLine("[SHUTDOWN] Disposing memory cleanup timer...");
|
|
memoryCleanupTimer.Dispose();
|
|
});
|
|
|
|
app.Run();
|