Files
Mimante/Mimante/Program.cs
Alberto Balbo 690f7e636a Ottimizzazione RAM, UI e sistema di timing aste
- 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
2026-02-07 19:28:30 +01:00

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();