SelectAuction(auction)"
style="cursor: pointer;">
@@ -107,7 +107,7 @@
@GetPriceDisplay(auction)
@GetTimerDisplay(auction)
@GetLastBidder(auction)
- @GetMyBidsCount(auction)
+ @GetMyBidsCount(auction)
@GetPingDisplay(auction)
@@ -230,6 +230,9 @@
CopyToClipboard(selectedAuction.OriginalUrl)" title="Copia">
+ OpenAuctionInNewTab(selectedAuction.OriginalUrl)" title="Apri in nuova scheda">
+
+
@@ -272,6 +275,31 @@
Verifica asta aperta prima di puntare
+
+
+
+
+ @if (isLoadingRecommendations)
+ {
+
+ Caricamento...
+ }
+ else
+ {
+
+ Applica Limiti Consigliati
+ }
+
+ @if (!string.IsNullOrEmpty(recommendationMessage))
+ {
+
+
+ @recommendationMessage
+
+ }
+
@@ -417,12 +445,14 @@
// Crea una copia locale per evitare modifiche durante l'enumerazione
var recentBidsCopy = selectedAuction.RecentBids.ToList();
+ // Calcola statistiche puntatori
var bidderStats = recentBidsCopy
.GroupBy(b => b.Username)
.Select(g => new { Username = g.Key, Count = g.Count(), IsMe = g.First().IsMyBid })
.OrderByDescending(s => s.Count)
.ToList();
+ var totalBids = recentBidsCopy.Count;
@@ -438,9 +468,9 @@
@for (int i = 0; i < bidderStats.Count; i++)
{
var bidder = bidderStats[i];
- var percentage = (bidder.Count * 100.0 / recentBidsCopy.Count);
+ var percentage = (bidder.Count * 100.0 / totalBids);
- #{i + 1}
+ #@(i + 1)
@bidder.Username
@if (bidder.IsMe)
diff --git a/Mimante/Pages/Index.razor.cs b/Mimante/Pages/Index.razor.cs
index 7251e64..baa4579 100644
--- a/Mimante/Pages/Index.razor.cs
+++ b/Mimante/Pages/Index.razor.cs
@@ -13,6 +13,7 @@ namespace AutoBidder.Pages
{
[Inject] private ApplicationStateService AppState { get; set; } = default!;
[Inject] private HtmlCacheService HtmlCache { get; set; } = default!;
+ [Inject] private StatsService StatsService { get; set; } = default!;
private List auctions => AppState.Auctions.ToList();
private AuctionInfo? selectedAuction
@@ -40,6 +41,11 @@ namespace AutoBidder.Pages
private int sessionRemainingBids;
private double sessionShopCredit;
private int sessionAuctionsWon;
+
+ // Recommended limits
+ private bool isLoadingRecommendations = false;
+ private string? recommendationMessage = null;
+ private bool recommendationSuccess = false;
protected override void OnInitialized()
{
@@ -631,6 +637,19 @@ namespace AutoBidder.Pages
}
}
+ private async Task OpenAuctionInNewTab(string url)
+ {
+ try
+ {
+ await JSRuntime.InvokeVoidAsync("window.open", url, "_blank");
+ AddLog("Asta aperta in nuova scheda");
+ }
+ catch (Exception ex)
+ {
+ AddLog($"Errore apertura: {ex.Message}");
+ }
+ }
+
// Helper methods per stili e classi
private string GetRowClass(AuctionInfo auction)
{
@@ -1086,5 +1105,67 @@ namespace AutoBidder.Pages
await RemoveSelectedAuctionWithConfirm();
}
}
+
+ private async Task ApplyRecommendedLimitsToSelected()
+ {
+ if (selectedAuction == null) return;
+
+ isLoadingRecommendations = true;
+ recommendationMessage = null;
+ StateHasChanged();
+
+ try
+ {
+ var limits = await StatsService.GetRecommendedLimitsAsync(selectedAuction.Name);
+
+ if (limits == null || limits.SampleSize == 0)
+ {
+ recommendationMessage = "Nessun dato storico disponibile per questo prodotto. Completa alcune aste per generare raccomandazioni.";
+ recommendationSuccess = false;
+ }
+ else if (limits.ConfidenceScore < 30)
+ {
+ // Applica comunque ma con avviso
+ selectedAuction.MinPrice = limits.MinPrice;
+ selectedAuction.MaxPrice = limits.MaxPrice;
+ selectedAuction.MinResets = limits.MinResets;
+ selectedAuction.MaxResets = limits.MaxResets;
+ selectedAuction.MaxClicks = limits.MaxBids;
+
+ SaveAuctions();
+
+ recommendationMessage = $"Limiti applicati con bassa confidenza ({limits.ConfidenceScore}%) - basati su {limits.SampleSize} aste";
+ recommendationSuccess = true;
+ }
+ else
+ {
+ // Applica limiti con buona confidenza
+ selectedAuction.MinPrice = limits.MinPrice;
+ selectedAuction.MaxPrice = limits.MaxPrice;
+ selectedAuction.MinResets = limits.MinResets;
+ selectedAuction.MaxResets = limits.MaxResets;
+ selectedAuction.MaxClicks = limits.MaxBids;
+
+ SaveAuctions();
+
+ var hourInfo = limits.BestHourToPlay.HasValue
+ ? $" | Ora migliore: {limits.BestHourToPlay}:00"
+ : "";
+
+ recommendationMessage = $"? Limiti applicati (confidenza {limits.ConfidenceScore}%, {limits.SampleSize} aste){hourInfo}";
+ recommendationSuccess = true;
+ }
+ }
+ catch (Exception ex)
+ {
+ recommendationMessage = $"Errore: {ex.Message}";
+ recommendationSuccess = false;
+ }
+ finally
+ {
+ isLoadingRecommendations = false;
+ StateHasChanged();
+ }
+ }
}
}
diff --git a/Mimante/Pages/Settings.razor b/Mimante/Pages/Settings.razor
index 44ed798..f4d0d84 100644
--- a/Mimante/Pages/Settings.razor
+++ b/Mimante/Pages/Settings.razor
@@ -12,7 +12,7 @@
Impostazioni
- Configura sessione, comportamento aste, limiti e database statistiche.
+ Configura sessione, comportamento aste e limiti.
@@ -208,78 +208,6 @@
-
-
-
-
-
-
-
-
- PostgreSQL per statistiche avanzate; SQLite come fallback.
-
-
-
-
- Usa PostgreSQL per statistiche avanzate
-
-
- @if (settings.UsePostgreSQL)
- {
-
- Connection string
-
-
-
-
-
-
-
- Auto-crea schema se mancante
-
-
-
-
-
- Fallback a SQLite se non disponibile
-
-
-
-
-
-
- @if (isTestingConnection)
- {
-
- Test...
- }
- else
- {
-
- Test connessione
- }
-
-
- @if (!string.IsNullOrEmpty(dbTestResult))
- {
-
-
- @dbTestResult
-
- }
-
- }
-
-
- Salva
-
-
-
-
@@ -300,21 +228,17 @@
@code {
- private string startupLoadMode = "Stopped";
+private string startupLoadMode = "Stopped";
- private string? currentUsername;
- private int remainingBids;
- private string cookieInput = "";
- private string usernameInput = "";
- private string? connectionError;
- private bool isConnecting;
+private string? currentUsername;
+private int remainingBids;
+private string cookieInput = "";
+private string usernameInput = "";
+private string? connectionError;
+private bool isConnecting;
- private bool isTestingConnection;
- private string? dbTestResult;
- private bool dbTestSuccess;
-
- private AutoBidder.Utilities.AppSettings settings = new();
- private System.Threading.Timer? updateTimer;
+private AutoBidder.Utilities.AppSettings settings = new();
+private System.Threading.Timer? updateTimer;
protected override void OnInitialized()
{
@@ -492,55 +416,6 @@
settings.DefaultNewAuctionState = state;
}
- private async Task TestDatabaseConnection()
- {
- isTestingConnection = true;
- dbTestResult = null;
- dbTestSuccess = false;
- StateHasChanged();
-
- try
- {
- var connString = settings.PostgresConnectionString;
-
- if (string.IsNullOrWhiteSpace(connString))
- {
- dbTestResult = "Connection string vuota";
- dbTestSuccess = false;
- return;
- }
-
- await using var conn = new Npgsql.NpgsqlConnection(connString);
- await conn.OpenAsync();
-
- await using var cmd = new Npgsql.NpgsqlCommand("SELECT version()", conn);
- var version = await cmd.ExecuteScalarAsync();
-
- var versionStr = version?.ToString() ?? "";
- var versionNumber = versionStr.Contains("PostgreSQL") ? versionStr.Split(' ')[1] : "Unknown";
-
- dbTestResult = $"Connessione OK (PostgreSQL {versionNumber})";
- dbTestSuccess = true;
-
- await conn.CloseAsync();
- }
- catch (Npgsql.NpgsqlException ex)
- {
- dbTestResult = $"Errore PostgreSQL: {ex.Message}";
- dbTestSuccess = false;
- }
- catch (Exception ex)
- {
- dbTestResult = $"Errore: {ex.Message}";
- dbTestSuccess = false;
- }
- finally
- {
- isTestingConnection = false;
- StateHasChanged();
- }
- }
-
private string GetRemainingBidsClass()
{
if (remainingBids < 50) return "bg-danger";
diff --git a/Mimante/Pages/Statistics.razor b/Mimante/Pages/Statistics.razor
index a38de41..29a7e97 100644
--- a/Mimante/Pages/Statistics.razor
+++ b/Mimante/Pages/Statistics.razor
@@ -1,238 +1,236 @@
@page "/statistics"
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
+@using AutoBidder.Models
+@using AutoBidder.Services
@inject StatsService StatsService
-@inject IJSRuntime JSRuntime
+@inject DatabaseService DatabaseService
Statistiche - AutoBidder
-
-
+
+
Statistiche
-
- @if (isLoading)
- {
-
- }
- else
- {
-
- }
- Aggiorna
-
+ @if (StatsService.IsAvailable)
+ {
+
+ @if (isLoading)
+ {
+
+ }
+ else
+ {
+
+ }
+ Aggiorna
+
+ }
- @if (errorMessage != null)
+ @if (!StatsService.IsAvailable)
{
-
-
@errorMessage
+
+
+
+
+
Statistiche non disponibili
+
Il database per le statistiche non è stato configurato o non è accessibile.
+
+
}
-
- @if (isLoading)
+ else if (isLoading)
{
Caricamento statistiche...
}
- else if (totalStats != null)
- {
-
-
-
-
-
-
-
@totalStats.TotalBidsUsed
-
Puntate Usate
-
€@((totalStats.TotalBidsUsed * 0.20).ToString("F2"))
-
-
-
-
-
-
-
-
@totalStats.TotalAuctionsWon
-
Aste Vinte
-
Win Rate: @totalStats.WinRate.ToString("F1")%
-
-
-
-
-
-
-
-
€@totalStats.TotalSavings.ToString("F2")
-
Risparmio Totale
-
ROI: @roi.ToString("F1")%
-
-
-
-
-
-
-
-
@totalStats.AverageBidsPerAuction.ToString("F1")
-
Puntate/Asta Media
-
Latency: @totalStats.AverageLatency.ToString("F0")ms
-
-
-
-
-
-
-
-
-
- @if (recentResults != null && recentResults.Any())
- {
-
-
-
-
-
-
-
- Asta
- Prezzo Finale
- Puntate
- Risultato
- Risparmio
- Data
-
-
-
- @foreach (var result in recentResults.Take(10))
- {
-
- @result.AuctionName
- €@result.FinalPrice.ToString("F2")
- @result.BidsUsed
-
- @if (result.Won)
- {
- Vinta
- }
- else
- {
- Persa
- }
-
-
- @if (result.Savings.HasValue)
- {
- @((result.Savings.Value > 0 ? "+" : "") + "€" + result.Savings.Value.ToString("F2"))
- }
- else
- {
- -
- }
-
- @DateTime.Parse(result.Timestamp).ToString("dd/MM HH:mm")
-
- }
-
-
-
-
-
- }
- }
else
{
-
-
- Nessuna statistica disponibile. Completa alcune aste per vedere le statistiche.
+
+
+
+
+
+
+ @if (recentAuctions == null || !recentAuctions.Any())
+ {
+
+
+
Nessuna asta terminata salvata
+
+ }
+ else
+ {
+
+
+
+
+ Nome
+ Prezzo
+ Puntate
+ Vincitore
+ Stato
+ Data
+
+
+
+ @foreach (var auction in recentAuctions)
+ {
+
+ @auction.AuctionName
+ €@auction.FinalPrice.ToString("F2")
+ @auction.BidsUsed
+ @(auction.WinnerUsername ?? "-")
+
+ @if (auction.Won)
+ {
+ ? Vinta
+ }
+ else
+ {
+ ? Persa
+ }
+
+ @FormatTimestamp(auction.Timestamp)
+
+ }
+
+
+
+ }
+
+
+
+
+
+
+
+
+
+ @if (products == null || !products.Any())
+ {
+
+
+
Nessun prodotto salvato
+
+ }
+ else
+ {
+
+
+
+
+ Prodotto
+ Aste
+ Win%
+ Limiti €
+ Azioni
+
+
+
+ @foreach (var product in products)
+ {
+ var winRate = product.TotalAuctions > 0
+ ? (product.WonAuctions * 100.0 / product.TotalAuctions)
+ : 0;
+
+
+
+ @product.ProductName
+
+ @product.TotalAuctions totali (@product.WonAuctions vinte)
+
+
+ @product.TotalAuctions
+
+
+
+ @winRate.ToString("F0")%
+
+
+
+ @if (product.RecommendedMinPrice.HasValue && product.RecommendedMaxPrice.HasValue)
+ {
+
+ @product.RecommendedMinPrice.Value.ToString("F2") - @product.RecommendedMaxPrice.Value.ToString("F2")
+
+ }
+ else
+ {
+ -
+ }
+
+
+ @if (product.RecommendedMinPrice.HasValue && product.RecommendedMaxPrice.HasValue)
+ {
+ ApplyLimitsToProduct(product)"
+ title="Applica limiti a tutte le aste di questo prodotto">
+ Applica
+
+ }
+ else
+ {
+ N/D
+ }
+
+
+ }
+
+
+
+ }
+
+
+
}
-
-
@code {
- private TotalStats? totalStats;
- private List
? recentResults;
- private string? errorMessage;
- private bool isLoading = false;
- private double roi = 0;
+private bool isLoading = true;
+private List? recentAuctions;
+private List? products;
+
+[Inject] private AuctionMonitor AuctionMonitor { get; set; } = default!;
+[Inject] private ApplicationStateService AppState { get; set; } = default!;
+[Inject] private IJSRuntime JSRuntime { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
await RefreshStats();
}
- protected override async Task OnAfterRenderAsync(bool firstRender)
- {
- if (firstRender && totalStats != null)
- {
- await RenderCharts();
- }
- }
-
private async Task RefreshStats()
{
+ isLoading = true;
+ StateHasChanged();
+
try
{
- isLoading = true;
- errorMessage = null;
- StateHasChanged();
+ // Carica aste recenti (ultime 50)
+ recentAuctions = await DatabaseService.GetRecentAuctionResultsAsync(50);
- // Carica statistiche
- totalStats = await StatsService.GetTotalStatsAsync();
- roi = await StatsService.CalculateROIAsync();
- recentResults = await StatsService.GetRecentAuctionResultsAsync(20);
-
- // Render grafici dopo il caricamento
- if (totalStats != null)
- {
- await RenderCharts();
- }
+ // Carica prodotti con statistiche
+ products = await DatabaseService.GetAllProductStatisticsAsync();
}
catch (Exception ex)
{
- errorMessage = $"Errore caricamento statistiche: {ex.Message}";
- Console.WriteLine($"[ERROR] Statistics: {ex}");
+ Console.WriteLine($"[Statistics] Error loading data: {ex.Message}");
}
finally
{
@@ -241,26 +239,52 @@
}
}
- private async Task RenderCharts()
+ private string FormatTimestamp(string timestamp)
+ {
+ if (DateTime.TryParse(timestamp, out var dt))
+ {
+ return dt.ToString("dd/MM HH:mm");
+ }
+ return timestamp;
+ }
+
+ private async Task ApplyLimitsToProduct(ProductStatisticsRecord product)
{
try
{
- var chartData = await StatsService.GetChartDataAsync(30);
-
- // Render grafico spesa
- await JSRuntime.InvokeVoidAsync("renderMoneyChart",
- chartData.Labels,
- chartData.MoneySpent,
- chartData.Savings);
-
- // Render grafico wins
- await JSRuntime.InvokeVoidAsync("renderWinsChart",
- totalStats!.TotalAuctionsWon,
- totalStats!.TotalAuctionsLost);
+ // Trova tutte le aste con questo ProductKey nel monitor
+ var matchingAuctions = AppState.Auctions
+ .Where(a => ProductStatisticsService.GenerateProductKey(a.Name) == product.ProductKey)
+ .ToList();
+
+ if (!matchingAuctions.Any())
+ {
+ await JSRuntime.InvokeVoidAsync("alert", $"Nessuna asta trovata per '{product.ProductName}'");
+ return;
+ }
+
+ // Applica i limiti
+ foreach (var auction in matchingAuctions)
+ {
+ auction.MinPrice = product.RecommendedMinPrice ?? 0;
+ auction.MaxPrice = product.RecommendedMaxPrice ?? 0;
+ auction.MinResets = product.RecommendedMinResets ?? 0;
+ auction.MaxResets = product.RecommendedMaxResets ?? 0;
+ }
+
+ // Salva
+ AutoBidder.Utilities.PersistenceManager.SaveAuctions(AppState.Auctions.ToList());
+
+ await JSRuntime.InvokeVoidAsync("alert",
+ $"? Limiti applicati a {matchingAuctions.Count} aste di '{product.ProductName}'\n\n" +
+ $"Min: €{product.RecommendedMinPrice:F2}\n" +
+ $"Max: €{product.RecommendedMaxPrice:F2}\n" +
+ $"Min Reset: {product.RecommendedMinResets}\n" +
+ $"Max Reset: {product.RecommendedMaxResets}");
}
catch (Exception ex)
{
- Console.WriteLine($"[ERROR] Render charts: {ex.Message}");
+ await JSRuntime.InvokeVoidAsync("alert", $"Errore: {ex.Message}");
}
}
}
diff --git a/Mimante/Program.cs b/Mimante/Program.cs
index d889dac..1c49bee 100644
--- a/Mimante/Program.cs
+++ b/Mimante/Program.cs
@@ -1,13 +1,9 @@
-using AutoBidder.Services;
+using AutoBidder.Services;
using AutoBidder.Data;
using AutoBidder.Models;
using Microsoft.EntityFrameworkCore;
-using Microsoft.AspNetCore.Components;
-using Microsoft.AspNetCore.Components.Web;
-using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.DataProtection;
-using System.Data.Common;
var builder = WebApplication.CreateBuilder(args);
@@ -23,7 +19,7 @@ else
}
// Configura Kestrel solo per HTTPS opzionale
-// HTTP è gestito da ASPNETCORE_URLS (default: http://+:8080 nel Dockerfile)
+// HTTP è gestito da ASPNETCORE_URLS (default: http://+:8080 nel Dockerfile)
var enableHttps = builder.Configuration.GetValue("Kestrel:EnableHttps", false);
if (enableHttps)
@@ -77,12 +73,29 @@ else
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(
- Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
- "AutoBidder",
- "DataProtection-Keys"
-);
+var dataProtectionPath = Path.Combine(dataBasePath, "DataProtection-Keys");
if (!Directory.Exists(dataProtectionPath))
{
@@ -93,16 +106,8 @@ builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(dataProtectionPath))
.SetApplicationName("AutoBidder");
-// ============================================
-// CONFIGURAZIONE AUTENTICAZIONE E SICUREZZA
-// ============================================
-
// Database per Identity (SQLite)
-var identityDbPath = Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
- "AutoBidder",
- "identity.db"
-);
+var identityDbPath = Path.Combine(dataBasePath, "identity.db");
builder.Services.AddDbContext(options =>
{
@@ -163,71 +168,6 @@ if (!builder.Environment.IsDevelopment())
});
}
-// Configura Database SQLite per statistiche (fallback locale)
-builder.Services.AddDbContext(options =>
-{
- var dbPath = Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
- "AutoBidder",
- "statistics.db"
- );
-
- // Crea directory se non esiste
- var directory = Path.GetDirectoryName(dbPath);
- if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
- {
- Directory.CreateDirectory(directory);
- }
-
- options.UseSqlite($"Data Source={dbPath}");
-});
-
-// Configura Database PostgreSQL per statistiche avanzate
-var usePostgres = builder.Configuration.GetValue("Database:UsePostgres", false);
-if (usePostgres)
-{
- try
- {
- var connString = builder.Environment.IsProduction()
- ? builder.Configuration.GetConnectionString("PostgresStatsProduction")
- : builder.Configuration.GetConnectionString("PostgresStats");
-
- // Sostituisci variabili ambiente in production
- if (builder.Environment.IsProduction())
- {
- connString = connString?
- .Replace("${POSTGRES_USER}", Environment.GetEnvironmentVariable("POSTGRES_USER") ?? "autobidder")
- .Replace("${POSTGRES_PASSWORD}", Environment.GetEnvironmentVariable("POSTGRES_PASSWORD") ?? "");
- }
-
- if (!string.IsNullOrEmpty(connString))
- {
- builder.Services.AddDbContext(options =>
- {
- options.UseNpgsql(connString, npgsqlOptions =>
- {
- npgsqlOptions.EnableRetryOnFailure(3);
- npgsqlOptions.CommandTimeout(30);
- });
- });
-
- Console.WriteLine("[Startup] PostgreSQL configured for statistics");
- }
- else
- {
- Console.WriteLine("[Startup] PostgreSQL connection string not found - using SQLite only");
- }
- }
- catch (Exception ex)
- {
- Console.WriteLine($"[Startup] PostgreSQL configuration failed: {ex.Message} - using SQLite only");
- }
-}
-else
-{
- Console.WriteLine("[Startup] PostgreSQL disabled in configuration - using SQLite only");
-}
-
// Registra servizi applicazione come Singleton per condividere stato
var htmlCacheService = new HtmlCacheService(
maxConcurrentRequests: 3,
@@ -245,23 +185,7 @@ builder.Services.AddSingleton(sp => new SessionService(auctionMonitor.GetApiClie
builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
-builder.Services.AddScoped(sp =>
-{
- var db = sp.GetRequiredService();
-
- // Prova a ottenere PostgreSQL context (potrebbe essere null)
- AutoBidder.Data.PostgresStatsContext? postgresDb = null;
- try
- {
- postgresDb = sp.GetService();
- }
- catch
- {
- // PostgreSQL non disponibile, usa solo SQLite
- }
-
- return new StatsService(db, postgresDb);
-});
+builder.Services.AddScoped();
builder.Services.AddScoped();
// Configura SignalR per real-time updates
@@ -352,139 +276,77 @@ using (var scope = app.Services.CreateScope())
// Verifica salute database
var isHealthy = await databaseService.CheckDatabaseHealthAsync();
Console.WriteLine($"[DB] Database health check: {(isHealthy ? "OK" : "FAILED")}");
+
+ // 🆕 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}");
- }
-}
-
-// Crea database statistiche se non esiste (senza migrations)
-using (var scope = app.Services.CreateScope())
-{
- var db = scope.ServiceProvider.GetRequiredService();
-
- try
- {
- // Log percorso database
- var connection = db.Database.GetDbConnection();
- Console.WriteLine($"[STATS DB] Database path: {connection.DataSource}");
- // Verifica se database esiste
- var dbExists = db.Database.CanConnect();
- Console.WriteLine($"[STATS DB] Database exists: {dbExists}");
-
- // Forza creazione tabelle se non esistono
- if (!dbExists || !db.ProductStats.Any())
- {
- Console.WriteLine("[STATS DB] Creating database schema...");
- db.Database.EnsureDeleted(); // Elimina database vecchio
- db.Database.EnsureCreated(); // Ricrea con schema aggiornato
- Console.WriteLine("[STATS DB] Database schema created successfully");
- }
- else
- {
- Console.WriteLine($"[STATS DB] Database already exists with {db.ProductStats.Count()} records");
- }
- }
- catch (Exception ex)
- {
- Console.WriteLine($"[STATS DB ERROR] Failed to initialize database: {ex.Message}");
- Console.WriteLine($"[STATS DB ERROR] Stack trace: {ex.StackTrace}");
-
- // Prova a ricreare forzatamente
+ // In caso di errore, esegui sempre la diagnostica
try
{
- Console.WriteLine("[STATS DB] Attempting forced recreation...");
- db.Database.EnsureDeleted();
- db.Database.EnsureCreated();
- Console.WriteLine("[STATS DB] Forced recreation successful");
+ await databaseService.RunDatabaseDiagnosticsAsync();
}
- catch (Exception ex2)
+ catch
{
- Console.WriteLine($"[STATS DB ERROR] Forced recreation failed: {ex2.Message}");
+ // Ignora errori nella diagnostica stessa
}
}
}
-// Inizializza PostgreSQL per statistiche avanzate
-using (var scope = app.Services.CreateScope())
+// ?? NUOVO: Collega evento OnAuctionCompleted per salvare statistiche
{
- try
+ var dbService = app.Services.GetRequiredService();
+
+
+
+ auctionMonitor.OnAuctionCompleted += async (auction, state, won) =>
{
- var postgresDb = scope.ServiceProvider.GetService();
-
- if (postgresDb != null)
+ try
{
- Console.WriteLine("[PostgreSQL] Initializing PostgreSQL statistics database...");
+ 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($"");
- var autoCreateSchema = app.Configuration.GetValue("Database:AutoCreateSchema", true);
+ // Crea un nuovo scope per StatsService (è Scoped)
+ using var scope = app.Services.CreateScope();
+ var statsService = scope.ServiceProvider.GetRequiredService();
- if (autoCreateSchema)
- {
- // Usa il metodo EnsureSchemaAsync che gestisce la creazione automatica
- var schemaCreated = await postgresDb.EnsureSchemaAsync();
-
- if (schemaCreated)
- {
- // Valida che tutte le tabelle siano state create
- var schemaValid = await postgresDb.ValidateSchemaAsync();
-
- if (schemaValid)
- {
- Console.WriteLine("[PostgreSQL] Statistics features ENABLED");
- }
- else
- {
- Console.WriteLine("[PostgreSQL] Schema validation failed");
- Console.WriteLine("[PostgreSQL] Statistics features DISABLED (missing tables)");
- }
- }
- else
- {
- Console.WriteLine("[PostgreSQL] Cannot connect to database");
- Console.WriteLine("[PostgreSQL] Statistics features will use SQLite fallback");
- }
- }
- else
- {
- Console.WriteLine("[PostgreSQL] Auto-create schema disabled");
-
- // Prova comunque a validare lo schema esistente
- try
- {
- var schemaValid = await postgresDb.ValidateSchemaAsync();
- if (schemaValid)
- {
- Console.WriteLine("[PostgreSQL] Existing schema validated successfully");
- Console.WriteLine("[PostgreSQL] Statistics features ENABLED");
- }
- else
- {
- Console.WriteLine("[PostgreSQL] Statistics features DISABLED (schema not found)");
- }
- }
- catch
- {
- Console.WriteLine("[PostgreSQL] Statistics features DISABLED (schema not found)");
- }
- }
+ 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($"");
}
- else
+ catch (Exception ex)
{
- Console.WriteLine("[PostgreSQL] Not configured - Statistics will use SQLite only");
+ 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($"");
}
- }
- catch (Exception ex)
- {
- Console.WriteLine($"[PostgreSQL ERROR] Initialization failed: {ex.Message}");
- Console.WriteLine($"[PostgreSQL ERROR] Stack trace: {ex.StackTrace}");
- Console.WriteLine($"[PostgreSQL] Statistics features will use SQLite fallback");
- }
+ };
+
+ Console.WriteLine("[STARTUP] OnAuctionCompleted event handler registered");
}
-// ??? NUOVO: Ripristina aste salvate e riprendi monitoraggio se configurato
+// ? NUOVO: Ripristina aste salvate e riprendi monitoraggio se configurato
using (var scope = app.Services.CreateScope())
{
try
@@ -519,15 +381,26 @@ using (var scope = app.Services.CreateScope())
// Gestisci comportamento di avvio
if (settings.RememberAuctionStates)
{
- // Modalità "Ricorda Stato": mantiene lo stato salvato di ogni asta
- var activeAuctions = savedAuctions.Where(a => a.IsActive && !a.IsPaused).ToList();
+ // 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] Resuming monitoring for {activeAuctions.Count} active auctions");
+ Console.WriteLine($"[STARTUP] Starting monitor for {activeAuctions.Count} active auctions ({resumeAuctions.Count} active, {pausedAuctions.Count} paused)");
monitor.Start();
appState.IsMonitoringActive = true;
- appState.AddLog($"[STARTUP] Ripristinato stato salvato: {activeAuctions.Count} aste attive");
+
+ 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
{
@@ -537,7 +410,7 @@ using (var scope = app.Services.CreateScope())
}
else
{
- // Modalità "Default": applica DefaultStartAuctionsOnLoad a tutte le aste
+ // Modalità "Default": applica DefaultStartAuctionsOnLoad a tutte le aste
switch (settings.DefaultStartAuctionsOnLoad)
{
case "Active":
@@ -597,7 +470,7 @@ if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
- // Abilita HSTS solo se HTTPS è attivo
+ // Abilita HSTS solo se HTTPS è attivo
if (enableHttps)
{
app.UseHsts();
@@ -608,7 +481,7 @@ else
app.UseDeveloperExceptionPage();
}
-// Abilita HTTPS redirection solo se HTTPS è configurato
+// Abilita HTTPS redirection solo se HTTPS è configurato
if (enableHttps)
{
app.UseHttpsRedirection();
diff --git a/Mimante/Services/AuctionMonitor.cs b/Mimante/Services/AuctionMonitor.cs
index 7d82024..b921b3f 100644
--- a/Mimante/Services/AuctionMonitor.cs
+++ b/Mimante/Services/AuctionMonitor.cs
@@ -22,6 +22,12 @@ namespace AutoBidder.Services
public event Action? OnBidExecuted;
public event Action? OnLog;
public event Action? OnResetCountChanged;
+
+ ///
+ /// Evento fired quando un'asta termina (vinta, persa o chiusa).
+ /// Parametri: AuctionInfo, AuctionState finale, bool won (true se vinta dall'utente)
+ ///
+ public event Action? OnAuctionCompleted;
public AuctionMonitor()
{
@@ -101,12 +107,52 @@ namespace AutoBidder.Services
var auction = _auctions.FirstOrDefault(a => a.AuctionId == auctionId);
if (auction != null)
{
+ // ?? Se l'asta è terminata, salva le statistiche prima di rimuoverla
+ if (!auction.IsActive && auction.LastState != null)
+ {
+ OnLog?.Invoke($"[STATS] Asta terminata rilevata: {auction.Name} - Salvataggio statistiche in corso...");
+
+ try
+ {
+ // Determina se è stata vinta dall'utente
+ var won = IsAuctionWonByUser(auction);
+
+ OnLog?.Invoke($"[STATS] Asta {auction.Name} - Stato: {(won ? "VINTA" : "PERSA")}");
+
+ // Emetti evento per salvare le statistiche
+ // Questo trigger sarà gestito in Program.cs con scraping HTML
+ OnAuctionCompleted?.Invoke(auction, auction.LastState, won);
+ }
+ catch (Exception ex)
+ {
+ OnLog?.Invoke($"[STATS ERROR] Errore durante salvataggio statistiche per {auction.Name}: {ex.Message}");
+ }
+ }
+ else
+ {
+ OnLog?.Invoke($"[REMOVE] Rimozione asta non terminata: {auction.Name} (non salvata nelle statistiche)");
+ }
+
_auctions.Remove(auction);
- // ? RIMOSSO: Log ridondante - viene già loggato da MainWindow con più dettagli
- // OnLog?.Invoke($"[-] Asta rimossa: {auction.Name}");
}
}
}
+
+ ///
+ /// Determina se l'asta è stata vinta dall'utente corrente
+ ///
+ private bool IsAuctionWonByUser(AuctionInfo auction)
+ {
+ if (auction.LastState == null) return false;
+
+ var session = _apiClient.GetSession();
+ var username = session?.Username;
+
+ if (string.IsNullOrEmpty(username)) return false;
+
+ // Controlla se l'ultimo puntatore è l'utente
+ return auction.LastState.LastBidder?.Equals(username, StringComparison.OrdinalIgnoreCase) == true;
+ }
public IReadOnlyList GetAuctions()
{
@@ -115,6 +161,62 @@ namespace AutoBidder.Services
return _auctions.ToList();
}
}
+
+ ///
+ /// Applica i limiti consigliati a un'asta specifica
+ ///
+ public bool ApplyLimitsToAuction(string auctionId, double minPrice, double maxPrice, int minResets, int maxResets, int maxBids)
+ {
+ lock (_auctions)
+ {
+ var auction = _auctions.FirstOrDefault(a => a.AuctionId == auctionId);
+ if (auction == null) return false;
+
+ auction.MinPrice = minPrice;
+ auction.MaxPrice = maxPrice;
+ auction.MinResets = minResets;
+ auction.MaxResets = maxResets;
+ auction.MaxClicks = maxBids;
+
+ OnLog?.Invoke($"[LIMITS] Aggiornati limiti per {auction.Name}: MinPrice={minPrice:F2}, MaxPrice={maxPrice:F2}, MinResets={minResets}, MaxResets={maxResets}, MaxBids={maxBids}");
+ return true;
+ }
+ }
+
+ ///
+ /// Applica i limiti consigliati a tutte le aste con lo stesso ProductKey
+ ///
+ public int ApplyLimitsToProductAuctions(string productKey, double minPrice, double maxPrice, int minResets, int maxResets, int maxBids)
+ {
+ int count = 0;
+
+ lock (_auctions)
+ {
+ foreach (var auction in _auctions)
+ {
+ var auctionProductKey = ProductStatisticsService.GenerateProductKey(auction.Name);
+
+ if (auctionProductKey == productKey)
+ {
+ auction.MinPrice = minPrice;
+ auction.MaxPrice = maxPrice;
+ auction.MinResets = minResets;
+ auction.MaxResets = maxResets;
+ auction.MaxClicks = maxBids;
+ count++;
+
+ OnLog?.Invoke($"[LIMITS] Aggiornati limiti per {auction.Name}");
+ }
+ }
+ }
+
+ if (count > 0)
+ {
+ OnLog?.Invoke($"[LIMITS] Applicati limiti a {count} aste con productKey={productKey}");
+ }
+
+ return count;
+ }
public void Start()
{
@@ -247,6 +349,7 @@ namespace AutoBidder.Services
return;
}
+
auction.PollingLatencyMs = state.PollingLatencyMs;
// ? AGGIORNATO: Aggiorna storia puntate mantenendo quelle vecchie
@@ -262,7 +365,10 @@ namespace AutoBidder.Services
string statusMsg = state.Status == AuctionStatus.EndedWon ? "VINTA" :
state.Status == AuctionStatus.EndedLost ? "Persa" : "Chiusa";
+ bool won = state.Status == AuctionStatus.EndedWon;
+
auction.IsActive = false;
+ auction.LastState = state; // Salva stato finale per statistiche
auction.AddLog($"[ASTA TERMINATA] {statusMsg}");
OnLog?.Invoke($"[FINE] [{auction.AuctionId}] Asta {statusMsg} - Polling fermato");
@@ -277,6 +383,18 @@ namespace AutoBidder.Services
});
OnAuctionUpdated?.Invoke(state);
+
+ // ?? Emetti evento per salvare statistiche
+ try
+ {
+ OnAuctionCompleted?.Invoke(auction, state, won);
+ OnLog?.Invoke($"[STATS] Evento OnAuctionCompleted emesso per {auction.Name}");
+ }
+ catch (Exception ex)
+ {
+ OnLog?.Invoke($"[STATS ERROR] Errore in OnAuctionCompleted: {ex.Message}");
+ }
+
return;
}
@@ -473,7 +591,11 @@ namespace AutoBidder.Services
private bool ShouldBid(AuctionInfo auction, AuctionState state)
{
// ?? CONTROLLO 0: Verifica convenienza (se dati disponibili)
- if (auction.CalculatedValue != null &&
+ // ?? IMPORTANTE: Applica solo se BuyNowPrice è valido (> 0)
+ // Se BuyNowPrice == 0, significa errore scraping - non bloccare le puntate
+ if (auction.BuyNowPrice.HasValue &&
+ auction.BuyNowPrice.Value > 0 &&
+ auction.CalculatedValue != null &&
auction.CalculatedValue.Savings.HasValue &&
!auction.CalculatedValue.IsWorthIt)
{
diff --git a/Mimante/Services/BidooBrowserService.cs b/Mimante/Services/BidooBrowserService.cs
index 061deb6..9a5108a 100644
--- a/Mimante/Services/BidooBrowserService.cs
+++ b/Mimante/Services/BidooBrowserService.cs
@@ -219,6 +219,14 @@ namespace AutoBidder.Services
// Parse aste dall'HTML (fragment AJAX)
auctions = ParseAuctionsFromHtml(html);
+ // ?? FIX: Filtra solo aste di puntate se categoria "Aste di Puntate" (TabId = 1)
+ if (category.IsSpecialCategory && category.TabId == 1)
+ {
+ var before = auctions.Count;
+ auctions = auctions.Where(a => a.IsCreditAuction).ToList();
+ Console.WriteLine($"[BidooBrowser] Filtrate {before} -> {auctions.Count} aste di puntate (IsCreditAuction = true)");
+ }
+
Console.WriteLine($"[BidooBrowser] Trovate {auctions.Count} aste nella categoria {category.DisplayName}");
}
catch (Exception ex)
diff --git a/Mimante/Services/DatabaseService.cs b/Mimante/Services/DatabaseService.cs
index e2b18b8..da13c5a 100644
--- a/Mimante/Services/DatabaseService.cs
+++ b/Mimante/Services/DatabaseService.cs
@@ -1,26 +1,81 @@
-using Microsoft.Data.Sqlite;
+using Microsoft.Data.Sqlite;
using System;
using System.IO;
+using System.Collections.Generic;
using System.Threading.Tasks;
+using AutoBidder.Models;
namespace AutoBidder.Services
{
///
- /// Servizio per gestione database SQLite con auto-creazione tabelle e migrations
+ /// Servizio per gestione database SQLite con auto-creazione tabelle e migrations.
+ /// Il path del database può essere configurato tramite variabile ambiente DATA_PATH
+ /// per supportare volumi Docker persistenti.
///
public class DatabaseService : IDisposable
{
private readonly string _connectionString;
private readonly string _databasePath;
+ private bool _isInitialized = false;
+ private bool _isAvailable = false;
+ private string? _initializationError;
+ ///
+ /// Indica se il database è stato inizializzato correttamente
+ ///
+ public bool IsInitialized => _isInitialized;
+
+ ///
+ /// Indica se il database è disponibile e funzionante
+ ///
+ public bool IsAvailable => _isAvailable;
+
+ ///
+ /// Eventuale errore di inizializzazione
+ ///
+ public string? InitializationError => _initializationError;
+
+ ///
+ /// Path del database
+ ///
+ public string DatabasePath => _databasePath;
+
+ ///
+ /// Crea una nuova istanza di DatabaseService.
+ /// Il path del database viene determinato in questo ordine:
+ /// 1. Variabile ambiente DATA_PATH (per Docker)
+ /// 2. Directory "data" relativa all'eseguibile (fallback)
+ ///
public DatabaseService()
{
- // Crea directory data se non esiste
- var dataDir = Path.Combine(AppContext.BaseDirectory, "data");
- Directory.CreateDirectory(dataDir);
+ // Determina il path base per i dati
+ // 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 (coerente con Program.cs)
+ dataBasePath = Path.Combine(AppContext.BaseDirectory, "Data");
+ }
+
+ // Verifica se la directory esiste o può essere creata
+ try
+ {
+ Directory.CreateDirectory(dataBasePath);
+ _isAvailable = true;
+ }
+ catch (Exception ex)
+ {
+ _isAvailable = false;
+ _initializationError = $"Impossibile creare directory dati: {ex.Message}";
+ Console.WriteLine($"[DatabaseService ERROR] {_initializationError}");
+ }
- _databasePath = Path.Combine(dataDir, "autobidder.db");
+ _databasePath = Path.Combine(dataBasePath, "autobidder.db");
_connectionString = $"Data Source={_databasePath}";
+
+ Console.WriteLine($"[DatabaseService] Database path: {_databasePath}");
+ Console.WriteLine($"[DatabaseService] Available: {_isAvailable}");
}
///
@@ -28,21 +83,83 @@ namespace AutoBidder.Services
///
public async Task InitializeDatabaseAsync()
{
- await using var connection = new SqliteConnection(_connectionString);
- await connection.OpenAsync();
-
- // Abilita foreign keys
- await using (var cmd = connection.CreateCommand())
+ if (!_isAvailable)
{
- cmd.CommandText = "PRAGMA foreign_keys = ON;";
- await cmd.ExecuteNonQueryAsync();
+ Console.WriteLine("[DatabaseService] Skipping initialization - database not available");
+ return;
}
- // Crea tabelle se non esistono
- await CreateTablesAsync(connection);
+ try
+ {
+ Console.WriteLine("[DatabaseService] ===== INIZIO INIZIALIZZAZIONE DATABASE =====");
+ Console.WriteLine($"[DatabaseService] Apertura connessione a: {_databasePath}");
+
+ await using var connection = new SqliteConnection(_connectionString);
+ await connection.OpenAsync();
+
+ Console.WriteLine("[DatabaseService] ✓ Connessione aperta con successo");
- // Esegui migrations
- await RunMigrationsAsync(connection);
+ // Abilita foreign keys
+ await using (var cmd = connection.CreateCommand())
+ {
+ cmd.CommandText = "PRAGMA foreign_keys = ON;";
+ await cmd.ExecuteNonQueryAsync();
+ Console.WriteLine("[DatabaseService] ✓ Foreign keys abilitati");
+ }
+
+ // Crea tabelle se non esistono
+ Console.WriteLine("[DatabaseService] Creazione tabelle base...");
+ await CreateTablesAsync(connection);
+ Console.WriteLine("[DatabaseService] ✓ Tabelle base create");
+
+ // Esegui migrations
+ Console.WriteLine("[DatabaseService] Esecuzione migrations...");
+ await RunMigrationsAsync(connection);
+ Console.WriteLine("[DatabaseService] ✓ Migrations completate");
+
+ _isInitialized = true;
+ _isAvailable = true;
+ Console.WriteLine("[DatabaseService] ===== INIZIALIZZAZIONE COMPLETATA =====");
+ }
+ catch (SqliteException sqlEx)
+ {
+ _isInitialized = false;
+ _isAvailable = false;
+ _initializationError = $"SQLite Error: {sqlEx.Message} (Code: {sqlEx.SqliteErrorCode}, Extended: {sqlEx.SqliteExtendedErrorCode})";
+
+ Console.WriteLine("");
+ Console.WriteLine("â•”â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine("â•‘ [DATABASE ERROR] Errore SQLite Durante Inizializzazione");
+ Console.WriteLine("â• â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine($"â•‘ Messaggio: {sqlEx.Message}");
+ Console.WriteLine($"â•‘ SQLite Error Code: {sqlEx.SqliteErrorCode}");
+ Console.WriteLine($"â•‘ SQLite Extended Code: {sqlEx.SqliteExtendedErrorCode}");
+ Console.WriteLine($"â•‘ Database Path: {_databasePath}");
+ Console.WriteLine("â• â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine($"â•‘ Stack Trace:");
+ Console.WriteLine($"{sqlEx.StackTrace}");
+ Console.WriteLine("╚â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine("");
+ }
+ catch (Exception ex)
+ {
+ _isInitialized = false;
+ _isAvailable = false;
+ _initializationError = $"Errore inizializzazione database: {ex.Message}";
+
+ Console.WriteLine("");
+ Console.WriteLine("â•”â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine("â•‘ [DATABASE ERROR] Errore Generico Durante Inizializzazione");
+ Console.WriteLine("â• â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine($"â•‘ Tipo: {ex.GetType().Name}");
+ Console.WriteLine($"â•‘ Messaggio: {ex.Message}");
+ Console.WriteLine($"â•‘ Database Path: {_databasePath}");
+ Console.WriteLine("â• â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine($"â•‘ Stack Trace:");
+ Console.WriteLine($"{ex.StackTrace}");
+ Console.WriteLine("╚â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine("");
+ }
}
///
@@ -122,7 +239,7 @@ namespace AutoBidder.Services
FOREIGN KEY (AuctionId) REFERENCES Auctions(AuctionId) ON DELETE CASCADE
);
- -- Tabella statistiche prodotti (già esistente da StatsService)
+ -- Tabella statistiche prodotti (già esistente da StatsService)
CREATE TABLE IF NOT EXISTS ProductStats (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
ProductKey TEXT NOT NULL,
@@ -155,12 +272,13 @@ namespace AutoBidder.Services
private async Task RunMigrationsAsync(SqliteConnection connection)
{
var currentVersion = await GetDatabaseVersionAsync(connection);
+ Console.WriteLine($"[DatabaseService] Versione database corrente: {currentVersion}");
// Migrations in ordine crescente
var migrations = new[]
{
new Migration(1, "Initial schema", async (conn) => {
- // Schema già creato in CreateTablesAsync
+ // Schema già creato in CreateTablesAsync
await Task.CompletedTask;
}),
@@ -336,6 +454,148 @@ namespace AutoBidder.Services
await using var cmd = conn.CreateCommand();
cmd.CommandText = sql;
await cmd.ExecuteNonQueryAsync();
+ }),
+
+ new Migration(8, "Enhance AuctionResults for analytics", async (conn) => {
+ var sql = @"
+ -- Aggiungi colonne per analytics avanzate
+ ALTER TABLE AuctionResults ADD COLUMN WinnerUsername TEXT;
+ ALTER TABLE AuctionResults ADD COLUMN ClosedAtHour INTEGER;
+ ALTER TABLE AuctionResults ADD COLUMN ProductKey TEXT;
+ ALTER TABLE AuctionResults ADD COLUMN TotalResets INTEGER DEFAULT 0;
+ ALTER TABLE AuctionResults ADD COLUMN WinnerBidsUsed INTEGER;
+
+ -- Indici per query per prodotto e fascia oraria
+ CREATE INDEX IF NOT EXISTS idx_auctionresults_productkey ON AuctionResults(ProductKey);
+ CREATE INDEX IF NOT EXISTS idx_auctionresults_hour ON AuctionResults(ClosedAtHour);
+ ";
+ await using var cmd = conn.CreateCommand();
+ cmd.CommandText = sql;
+ await cmd.ExecuteNonQueryAsync();
+ }),
+
+ new Migration(9, "Create ProductStatistics table for auto-limits", async (conn) => {
+ var sql = @"
+ -- Tabella statistiche aggregate per prodotto
+ CREATE TABLE IF NOT EXISTS ProductStatistics (
+ ProductKey TEXT PRIMARY KEY,
+ ProductName TEXT NOT NULL,
+ TotalAuctions INTEGER NOT NULL DEFAULT 0,
+ WonAuctions INTEGER NOT NULL DEFAULT 0,
+ LostAuctions INTEGER NOT NULL DEFAULT 0,
+ AvgFinalPrice REAL DEFAULT 0,
+ MinFinalPrice REAL,
+ MaxFinalPrice REAL,
+ AvgBidsToWin REAL DEFAULT 0,
+ MinBidsToWin INTEGER,
+ MaxBidsToWin INTEGER,
+ AvgResets REAL DEFAULT 0,
+ MinResets INTEGER,
+ MaxResets INTEGER,
+ -- Limiti consigliati (calcolati dall'algoritmo)
+ RecommendedMinPrice REAL,
+ RecommendedMaxPrice REAL,
+ RecommendedMinResets INTEGER,
+ RecommendedMaxResets INTEGER,
+ RecommendedMaxBids INTEGER,
+ -- Statistiche per fascia oraria (JSON)
+ HourlyStatsJson TEXT,
+ -- Metadata
+ LastUpdated TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ CreatedAt TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
+ );
+
+ -- Indici per performance
+ CREATE INDEX IF NOT EXISTS idx_productstats_name ON ProductStatistics(ProductName);
+ CREATE INDEX IF NOT EXISTS idx_productstats_updated ON ProductStatistics(LastUpdated DESC);
+ ";
+ await using var cmd = conn.CreateCommand();
+ cmd.CommandText = sql;
+ await cmd.ExecuteNonQueryAsync();
+ }),
+
+ new Migration(10, "Add performance indexes for analytics queries", async (conn) => {
+ var sql = @"
+ -- ✅ Indici compositi per query su ProductKey + Won (usato in algoritmo limiti)
+ CREATE INDEX IF NOT EXISTS idx_auctionresults_productkey_won
+ ON AuctionResults(ProductKey, Won);
+
+ -- ✅ Indice per query temporali filtrate per vincita
+ CREATE INDEX IF NOT EXISTS idx_auctionresults_timestamp_won
+ ON AuctionResults(Timestamp DESC, Won);
+
+ -- ✅ Indice per query per fascia oraria
+ CREATE INDEX IF NOT EXISTS idx_auctionresults_productkey_hour
+ ON AuctionResults(ProductKey, ClosedAtHour);
+
+ -- ✅ Indice per ordinamento per prezzo finale
+ CREATE INDEX IF NOT EXISTS idx_auctionresults_productkey_price
+ ON AuctionResults(ProductKey, FinalPrice);
+
+ -- ✅ Indice per query puntate vincitore
+ CREATE INDEX IF NOT EXISTS idx_auctionresults_winnerbids
+ ON AuctionResults(ProductKey, WinnerBidsUsed)
+ WHERE WinnerBidsUsed IS NOT NULL;
+
+ -- ✅ Indice per query reset totali
+ CREATE INDEX IF NOT EXISTS idx_auctionresults_productkey_resets
+ ON AuctionResults(ProductKey, TotalResets)
+ WHERE TotalResets IS NOT NULL;
+ ";
+ await using var cmd = conn.CreateCommand();
+ cmd.CommandText = sql;
+ await cmd.ExecuteNonQueryAsync();
+ }),
+
+ new Migration(11, "Remove foreign key constraint from AuctionResults", async (conn) => {
+ // SQLite non supporta DROP CONSTRAINT, quindi dobbiamo ricreare la tabella
+ var sql = @"
+ -- 1. Rinomina la tabella esistente
+ ALTER TABLE AuctionResults RENAME TO AuctionResults_old;
+
+ -- 2. Ricrea la tabella SENZA foreign key
+ CREATE TABLE AuctionResults (
+ Id INTEGER PRIMARY KEY AUTOINCREMENT,
+ AuctionId TEXT NOT NULL,
+ AuctionName TEXT NOT NULL,
+ FinalPrice REAL NOT NULL,
+ BidsUsed INTEGER NOT NULL,
+ Won INTEGER NOT NULL DEFAULT 0,
+ Timestamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ BuyNowPrice REAL,
+ ShippingCost REAL,
+ TotalCost REAL,
+ Savings REAL,
+ WinnerUsername TEXT,
+ ClosedAtHour INTEGER,
+ ProductKey TEXT,
+ TotalResets INTEGER DEFAULT 0,
+ WinnerBidsUsed INTEGER
+ );
+
+ -- 3. Copia i dati dalla vecchia tabella
+ INSERT INTO AuctionResults
+ SELECT * FROM AuctionResults_old;
+
+ -- 4. Elimina la vecchia tabella
+ DROP TABLE AuctionResults_old;
+
+ -- 5. Ricrea gli indici
+ CREATE INDEX IF NOT EXISTS idx_auctionresults_timestamp ON AuctionResults(Timestamp DESC);
+ CREATE INDEX IF NOT EXISTS idx_auctionresults_won ON AuctionResults(Won);
+ CREATE INDEX IF NOT EXISTS idx_auctionresults_auctionid ON AuctionResults(AuctionId);
+ CREATE INDEX IF NOT EXISTS idx_auctionresults_productkey ON AuctionResults(ProductKey);
+ CREATE INDEX IF NOT EXISTS idx_auctionresults_hour ON AuctionResults(ClosedAtHour);
+ CREATE INDEX IF NOT EXISTS idx_auctionresults_productkey_won ON AuctionResults(ProductKey, Won);
+ CREATE INDEX IF NOT EXISTS idx_auctionresults_timestamp_won ON AuctionResults(Timestamp DESC, Won);
+ CREATE INDEX IF NOT EXISTS idx_auctionresults_productkey_hour ON AuctionResults(ProductKey, ClosedAtHour);
+ CREATE INDEX IF NOT EXISTS idx_auctionresults_productkey_price ON AuctionResults(ProductKey, FinalPrice);
+ CREATE INDEX IF NOT EXISTS idx_auctionresults_winnerbids ON AuctionResults(ProductKey, WinnerBidsUsed) WHERE WinnerBidsUsed IS NOT NULL;
+ CREATE INDEX IF NOT EXISTS idx_auctionresults_productkey_resets ON AuctionResults(ProductKey, TotalResets) WHERE TotalResets IS NOT NULL;
+ ";
+ await using var cmd = conn.CreateCommand();
+ cmd.CommandText = sql;
+ await cmd.ExecuteNonQueryAsync();
})
};
@@ -343,8 +603,50 @@ namespace AutoBidder.Services
{
if (migration.Version > currentVersion)
{
- await migration.Execute(connection);
- await SetDatabaseVersionAsync(connection, migration.Version, migration.Description);
+ try
+ {
+ Console.WriteLine($"[DatabaseService] ┌─ Applicazione Migration {migration.Version}: {migration.Description}");
+ await migration.Execute(connection);
+ await SetDatabaseVersionAsync(connection, migration.Version, migration.Description);
+ Console.WriteLine($"[DatabaseService] └─ ✓ Migration {migration.Version} completata");
+ }
+ catch (SqliteException sqlEx)
+ {
+ Console.WriteLine("");
+ Console.WriteLine($"â•”â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine($"â•‘ [MIGRATION ERROR] Errore in Migration {migration.Version}");
+ Console.WriteLine($"â• â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine($"â•‘ Descrizione: {migration.Description}");
+ Console.WriteLine($"â•‘ SQLite Error Code: {sqlEx.SqliteErrorCode}");
+ Console.WriteLine($"â•‘ SQLite Extended Code: {sqlEx.SqliteExtendedErrorCode}");
+ Console.WriteLine($"â•‘ Messaggio: {sqlEx.Message}");
+ Console.WriteLine($"â• â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine($"â•‘ Stack Trace:");
+ Console.WriteLine($"{sqlEx.StackTrace}");
+ Console.WriteLine($"╚â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine("");
+ throw; // Re-throw per fermare inizializzazione
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("");
+ Console.WriteLine($"â•”â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine($"â•‘ [MIGRATION ERROR] Errore Generico in Migration {migration.Version}");
+ Console.WriteLine($"â• â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine($"â•‘ Descrizione: {migration.Description}");
+ Console.WriteLine($"â•‘ Tipo: {ex.GetType().Name}");
+ Console.WriteLine($"â•‘ Messaggio: {ex.Message}");
+ Console.WriteLine($"â• â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine($"â•‘ Stack Trace:");
+ Console.WriteLine($"{ex.StackTrace}");
+ Console.WriteLine($"╚â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine("");
+ throw;
+ }
+ }
+ else
+ {
+ Console.WriteLine($"[DatabaseService] ⊘ Migration {migration.Version} già applicata");
}
}
}
@@ -381,9 +683,40 @@ namespace AutoBidder.Services
///
public async Task GetConnectionAsync()
{
- var connection = new SqliteConnection(_connectionString);
- await connection.OpenAsync();
- return connection;
+ try
+ {
+ var connection = new SqliteConnection(_connectionString);
+ await connection.OpenAsync();
+ return connection;
+ }
+ catch (SqliteException sqlEx)
+ {
+ Console.WriteLine("");
+ Console.WriteLine("â•”â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine("â•‘ [CONNECTION ERROR] Impossibile aprire connessione database");
+ Console.WriteLine("â• â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine($"â•‘ Database Path: {_databasePath}");
+ Console.WriteLine($"â•‘ SQLite Error Code: {sqlEx.SqliteErrorCode}");
+ Console.WriteLine($"â•‘ SQLite Extended Code: {sqlEx.SqliteExtendedErrorCode}");
+ Console.WriteLine($"â•‘ Messaggio: {sqlEx.Message}");
+ Console.WriteLine("â• â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+
+ // Suggerimenti specifici per errori comuni
+ if (sqlEx.SqliteErrorCode == 5) // SQLITE_BUSY
+ {
+ Console.WriteLine("â•‘ SUGGERIMENTO: Database locked - altra connessione attiva");
+ Console.WriteLine("â•‘ Potrebbe essere un problema di concorrenza");
+ }
+ else if (sqlEx.SqliteErrorCode == 14) // SQLITE_CANTOPEN
+ {
+ Console.WriteLine("â•‘ SUGGERIMENTO: Impossibile aprire il file database");
+ Console.WriteLine($"â•‘ Verifica permessi su: {_databasePath}");
+ }
+
+ Console.WriteLine("╚â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine("");
+ throw;
+ }
}
///
@@ -391,14 +724,45 @@ namespace AutoBidder.Services
///
public async Task ExecuteNonQueryAsync(string sql, params SqliteParameter[] parameters)
{
- await using var connection = await GetConnectionAsync();
- await using var cmd = connection.CreateCommand();
- cmd.CommandText = sql;
- if (parameters != null)
+ try
{
- cmd.Parameters.AddRange(parameters);
+ await using var connection = await GetConnectionAsync();
+ await using var cmd = connection.CreateCommand();
+ cmd.CommandText = sql;
+ if (parameters != null)
+ {
+ cmd.Parameters.AddRange(parameters);
+ }
+ return await cmd.ExecuteNonQueryAsync();
+ }
+ catch (SqliteException sqlEx)
+ {
+ Console.WriteLine("");
+ Console.WriteLine("â•”â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine("â•‘ [QUERY ERROR] Errore SQLite in ExecuteNonQueryAsync");
+ Console.WriteLine("â• â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine($"â•‘ SQLite Error Code: {sqlEx.SqliteErrorCode}");
+ Console.WriteLine($"â•‘ SQLite Extended Code: {sqlEx.SqliteExtendedErrorCode}");
+ Console.WriteLine($"â•‘ Messaggio: {sqlEx.Message}");
+ Console.WriteLine("â• â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine($"â•‘ SQL Query (primi 500 chars):");
+ Console.WriteLine($"â•‘ {(sql.Length > 500 ? sql.Substring(0, 500) + "..." : sql)}");
+ Console.WriteLine("â• â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ if (parameters != null && parameters.Length > 0)
+ {
+ Console.WriteLine($"â•‘ Parametri:");
+ foreach (var param in parameters)
+ {
+ Console.WriteLine($"â•‘ {param.ParameterName} = {param.Value}");
+ }
+ Console.WriteLine("â• â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ }
+ Console.WriteLine($"â•‘ Stack Trace:");
+ Console.WriteLine($"{sqlEx.StackTrace}");
+ Console.WriteLine("╚â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine("");
+ throw;
}
- return await cmd.ExecuteNonQueryAsync();
}
///
@@ -406,14 +770,33 @@ namespace AutoBidder.Services
///
public async Task ExecuteScalarAsync(string sql, params SqliteParameter[] parameters)
{
- await using var connection = await GetConnectionAsync();
- await using var cmd = connection.CreateCommand();
- cmd.CommandText = sql;
- if (parameters != null)
+ try
{
- cmd.Parameters.AddRange(parameters);
+ await using var connection = await GetConnectionAsync();
+ await using var cmd = connection.CreateCommand();
+ cmd.CommandText = sql;
+ if (parameters != null)
+ {
+ cmd.Parameters.AddRange(parameters);
+ }
+ return await cmd.ExecuteScalarAsync();
+ }
+ catch (SqliteException sqlEx)
+ {
+ Console.WriteLine("");
+ Console.WriteLine("â•”â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine("â•‘ [QUERY ERROR] Errore SQLite in ExecuteScalarAsync");
+ Console.WriteLine("â• â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine($"â•‘ SQLite Error Code: {sqlEx.SqliteErrorCode}");
+ Console.WriteLine($"â•‘ SQLite Extended Code: {sqlEx.SqliteExtendedErrorCode}");
+ Console.WriteLine($"â•‘ Messaggio: {sqlEx.Message}");
+ Console.WriteLine("â• â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine($"â•‘ SQL Query (primi 500 chars):");
+ Console.WriteLine($"â•‘ {(sql.Length > 500 ? sql.Substring(0, 500) + "..." : sql)}");
+ Console.WriteLine("╚â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine("");
+ throw;
}
- return await cmd.ExecuteScalarAsync();
}
///
@@ -429,11 +812,129 @@ namespace AutoBidder.Services
var result = await cmd.ExecuteScalarAsync();
return Convert.ToInt32(result) > 0;
}
- catch
+ catch (Exception ex)
{
+ Console.WriteLine($"[DatabaseService] Health check failed: {ex.Message}");
return false;
}
}
+
+ ///
+ /// Diagnostica completa del database - mostra tutte le tabelle, indici e versione
+ ///
+ public async Task RunDatabaseDiagnosticsAsync()
+ {
+ try
+ {
+ Console.WriteLine("");
+ Console.WriteLine("â•”â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine("â•‘ [DIAGNOSTICA] Analisi Database SQLite");
+ Console.WriteLine("â• â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine($"â•‘ Path: {_databasePath}");
+ Console.WriteLine($"â•‘ Disponibile: {_isAvailable}");
+ Console.WriteLine($"â•‘ Inizializzato: {_isInitialized}");
+
+ if (File.Exists(_databasePath))
+ {
+ var fileInfo = new FileInfo(_databasePath);
+ Console.WriteLine($"â•‘ Dimensione: {FormatBytes(fileInfo.Length)}");
+ Console.WriteLine($"â•‘ Creato: {fileInfo.CreationTime}");
+ Console.WriteLine($"â•‘ Modificato: {fileInfo.LastWriteTime}");
+ }
+
+ Console.WriteLine("â• â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+
+ await using var connection = await GetConnectionAsync();
+
+ // Versione database
+ var version = await GetDatabaseVersionAsync(connection);
+ Console.WriteLine($"â•‘ Versione Schema: {version}");
+
+ // Lista tabelle
+ await using var cmdTables = connection.CreateCommand();
+ cmdTables.CommandText = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;";
+
+ Console.WriteLine("â• â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine("â•‘ Tabelle presenti:");
+
+ await using var readerTables = await cmdTables.ExecuteReaderAsync();
+ while (await readerTables.ReadAsync())
+ {
+ var tableName = readerTables.GetString(0);
+
+ // Conta record per tabella
+ await using var cmdCount = connection.CreateCommand();
+ cmdCount.CommandText = $"SELECT COUNT(*) FROM [{tableName}];";
+ var count = Convert.ToInt32(await cmdCount.ExecuteScalarAsync());
+
+ Console.WriteLine($"║ • {tableName,-30} ({count,6} record)");
+ }
+
+ // Lista indici
+ Console.WriteLine("â• â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine("â•‘ Indici presenti:");
+
+ await using var cmdIndexes = connection.CreateCommand();
+ cmdIndexes.CommandText = "SELECT name, tbl_name FROM sqlite_master WHERE type='index' AND name NOT LIKE 'sqlite_%' ORDER BY tbl_name, name;";
+
+ await using var readerIndexes = await cmdIndexes.ExecuteReaderAsync();
+ string? currentTable = null;
+ while (await readerIndexes.ReadAsync())
+ {
+ var indexName = readerIndexes.GetString(0);
+ var tableName = readerIndexes.GetString(1);
+
+ if (currentTable != tableName)
+ {
+ currentTable = tableName;
+ Console.WriteLine($"â•‘ [{tableName}]");
+ }
+
+ Console.WriteLine($"â•‘ - {indexName}");
+ }
+
+ // Migrations applicate
+ Console.WriteLine("â• â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine("â•‘ Migrations applicate:");
+
+ await using var cmdMigrations = connection.CreateCommand();
+ cmdMigrations.CommandText = "SELECT Version, AppliedAt, Description FROM DatabaseVersion ORDER BY Version;";
+
+ await using var readerMigrations = await cmdMigrations.ExecuteReaderAsync();
+ while (await readerMigrations.ReadAsync())
+ {
+ var ver = readerMigrations.GetInt32(0);
+ var appliedAt = readerMigrations.GetString(1);
+ var desc = readerMigrations.GetString(2);
+
+ Console.WriteLine($"â•‘ v{ver}: {desc}");
+ Console.WriteLine($"â•‘ Applicata: {appliedAt}");
+ }
+
+ Console.WriteLine("╚â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine("");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("â• â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine($"â•‘ ERRORE durante diagnostica: {ex.Message}");
+ Console.WriteLine("╚â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•");
+ Console.WriteLine("");
+ }
+ }
+
+ private static string FormatBytes(long bytes)
+ {
+ string[] sizes = { "B", "KB", "MB", "GB" };
+ double len = bytes;
+ int order = 0;
+ while (len >= 1024 && order < sizes.Length - 1)
+ {
+ order++;
+ len = len / 1024;
+ }
+ return $"{len:0.##} {sizes[order]}";
+ }
///
/// Ottiene informazioni sul database
@@ -535,15 +1036,20 @@ namespace AutoBidder.Services
}
///
- /// Salva risultato asta
+ /// Salva risultato asta con dati completi per analytics
///
public async Task SaveAuctionResultAsync(string auctionId, string auctionName, double finalPrice, int bidsUsed, bool won,
- double? buyNowPrice = null, double? shippingCost = null, double? totalCost = null, double? savings = null)
+ double? buyNowPrice = null, double? shippingCost = null, double? totalCost = null, double? savings = null,
+ string? winnerUsername = null, int? totalResets = null, int? winnerBidsUsed = null, string? productKey = null)
{
+ var closedAtHour = DateTime.UtcNow.Hour;
+
var sql = @"
INSERT INTO AuctionResults
- (AuctionId, AuctionName, FinalPrice, BidsUsed, Won, BuyNowPrice, ShippingCost, TotalCost, Savings, Timestamp)
- VALUES (@auctionId, @auctionName, @finalPrice, @bidsUsed, @won, @buyNowPrice, @shippingCost, @totalCost, @savings, @timestamp);
+ (AuctionId, AuctionName, FinalPrice, BidsUsed, Won, BuyNowPrice, ShippingCost, TotalCost, Savings, Timestamp,
+ WinnerUsername, ClosedAtHour, ProductKey, TotalResets, WinnerBidsUsed)
+ VALUES (@auctionId, @auctionName, @finalPrice, @bidsUsed, @won, @buyNowPrice, @shippingCost, @totalCost, @savings, @timestamp,
+ @winnerUsername, @closedAtHour, @productKey, @totalResets, @winnerBidsUsed);
";
await ExecuteNonQueryAsync(sql,
@@ -556,10 +1062,244 @@ namespace AutoBidder.Services
new SqliteParameter("@shippingCost", (object?)shippingCost ?? DBNull.Value),
new SqliteParameter("@totalCost", (object?)totalCost ?? DBNull.Value),
new SqliteParameter("@savings", (object?)savings ?? DBNull.Value),
- new SqliteParameter("@timestamp", DateTime.UtcNow.ToString("O"))
+ new SqliteParameter("@timestamp", DateTime.UtcNow.ToString("O")),
+ new SqliteParameter("@winnerUsername", (object?)winnerUsername ?? DBNull.Value),
+ new SqliteParameter("@closedAtHour", closedAtHour),
+ new SqliteParameter("@productKey", (object?)productKey ?? DBNull.Value),
+ new SqliteParameter("@totalResets", (object?)totalResets ?? DBNull.Value),
+ new SqliteParameter("@winnerBidsUsed", (object?)winnerBidsUsed ?? DBNull.Value)
);
}
+ ///
+ /// Aggiorna o inserisce statistiche aggregate per un prodotto
+ ///
+ public async Task UpsertProductStatisticsAsync(ProductStatisticsRecord stats)
+ {
+ var sql = @"
+ INSERT INTO ProductStatistics
+ (ProductKey, ProductName, TotalAuctions, WonAuctions, LostAuctions,
+ AvgFinalPrice, MinFinalPrice, MaxFinalPrice,
+ AvgBidsToWin, MinBidsToWin, MaxBidsToWin,
+ AvgResets, MinResets, MaxResets,
+ RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids,
+ HourlyStatsJson, LastUpdated)
+ VALUES (@productKey, @productName, @totalAuctions, @wonAuctions, @lostAuctions,
+ @avgFinalPrice, @minFinalPrice, @maxFinalPrice,
+ @avgBidsToWin, @minBidsToWin, @maxBidsToWin,
+ @avgResets, @minResets, @maxResets,
+ @recMinPrice, @recMaxPrice, @recMinResets, @recMaxResets, @recMaxBids,
+ @hourlyJson, @lastUpdated)
+ ON CONFLICT(ProductKey) DO UPDATE SET
+ ProductName = @productName,
+ TotalAuctions = @totalAuctions,
+ WonAuctions = @wonAuctions,
+ LostAuctions = @lostAuctions,
+ AvgFinalPrice = @avgFinalPrice,
+ MinFinalPrice = @minFinalPrice,
+ MaxFinalPrice = @maxFinalPrice,
+ AvgBidsToWin = @avgBidsToWin,
+ MinBidsToWin = @minBidsToWin,
+ MaxBidsToWin = @maxBidsToWin,
+ AvgResets = @avgResets,
+ MinResets = @minResets,
+ MaxResets = @maxResets,
+ RecommendedMinPrice = @recMinPrice,
+ RecommendedMaxPrice = @recMaxPrice,
+ RecommendedMinResets = @recMinResets,
+ RecommendedMaxResets = @recMaxResets,
+ RecommendedMaxBids = @recMaxBids,
+ HourlyStatsJson = @hourlyJson,
+ LastUpdated = @lastUpdated;
+ ";
+
+ await ExecuteNonQueryAsync(sql,
+ new SqliteParameter("@productKey", stats.ProductKey),
+ new SqliteParameter("@productName", stats.ProductName),
+ new SqliteParameter("@totalAuctions", stats.TotalAuctions),
+ new SqliteParameter("@wonAuctions", stats.WonAuctions),
+ new SqliteParameter("@lostAuctions", stats.LostAuctions),
+ new SqliteParameter("@avgFinalPrice", stats.AvgFinalPrice),
+ new SqliteParameter("@minFinalPrice", (object?)stats.MinFinalPrice ?? DBNull.Value),
+ new SqliteParameter("@maxFinalPrice", (object?)stats.MaxFinalPrice ?? DBNull.Value),
+ new SqliteParameter("@avgBidsToWin", stats.AvgBidsToWin),
+ new SqliteParameter("@minBidsToWin", (object?)stats.MinBidsToWin ?? DBNull.Value),
+ new SqliteParameter("@maxBidsToWin", (object?)stats.MaxBidsToWin ?? DBNull.Value),
+ new SqliteParameter("@avgResets", stats.AvgResets),
+ new SqliteParameter("@minResets", (object?)stats.MinResets ?? DBNull.Value),
+ new SqliteParameter("@maxResets", (object?)stats.MaxResets ?? DBNull.Value),
+ new SqliteParameter("@recMinPrice", (object?)stats.RecommendedMinPrice ?? DBNull.Value),
+ new SqliteParameter("@recMaxPrice", (object?)stats.RecommendedMaxPrice ?? DBNull.Value),
+ new SqliteParameter("@recMinResets", (object?)stats.RecommendedMinResets ?? DBNull.Value),
+ new SqliteParameter("@recMaxResets", (object?)stats.RecommendedMaxResets ?? DBNull.Value),
+ new SqliteParameter("@recMaxBids", (object?)stats.RecommendedMaxBids ?? DBNull.Value),
+ new SqliteParameter("@hourlyJson", (object?)stats.HourlyStatsJson ?? DBNull.Value),
+ new SqliteParameter("@lastUpdated", DateTime.UtcNow.ToString("O"))
+ );
+ }
+
+ ///
+ /// Ottiene statistiche per un prodotto specifico
+ ///
+ public async Task GetProductStatisticsAsync(string productKey)
+ {
+ var sql = @"
+ SELECT ProductKey, ProductName, TotalAuctions, WonAuctions, LostAuctions,
+ AvgFinalPrice, MinFinalPrice, MaxFinalPrice,
+ AvgBidsToWin, MinBidsToWin, MaxBidsToWin,
+ AvgResets, MinResets, MaxResets,
+ RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids,
+ HourlyStatsJson, LastUpdated
+ FROM ProductStatistics
+ WHERE ProductKey = @productKey;
+ ";
+
+ await using var connection = await GetConnectionAsync();
+ await using var cmd = connection.CreateCommand();
+ cmd.CommandText = sql;
+ cmd.Parameters.AddWithValue("@productKey", productKey);
+
+ await using var reader = await cmd.ExecuteReaderAsync();
+ if (await reader.ReadAsync())
+ {
+ return new ProductStatisticsRecord
+ {
+ ProductKey = reader.GetString(0),
+ ProductName = reader.GetString(1),
+ TotalAuctions = reader.GetInt32(2),
+ WonAuctions = reader.GetInt32(3),
+ LostAuctions = reader.GetInt32(4),
+ AvgFinalPrice = reader.GetDouble(5),
+ MinFinalPrice = reader.IsDBNull(6) ? null : reader.GetDouble(6),
+ MaxFinalPrice = reader.IsDBNull(7) ? null : reader.GetDouble(7),
+ AvgBidsToWin = reader.GetDouble(8),
+ MinBidsToWin = reader.IsDBNull(9) ? null : reader.GetInt32(9),
+ MaxBidsToWin = reader.IsDBNull(10) ? null : reader.GetInt32(10),
+ AvgResets = reader.GetDouble(11),
+ MinResets = reader.IsDBNull(12) ? null : reader.GetInt32(12),
+ MaxResets = reader.IsDBNull(13) ? null : reader.GetInt32(13),
+ RecommendedMinPrice = reader.IsDBNull(14) ? null : reader.GetDouble(14),
+ RecommendedMaxPrice = reader.IsDBNull(15) ? null : reader.GetDouble(15),
+ RecommendedMinResets = reader.IsDBNull(16) ? null : reader.GetInt32(16),
+ RecommendedMaxResets = reader.IsDBNull(17) ? null : reader.GetInt32(17),
+ RecommendedMaxBids = reader.IsDBNull(18) ? null : reader.GetInt32(18),
+ HourlyStatsJson = reader.IsDBNull(19) ? null : reader.GetString(19),
+ LastUpdated = reader.GetString(20)
+ };
+ }
+
+ return null;
+ }
+
+ ///
+ /// Ottiene tutti i risultati aste per un prodotto specifico
+ ///
+ public async Task> GetAuctionResultsByProductAsync(string productKey, int limit = 100)
+ {
+ var sql = @"
+ SELECT Id, AuctionId, AuctionName, FinalPrice, BidsUsed, Won, Timestamp,
+ BuyNowPrice, ShippingCost, TotalCost, Savings,
+ WinnerUsername, ClosedAtHour, ProductKey, TotalResets, WinnerBidsUsed
+ FROM AuctionResults
+ WHERE ProductKey = @productKey
+ ORDER BY Timestamp DESC
+ LIMIT @limit;
+ ";
+
+ var results = new List();
+
+ await using var connection = await GetConnectionAsync();
+ await using var cmd = connection.CreateCommand();
+ cmd.CommandText = sql;
+ cmd.Parameters.AddWithValue("@productKey", productKey);
+ cmd.Parameters.AddWithValue("@limit", limit);
+
+ await using var reader = await cmd.ExecuteReaderAsync();
+ while (await reader.ReadAsync())
+ {
+ results.Add(ReadAuctionResultExtended(reader));
+ }
+
+ return results;
+ }
+
+ ///
+ /// Ottiene tutti i prodotti con statistiche
+ ///
+ public async Task> GetAllProductStatisticsAsync()
+ {
+ var sql = @"
+ SELECT ProductKey, ProductName, TotalAuctions, WonAuctions, LostAuctions,
+ AvgFinalPrice, MinFinalPrice, MaxFinalPrice,
+ AvgBidsToWin, MinBidsToWin, MaxBidsToWin,
+ AvgResets, MinResets, MaxResets,
+ RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids,
+ HourlyStatsJson, LastUpdated
+ FROM ProductStatistics
+ ORDER BY TotalAuctions DESC;
+ ";
+
+ var results = new List();
+
+ await using var connection = await GetConnectionAsync();
+ await using var cmd = connection.CreateCommand();
+ cmd.CommandText = sql;
+
+ await using var reader = await cmd.ExecuteReaderAsync();
+ while (await reader.ReadAsync())
+ {
+ results.Add(new ProductStatisticsRecord
+ {
+ ProductKey = reader.GetString(0),
+ ProductName = reader.GetString(1),
+ TotalAuctions = reader.GetInt32(2),
+ WonAuctions = reader.GetInt32(3),
+ LostAuctions = reader.GetInt32(4),
+ AvgFinalPrice = reader.GetDouble(5),
+ MinFinalPrice = reader.IsDBNull(6) ? null : reader.GetDouble(6),
+ MaxFinalPrice = reader.IsDBNull(7) ? null : reader.GetDouble(7),
+ AvgBidsToWin = reader.GetDouble(8),
+ MinBidsToWin = reader.IsDBNull(9) ? null : reader.GetInt32(9),
+ MaxBidsToWin = reader.IsDBNull(10) ? null : reader.GetInt32(10),
+ AvgResets = reader.GetDouble(11),
+ MinResets = reader.IsDBNull(12) ? null : reader.GetInt32(12),
+ MaxResets = reader.IsDBNull(13) ? null : reader.GetInt32(13),
+ RecommendedMinPrice = reader.IsDBNull(14) ? null : reader.GetDouble(14),
+ RecommendedMaxPrice = reader.IsDBNull(15) ? null : reader.GetDouble(15),
+ RecommendedMinResets = reader.IsDBNull(16) ? null : reader.GetInt32(16),
+ RecommendedMaxResets = reader.IsDBNull(17) ? null : reader.GetInt32(17),
+ RecommendedMaxBids = reader.IsDBNull(18) ? null : reader.GetInt32(18),
+ HourlyStatsJson = reader.IsDBNull(19) ? null : reader.GetString(19),
+ LastUpdated = reader.GetString(20)
+ });
+ }
+
+ return results;
+ }
+
+ private AuctionResultExtended ReadAuctionResultExtended(Microsoft.Data.Sqlite.SqliteDataReader reader)
+ {
+ return new AuctionResultExtended
+ {
+ Id = reader.GetInt32(0),
+ AuctionId = reader.GetString(1),
+ AuctionName = reader.GetString(2),
+ FinalPrice = reader.GetDouble(3),
+ BidsUsed = reader.GetInt32(4),
+ Won = reader.GetInt32(5) == 1,
+ Timestamp = reader.GetString(6),
+ BuyNowPrice = reader.IsDBNull(7) ? null : reader.GetDouble(7),
+ ShippingCost = reader.IsDBNull(8) ? null : reader.GetDouble(8),
+ TotalCost = reader.IsDBNull(9) ? null : reader.GetDouble(9),
+ Savings = reader.IsDBNull(10) ? null : reader.GetDouble(10),
+ WinnerUsername = reader.IsDBNull(11) ? null : reader.GetString(11),
+ ClosedAtHour = reader.IsDBNull(12) ? null : reader.GetInt32(12),
+ ProductKey = reader.IsDBNull(13) ? null : reader.GetString(13),
+ TotalResets = reader.IsDBNull(14) ? null : reader.GetInt32(14),
+ WinnerBidsUsed = reader.IsDBNull(15) ? null : reader.GetInt32(15)
+ };
+ }
+
///
/// Ottiene statistiche giornaliere per un range di date
///
@@ -599,19 +1339,20 @@ namespace AutoBidder.Services
}
///
- /// Ottiene risultati aste recenti
+ /// Ottiene risultati aste recenti con campi estesi per analytics
///
- public async Task> GetRecentAuctionResultsAsync(int limit = 50)
+ public async Task> GetRecentAuctionResultsAsync(int limit = 50)
{
var sql = @"
SELECT Id, AuctionId, AuctionName, FinalPrice, BidsUsed, Won, Timestamp,
- BuyNowPrice, ShippingCost, TotalCost, Savings
+ BuyNowPrice, ShippingCost, TotalCost, Savings,
+ WinnerUsername, ClosedAtHour, ProductKey, TotalResets, WinnerBidsUsed
FROM AuctionResults
ORDER BY Timestamp DESC
LIMIT @limit;
";
- var results = new List();
+ var results = new List();
await using var connection = await GetConnectionAsync();
await using var cmd = connection.CreateCommand();
@@ -621,20 +1362,7 @@ namespace AutoBidder.Services
await using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
- results.Add(new AuctionResult
- {
- Id = reader.GetInt32(0),
- AuctionId = reader.GetString(1),
- AuctionName = reader.GetString(2),
- FinalPrice = reader.GetDouble(3),
- BidsUsed = reader.GetInt32(4),
- Won = reader.GetInt32(5) == 1,
- Timestamp = reader.GetString(6),
- BuyNowPrice = reader.IsDBNull(7) ? null : reader.GetDouble(7),
- ShippingCost = reader.IsDBNull(8) ? null : reader.GetDouble(8),
- TotalCost = reader.IsDBNull(9) ? null : reader.GetDouble(9),
- Savings = reader.IsDBNull(10) ? null : reader.GetDouble(10)
- });
+ results.Add(ReadAuctionResultExtended(reader));
}
return results;
@@ -653,6 +1381,7 @@ namespace AutoBidder.Services
public int Version { get; }
public string Description { get; }
private readonly Func _execute;
+ private string? _lastSqlExecuted;
public Migration(int version, string description, Func execute)
{
@@ -663,7 +1392,24 @@ namespace AutoBidder.Services
public async Task Execute(SqliteConnection connection)
{
- await _execute(connection);
+ try
+ {
+ await _execute(connection);
+ }
+ catch (SqliteException sqlEx)
+ {
+ // Log dettagliato dell'SQL che ha causato l'errore
+ Console.WriteLine($"[Migration {Version}] ✗ ERRORE durante esecuzione");
+
+ // Se possibile, estrae l'SQL dal messaggio di errore
+ if (sqlEx.Message.Contains("duplicate column"))
+ {
+ Console.WriteLine($"[Migration {Version}] PROBABILE CAUSA: Colonna già esistente (migration già applicata parzialmente)");
+ Console.WriteLine($"[Migration {Version}] SOLUZIONE: Verifica versione database o elimina e ricrea il database");
+ }
+
+ throw;
+ }
}
}
}
diff --git a/Mimante/Services/ProductStatisticsService.cs b/Mimante/Services/ProductStatisticsService.cs
new file mode 100644
index 0000000..55f62cb
--- /dev/null
+++ b/Mimante/Services/ProductStatisticsService.cs
@@ -0,0 +1,340 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using AutoBidder.Models;
+
+namespace AutoBidder.Services
+{
+ ///
+ /// Servizio per calcolare statistiche aggregate per prodotto e generare limiti consigliati.
+ /// L'algoritmo analizza le aste storiche per determinare i parametri ottimali.
+ ///
+ public class ProductStatisticsService
+ {
+ private readonly DatabaseService _db;
+
+ public ProductStatisticsService(DatabaseService db)
+ {
+ _db = db;
+ }
+
+ ///
+ /// Genera una chiave univoca normalizzata per raggruppare prodotti simili.
+ /// Rimuove varianti, numeri di serie, colori ecc.
+ ///
+ public static string GenerateProductKey(string productName)
+ {
+ if (string.IsNullOrWhiteSpace(productName))
+ return "unknown";
+
+ var normalized = productName.ToLowerInvariant().Trim();
+
+ // Rimuovi contenuto tra parentesi (varianti, colori, capacità)
+ normalized = Regex.Replace(normalized, @"\([^)]*\)", "");
+ normalized = Regex.Replace(normalized, @"\[[^\]]*\]", "");
+
+ // Rimuovi colori comuni
+ var colors = new[] { "nero", "bianco", "grigio", "rosso", "blu", "verde", "oro", "argento",
+ "black", "white", "gray", "red", "blue", "green", "gold", "silver",
+ "space gray", "midnight", "starlight" };
+ foreach (var color in colors)
+ {
+ normalized = Regex.Replace(normalized, $@"\b{color}\b", "", RegexOptions.IgnoreCase);
+ }
+
+ // Rimuovi capacità storage (64gb, 128gb, 256gb, ecc.)
+ normalized = Regex.Replace(normalized, @"\b\d+\s*(gb|tb|mb)\b", "", RegexOptions.IgnoreCase);
+
+ // Rimuovi numeri di serie e codici prodotto
+ normalized = Regex.Replace(normalized, @"\b[A-Z]{2,}\d{3,}\b", "", RegexOptions.IgnoreCase);
+
+ // Normalizza spazi e caratteri speciali
+ normalized = Regex.Replace(normalized, @"[^a-z0-9\s]", " ");
+ normalized = Regex.Replace(normalized, @"\s+", "_");
+ normalized = normalized.Trim('_');
+
+ // Limita lunghezza
+ if (normalized.Length > 50)
+ normalized = normalized.Substring(0, 50);
+
+ return string.IsNullOrEmpty(normalized) ? "unknown" : normalized;
+ }
+
+ ///
+ /// Aggiorna le statistiche aggregate per un prodotto dopo una nuova asta completata
+ ///
+ public async Task UpdateProductStatisticsAsync(string productKey, string productName)
+ {
+ try
+ {
+ // Ottieni tutti i risultati per questo prodotto
+ var results = await _db.GetAuctionResultsByProductAsync(productKey, 500);
+
+ if (results.Count == 0)
+ {
+ Console.WriteLine($"[ProductStats] No results found for product: {productKey}");
+ return;
+ }
+
+ // Calcola statistiche aggregate
+ var wonResults = results.Where(r => r.Won).ToList();
+ var lostResults = results.Where(r => !r.Won).ToList();
+
+ var stats = new ProductStatisticsRecord
+ {
+ ProductKey = productKey,
+ ProductName = productName,
+ TotalAuctions = results.Count,
+ WonAuctions = wonResults.Count,
+ LostAuctions = lostResults.Count
+ };
+
+ // Statistiche prezzo (usa aste vinte per calcolare i target)
+ if (wonResults.Any())
+ {
+ stats.AvgFinalPrice = wonResults.Average(r => r.FinalPrice);
+ stats.MinFinalPrice = wonResults.Min(r => r.FinalPrice);
+ stats.MaxFinalPrice = wonResults.Max(r => r.FinalPrice);
+ }
+ else if (results.Any())
+ {
+ stats.AvgFinalPrice = results.Average(r => r.FinalPrice);
+ stats.MinFinalPrice = results.Min(r => r.FinalPrice);
+ stats.MaxFinalPrice = results.Max(r => r.FinalPrice);
+ }
+
+ // Statistiche puntate (usa WinnerBidsUsed se disponibile, altrimenti BidsUsed)
+ var bidsData = wonResults
+ .Where(r => r.WinnerBidsUsed.HasValue || r.BidsUsed > 0)
+ .Select(r => r.WinnerBidsUsed ?? r.BidsUsed)
+ .ToList();
+
+ if (bidsData.Any())
+ {
+ stats.AvgBidsToWin = bidsData.Select(b => (double)b).Average();
+ stats.MinBidsToWin = bidsData.Min();
+ stats.MaxBidsToWin = bidsData.Max();
+ }
+
+ // Statistiche reset
+ var resetData = wonResults.Where(r => r.TotalResets.HasValue).Select(r => r.TotalResets!.Value).ToList();
+ if (resetData.Any())
+ {
+ stats.AvgResets = resetData.Select(r => (double)r).Average();
+ stats.MinResets = resetData.Min();
+ stats.MaxResets = resetData.Max();
+ }
+
+ // Calcola limiti consigliati
+ var limits = CalculateRecommendedLimits(results);
+ stats.RecommendedMinPrice = limits.MinPrice;
+ stats.RecommendedMaxPrice = limits.MaxPrice;
+ stats.RecommendedMinResets = limits.MinResets;
+ stats.RecommendedMaxResets = limits.MaxResets;
+ stats.RecommendedMaxBids = limits.MaxBids;
+
+ // Calcola statistiche per fascia oraria
+ var hourlyStats = CalculateHourlyStats(results);
+ stats.HourlyStatsJson = JsonSerializer.Serialize(hourlyStats);
+
+ // Salva nel database
+ await _db.UpsertProductStatisticsAsync(stats);
+
+ Console.WriteLine($"[ProductStats] Updated stats for {productKey}: {stats.TotalAuctions} auctions, WinRate={stats.WinRate:F1}%");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[ProductStats ERROR] Failed to update stats for {productKey}: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Calcola i limiti consigliati basandosi sui dati storici
+ ///
+ public RecommendedLimits CalculateRecommendedLimits(List results)
+ {
+ var limits = new RecommendedLimits
+ {
+ SampleSize = results.Count
+ };
+
+ if (results.Count < 3)
+ {
+ limits.ConfidenceScore = 0;
+ return limits;
+ }
+
+ var wonResults = results.Where(r => r.Won).ToList();
+
+ if (wonResults.Count == 0)
+ {
+ // Nessuna vittoria: usa tutti i risultati con margine conservativo
+ limits.ConfidenceScore = 10;
+ limits.MinPrice = results.Min(r => r.FinalPrice) * 0.8;
+ limits.MaxPrice = results.Max(r => r.FinalPrice) * 1.2;
+ return limits;
+ }
+
+ // Calcola percentili sui prezzi delle aste vinte
+ var prices = wonResults.Select(r => r.FinalPrice).OrderBy(p => p).ToList();
+ limits.MinPrice = CalculatePercentile(prices, 10); // 10° percentile - entrare presto
+ limits.MaxPrice = CalculatePercentile(prices, 90); // 90° percentile - limite sicuro
+
+ // Calcola limiti reset
+ var resets = wonResults.Where(r => r.TotalResets.HasValue).Select(r => r.TotalResets!.Value).ToList();
+ if (resets.Any())
+ {
+ var avgResets = resets.Average();
+ var stdDev = CalculateStandardDeviation(resets.Select(r => (double)r).ToList());
+
+ limits.MinResets = Math.Max(0, (int)(avgResets - stdDev)); // Media - 1 stddev
+ limits.MaxResets = (int)(avgResets + stdDev); // Media + 1 stddev
+ }
+
+ // Calcola limiti puntate
+ var bids = wonResults
+ .Where(r => r.WinnerBidsUsed.HasValue || r.BidsUsed > 0)
+ .Select(r => r.WinnerBidsUsed ?? r.BidsUsed)
+ .OrderBy(b => b)
+ .ToList();
+
+ if (bids.Any())
+ {
+ // 90° percentile + 10% buffer
+ limits.MaxBids = (int)(CalculatePercentile(bids.Select(b => (double)b).ToList(), 90) * 1.1);
+ }
+
+ // Trova la fascia oraria migliore
+ var hourlyWins = wonResults
+ .Where(r => r.ClosedAtHour.HasValue)
+ .GroupBy(r => r.ClosedAtHour!.Value)
+ .Select(g => new { Hour = g.Key, Wins = g.Count() })
+ .OrderByDescending(x => x.Wins)
+ .FirstOrDefault();
+
+ if (hourlyWins != null)
+ {
+ limits.BestHourToPlay = hourlyWins.Hour;
+ }
+
+ // Win rate
+ limits.AverageWinRate = results.Count > 0 ? (double)wonResults.Count / results.Count * 100 : 0;
+
+ // Confidence score basato sul sample size
+ limits.ConfidenceScore = results.Count switch
+ {
+ >= 50 => 95,
+ >= 30 => 85,
+ >= 20 => 70,
+ >= 10 => 50,
+ >= 5 => 30,
+ _ => 15
+ };
+
+ return limits;
+ }
+
+ ///
+ /// Calcola statistiche aggregate per ogni fascia oraria
+ ///
+ private List CalculateHourlyStats(List results)
+ {
+ var stats = new List();
+
+ var grouped = results
+ .Where(r => r.ClosedAtHour.HasValue)
+ .GroupBy(r => r.ClosedAtHour!.Value);
+
+ foreach (var group in grouped)
+ {
+ var hourResults = group.ToList();
+ var wonInHour = hourResults.Where(r => r.Won).ToList();
+
+ stats.Add(new HourlyStats
+ {
+ Hour = group.Key,
+ TotalAuctions = hourResults.Count,
+ WonAuctions = wonInHour.Count,
+ AvgFinalPrice = hourResults.Any() ? hourResults.Average(r => r.FinalPrice) : 0,
+ AvgBidsUsed = hourResults.Any() ? hourResults.Average(r => r.BidsUsed) : 0
+ });
+ }
+
+ return stats.OrderBy(s => s.Hour).ToList();
+ }
+
+ ///
+ /// Ottiene le statistiche per un prodotto
+ ///
+ public async Task GetProductStatisticsAsync(string productKey)
+ {
+ return await _db.GetProductStatisticsAsync(productKey);
+ }
+
+ ///
+ /// Ottiene tutti i prodotti con statistiche
+ ///
+ public async Task> GetAllProductStatisticsAsync()
+ {
+ return await _db.GetAllProductStatisticsAsync();
+ }
+
+ ///
+ /// Ottiene i limiti consigliati per un prodotto
+ ///
+ public async Task GetRecommendedLimitsAsync(string productKey)
+ {
+ var stats = await _db.GetProductStatisticsAsync(productKey);
+
+ if (stats == null)
+ return null;
+
+ return new RecommendedLimits
+ {
+ MinPrice = stats.RecommendedMinPrice ?? 0,
+ MaxPrice = stats.RecommendedMaxPrice ?? 0,
+ MinResets = stats.RecommendedMinResets ?? 0,
+ MaxResets = stats.RecommendedMaxResets ?? 0,
+ MaxBids = stats.RecommendedMaxBids ?? 0,
+ ConfidenceScore = stats.TotalAuctions switch
+ {
+ >= 50 => 95,
+ >= 30 => 85,
+ >= 20 => 70,
+ >= 10 => 50,
+ >= 5 => 30,
+ _ => 15
+ },
+ SampleSize = stats.TotalAuctions,
+ AverageWinRate = stats.WinRate
+ };
+ }
+
+ // Helpers per calcoli statistici
+ private static double CalculatePercentile(List sortedData, int percentile)
+ {
+ if (sortedData.Count == 0) return 0;
+ if (sortedData.Count == 1) return sortedData[0];
+
+ double index = (percentile / 100.0) * (sortedData.Count - 1);
+ int lower = (int)Math.Floor(index);
+ int upper = (int)Math.Ceiling(index);
+
+ if (lower == upper) return sortedData[lower];
+
+ return sortedData[lower] + (sortedData[upper] - sortedData[lower]) * (index - lower);
+ }
+
+ private static double CalculateStandardDeviation(List data)
+ {
+ if (data.Count < 2) return 0;
+
+ double avg = data.Average();
+ double sumSquares = data.Sum(d => Math.Pow(d - avg, 2));
+ return Math.Sqrt(sumSquares / (data.Count - 1));
+ }
+ }
+}
diff --git a/Mimante/Services/StatsService.cs b/Mimante/Services/StatsService.cs
index 1550236..22d2081 100644
--- a/Mimante/Services/StatsService.cs
+++ b/Mimante/Services/StatsService.cs
@@ -2,64 +2,145 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using AutoBidder.Models;
-using AutoBidder.Data;
using System.Collections.Generic;
-using Microsoft.EntityFrameworkCore;
namespace AutoBidder.Services
{
///
- /// Servizio per calcolo e gestione statistiche avanzate
- /// Usa PostgreSQL per statistiche persistenti e SQLite locale come fallback
+ /// Servizio per calcolo e gestione statistiche.
+ /// Usa esclusivamente il database SQLite interno gestito da DatabaseService.
+ /// Le statistiche sono disabilitate se il database non è disponibile.
///
public class StatsService
{
private readonly DatabaseService _db;
- private readonly PostgresStatsContext? _postgresDb;
- private readonly bool _postgresAvailable;
- public StatsService(DatabaseService db, PostgresStatsContext? postgresDb = null)
+ ///
+ /// Indica se le statistiche sono disponibili (database SQLite funzionante)
+ ///
+ public bool IsAvailable => _db.IsAvailable && _db.IsInitialized;
+
+ ///
+ /// Messaggio di errore se le statistiche non sono disponibili
+ ///
+ public string? ErrorMessage => !IsAvailable ? _db.InitializationError ?? "Database non disponibile" : null;
+
+ ///
+ /// Path del database SQLite
+ ///
+ public string DatabasePath => _db.DatabasePath;
+
+ private ProductStatisticsService? _productStatsService;
+
+ public StatsService(DatabaseService db)
{
_db = db;
- _postgresDb = postgresDb;
- _postgresAvailable = false;
+ _productStatsService = new ProductStatisticsService(db);
- // Verifica disponibilità PostgreSQL
- if (_postgresDb != null)
+ // Log stato database SQLite
+ Console.WriteLine($"[StatsService] Database available: {_db.IsAvailable}");
+ Console.WriteLine($"[StatsService] Database initialized: {_db.IsInitialized}");
+
+ if (!_db.IsAvailable)
{
- try
- {
- _postgresAvailable = _postgresDb.Database.CanConnect();
- var status = _postgresAvailable ? "AVAILABLE" : "UNAVAILABLE";
- Console.WriteLine($"[StatsService] PostgreSQL status: {status}");
- }
- catch (Exception ex)
- {
- Console.WriteLine($"[StatsService] PostgreSQL connection failed: {ex.Message}");
- }
- }
- else
- {
- Console.WriteLine("[StatsService] PostgreSQL not configured - using SQLite only");
+ Console.WriteLine($"[StatsService] Database error: {_db.InitializationError}");
}
}
///
- /// Registra il completamento di un'asta (sia su PostgreSQL che SQLite)
+ /// Registra il completamento di un'asta con tutti i dati per analytics
+ /// Include scraping HTML per ottenere le puntate del vincitore
///
- public async Task RecordAuctionCompletedAsync(AuctionInfo auction, bool won)
+ public async Task RecordAuctionCompletedAsync(AuctionInfo auction, AuctionState state, bool won)
{
+ // Skip se database non disponibile
+ if (!IsAvailable)
+ {
+ Console.WriteLine("[StatsService] Skipping record - database not available");
+ return;
+ }
+
try
{
+ Console.WriteLine($"[StatsService] ====== INIZIO SALVATAGGIO ASTA TERMINATA ======");
+ Console.WriteLine($"[StatsService] Asta: {auction.Name} (ID: {auction.AuctionId})");
+ Console.WriteLine($"[StatsService] Stato: {(won ? "VINTA" : "PERSA")}");
+
var today = DateTime.UtcNow.ToString("yyyy-MM-dd");
var bidsUsed = auction.BidsUsedOnThisAuction ?? 0;
var bidCost = auction.BidCost;
var moneySpent = bidsUsed * bidCost;
- var finalPrice = auction.LastState?.Price ?? 0;
+ var finalPrice = state.Price;
var buyNowPrice = auction.BuyNowPrice;
var shippingCost = auction.ShippingCost ?? 0;
+ // Dati aggiuntivi per analytics
+ var winnerUsername = state.LastBidder;
+ var totalResets = auction.ResetCount;
+ var productKey = ProductStatisticsService.GenerateProductKey(auction.Name);
+
+ Console.WriteLine($"[StatsService] Prezzo finale: €{finalPrice:F2}");
+ Console.WriteLine($"[StatsService] Puntate usate (utente): {bidsUsed}");
+ Console.WriteLine($"[StatsService] Vincitore: {winnerUsername ?? "N/A"}");
+ Console.WriteLine($"[StatsService] Reset totali: {totalResets}");
+
+ // ?? SCRAPING HTML: Ottieni puntate del vincitore dalla pagina dell'asta
+ int? winnerBidsUsed = null;
+
+ if (!string.IsNullOrEmpty(winnerUsername))
+ {
+ Console.WriteLine($"[StatsService] Avvio scraping HTML per ottenere puntate del vincitore...");
+ winnerBidsUsed = await ScrapeWinnerBidsFromAuctionPageAsync(auction.OriginalUrl);
+
+ // ? VALIDAZIONE: Verifica che i dati estratti siano ragionevoli
+ if (winnerBidsUsed.HasValue)
+ {
+ if (winnerBidsUsed.Value < 0)
+ {
+ Console.WriteLine($"[StatsService] ? VALIDAZIONE FALLITA: Puntate negative ({winnerBidsUsed.Value}) - Dato scartato");
+ winnerBidsUsed = null;
+ }
+ else if (winnerBidsUsed.Value > 50000)
+ {
+ Console.WriteLine($"[StatsService] ? VALIDAZIONE FALLITA: Puntate sospette ({winnerBidsUsed.Value} > 50000) - Dato scartato");
+ winnerBidsUsed = null;
+ }
+ else if (winnerBidsUsed.Value == 0 && totalResets > 10)
+ {
+ Console.WriteLine($"[StatsService] ? VALIDAZIONE FALLITA: 0 puntate con {totalResets} reset - Dato sospetto");
+ winnerBidsUsed = null;
+ }
+ else
+ {
+ Console.WriteLine($"[StatsService] ? Puntate vincitore estratte da HTML: {winnerBidsUsed.Value} (validazione OK)");
+ }
+ }
+
+ // Fallback se validazione fallita o scraping non riuscito
+ if (!winnerBidsUsed.HasValue)
+ {
+ Console.WriteLine($"[StatsService] ? Impossibile estrarre puntate valide del vincitore da HTML");
+
+ // Fallback: conta da RecentBids (meno affidabile)
+ if (auction.RecentBids != null)
+ {
+ winnerBidsUsed = auction.RecentBids
+ .Count(b => b.Username?.Equals(winnerUsername, StringComparison.OrdinalIgnoreCase) == true);
+
+ if (winnerBidsUsed.Value > 0)
+ {
+ Console.WriteLine($"[StatsService] Fallback: puntate vincitore da RecentBids: {winnerBidsUsed.Value}");
+ }
+ else
+ {
+ Console.WriteLine($"[StatsService] ? Fallback fallito: nessuna puntata trovata in RecentBids");
+ winnerBidsUsed = null;
+ }
+ }
+ }
+ }
+
double? totalCost = null;
double? savings = null;
@@ -67,9 +148,14 @@ namespace AutoBidder.Services
{
totalCost = finalPrice + moneySpent + shippingCost;
savings = (buyNowPrice.Value + shippingCost) - totalCost.Value;
+
+ Console.WriteLine($"[StatsService] Costo totale: €{totalCost:F2}");
+ Console.WriteLine($"[StatsService] Risparmio: €{savings:F2}");
}
- // Salva su SQLite (sempre)
+ Console.WriteLine($"[StatsService] Salvataggio nel database...");
+
+ // Salva risultato asta con tutti i campi
await _db.SaveAuctionResultAsync(
auction.AuctionId,
auction.Name,
@@ -79,9 +165,16 @@ namespace AutoBidder.Services
buyNowPrice,
shippingCost,
totalCost,
- savings
+ savings,
+ winnerUsername,
+ totalResets,
+ winnerBidsUsed,
+ productKey
);
+ Console.WriteLine($"[StatsService] ? Risultato asta salvato");
+
+ // Aggiorna statistiche giornaliere
await _db.SaveDailyStatAsync(
today,
bidsUsed,
@@ -89,229 +182,159 @@ namespace AutoBidder.Services
won ? 1 : 0,
won ? 0 : 1,
savings ?? 0,
- auction.LastState?.PollingLatencyMs
+ state.PollingLatencyMs
);
- // Salva su PostgreSQL se disponibile
- if (_postgresAvailable && _postgresDb != null)
+ Console.WriteLine($"[StatsService] ? Statistiche giornaliere aggiornate");
+
+ // Aggiorna statistiche aggregate per prodotto
+ if (_productStatsService != null)
{
- await SaveToPostgresAsync(auction, won, finalPrice, bidsUsed, totalCost, savings);
+ Console.WriteLine($"[StatsService] Aggiornamento statistiche prodotto (key: {productKey})...");
+ await _productStatsService.UpdateProductStatisticsAsync(productKey, auction.Name);
+ Console.WriteLine($"[StatsService] ? Statistiche prodotto aggiornate");
}
- Console.WriteLine($"[StatsService] Recorded auction {auction.Name} - Won: {won}, Bids: {bidsUsed}, Savings: {savings:F2}€");
+ Console.WriteLine($"[StatsService] ====== SALVATAGGIO COMPLETATO ======");
}
catch (Exception ex)
{
Console.WriteLine($"[StatsService ERROR] Failed to record auction: {ex.Message}");
+ Console.WriteLine($"[StatsService ERROR] Stack: {ex.StackTrace}");
}
}
-
+
///
- /// Salva asta conclusa su PostgreSQL
+ /// Scarica l'HTML della pagina dell'asta e estrae le puntate del vincitore
///
- private async Task SaveToPostgresAsync(AuctionInfo auction, bool won, double finalPrice, int bidsUsed, double? totalCost, double? savings)
+ private async Task ScrapeWinnerBidsFromAuctionPageAsync(string auctionUrl)
{
- if (_postgresDb == null) return;
-
try
{
- var completedAuction = new CompletedAuction
- {
- AuctionId = auction.AuctionId,
- ProductName = auction.Name,
- FinalPrice = (decimal)finalPrice,
- BuyNowPrice = auction.BuyNowPrice.HasValue ? (decimal)auction.BuyNowPrice.Value : null,
- ShippingCost = auction.ShippingCost.HasValue ? (decimal)auction.ShippingCost.Value : null,
- TotalBids = auction.LastState?.MyBidsCount ?? bidsUsed, // Usa MyBidsCount se disponibile
- MyBidsCount = bidsUsed,
- ResetCount = auction.ResetCount,
- Won = won,
- WinnerUsername = won ? "ME" : auction.LastState?.LastBidder,
- CompletedAt = DateTime.UtcNow,
- AverageLatency = auction.LastState != null ? (decimal)auction.LastState.PollingLatencyMs : null, // PollingLatencyMs è int, non nullable
- Savings = savings.HasValue ? (decimal)savings.Value : null,
- TotalCost = totalCost.HasValue ? (decimal)totalCost.Value : null,
- CreatedAt = DateTime.UtcNow
- };
-
- _postgresDb.CompletedAuctions.Add(completedAuction);
- await _postgresDb.SaveChangesAsync();
-
- // Aggiorna statistiche prodotto
- await UpdateProductStatisticsAsync(auction, won, bidsUsed, finalPrice);
+ using var httpClient = new HttpClient();
+ // ? RIDOTTO: Timeout da 10s ? 5s per evitare rallentamenti
+ httpClient.Timeout = TimeSpan.FromSeconds(5);
- // Aggiorna metriche giornaliere
- await UpdateDailyMetricsAsync(DateTime.UtcNow.Date, bidsUsed, auction.BidCost, won, savings ?? 0);
-
- Console.WriteLine($"[PostgreSQL] Saved auction {auction.Name} to PostgreSQL");
- }
- catch (Exception ex)
- {
- Console.WriteLine($"[PostgreSQL ERROR] Failed to save auction: {ex.Message}");
- }
- }
-
- ///
- /// Aggiorna statistiche prodotto in PostgreSQL
- ///
- private async Task UpdateProductStatisticsAsync(AuctionInfo auction, bool won, int bidsUsed, double finalPrice)
- {
- if (_postgresDb == null) return;
-
- try
- {
- var productKey = GenerateProductKey(auction.Name);
- var stat = await _postgresDb.ProductStatistics.FirstOrDefaultAsync(p => p.ProductKey == productKey);
-
- if (stat == null)
- {
- stat = new ProductStatistic
- {
- ProductKey = productKey,
- ProductName = auction.Name,
- TotalAuctions = 0,
- MinBidsSeen = int.MaxValue,
- MaxBidsSeen = 0,
- CompetitionLevel = "Medium"
- };
- _postgresDb.ProductStatistics.Add(stat);
- }
-
- stat.TotalAuctions++;
- stat.AverageFinalPrice = ((stat.AverageFinalPrice * (stat.TotalAuctions - 1)) + (decimal)finalPrice) / stat.TotalAuctions;
- stat.AverageResets = ((stat.AverageResets * (stat.TotalAuctions - 1)) + auction.ResetCount) / stat.TotalAuctions;
+ // Headers browser-like per evitare rilevamento come bot
+ httpClient.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
+ httpClient.DefaultRequestHeaders.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
+ httpClient.DefaultRequestHeaders.Add("Accept-Language", "it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7");
- if (won)
- {
- stat.AverageWinningBids = ((stat.AverageWinningBids * Math.Max(1, stat.TotalAuctions - 1)) + bidsUsed) / stat.TotalAuctions;
- }
+ Console.WriteLine($"[StatsService] Downloading HTML from: {auctionUrl} (timeout: 5s)");
- stat.MinBidsSeen = Math.Min(stat.MinBidsSeen, bidsUsed);
- stat.MaxBidsSeen = Math.Max(stat.MaxBidsSeen, bidsUsed);
- stat.RecommendedMaxBids = (int)(stat.AverageWinningBids * 1.5m); // 50% buffer
- stat.RecommendedMaxPrice = stat.AverageFinalPrice * 1.2m; // 20% buffer
- stat.LastUpdated = DateTime.UtcNow;
-
- // Determina livello competizione
- if (stat.AverageWinningBids > 50) stat.CompetitionLevel = "High";
- else if (stat.AverageWinningBids < 20) stat.CompetitionLevel = "Low";
- else stat.CompetitionLevel = "Medium";
-
- await _postgresDb.SaveChangesAsync();
- }
- catch (Exception ex)
- {
- Console.WriteLine($"[PostgreSQL ERROR] Failed to update product statistics: {ex.Message}");
- }
- }
-
- ///
- /// Aggiorna metriche giornaliere in PostgreSQL
- ///
- private async Task UpdateDailyMetricsAsync(DateTime date, int bidsUsed, double bidCost, bool won, double savings)
- {
- if (_postgresDb == null) return;
-
- try
- {
- var metric = await _postgresDb.DailyMetrics.FirstOrDefaultAsync(m => m.Date.Date == date.Date);
-
- if (metric == null)
- {
- metric = new DailyMetric { Date = date.Date };
- _postgresDb.DailyMetrics.Add(metric);
- }
-
- metric.TotalBidsUsed += bidsUsed;
- metric.MoneySpent += (decimal)(bidsUsed * bidCost);
- if (won) metric.AuctionsWon++; else metric.AuctionsLost++;
- metric.TotalSavings += (decimal)savings;
-
- var totalAuctions = metric.AuctionsWon + metric.AuctionsLost;
- if (totalAuctions > 0)
- {
- metric.WinRate = ((decimal)metric.AuctionsWon / totalAuctions) * 100;
- }
-
- if (metric.MoneySpent > 0)
- {
- metric.ROI = (metric.TotalSavings / metric.MoneySpent) * 100;
- }
-
- await _postgresDb.SaveChangesAsync();
- }
- catch (Exception ex)
- {
- Console.WriteLine($"[PostgreSQL ERROR] Failed to update daily metrics: {ex.Message}");
- }
- }
-
- ///
- /// Genera chiave univoca per prodotto
- ///
- private string GenerateProductKey(string productName)
- {
- var normalized = productName.ToLowerInvariant()
- .Replace(" ", "_")
- .Replace("-", "_");
- return System.Text.RegularExpressions.Regex.Replace(normalized, "[^a-z0-9_]", "");
- }
-
- ///
- /// Ottiene raccomandazioni strategiche da PostgreSQL
- ///
- public async Task> GetStrategicInsightsAsync(string? productKey = null)
- {
- if (!_postgresAvailable || _postgresDb == null)
- {
- return new List();
- }
-
- try
- {
- var query = _postgresDb.StrategicInsights.Where(i => i.IsActive);
+ var html = await httpClient.GetStringAsync(auctionUrl);
- if (!string.IsNullOrEmpty(productKey))
- {
- query = query.Where(i => i.ProductKey == productKey);
- }
-
- return await query.OrderByDescending(i => i.ConfidenceLevel).ToListAsync();
+ Console.WriteLine($"[StatsService] HTML scaricato ({html.Length} chars), parsing...");
+
+ // Usa il metodo esistente di ClosedAuctionsScraper per estrarre le puntate
+ var bidsUsed = ExtractBidsUsedFromHtml(html);
+
+ return bidsUsed;
+ }
+ catch (TaskCanceledException)
+ {
+ Console.WriteLine($"[StatsService] ? Timeout durante download HTML (>5s) - URL: {auctionUrl}");
+ return null;
+ }
+ catch (HttpRequestException ex)
+ {
+ Console.WriteLine($"[StatsService] ? Errore HTTP durante scraping: {ex.Message}");
+ return null;
}
catch (Exception ex)
{
- Console.WriteLine($"[PostgreSQL ERROR] Failed to get insights: {ex.Message}");
- return new List();
+ Console.WriteLine($"[StatsService] ? Errore generico durante scraping: {ex.Message}");
+ return null;
}
}
-
+
///
- /// Ottiene performance puntatori da PostgreSQL
+ /// Estrae le puntate usate dall'HTML (stesso algoritmo di ClosedAuctionsScraper)
///
- public async Task> GetTopCompetitorsAsync(int limit = 10)
+ private int? ExtractBidsUsedFromHtml(string html)
{
- if (!_postgresAvailable || _postgresDb == null)
+ if (string.IsNullOrEmpty(html)) return null;
+
+ // 1) Look for the explicit bids-used span: 628 Puntate utilizzate
+ var match = System.Text.RegularExpressions.Regex.Match(html,
+ "class=\\\"bids-used\\\"[^>]*>[^<]*]*>(?[0-9]{1,7}) ",
+ System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Singleline);
+
+ if (match.Success && int.TryParse(match.Groups["n"].Value, out var val1))
{
- return new List();
+ Console.WriteLine($"[StatsService] Puntate estratte (metodo 1 - bids-used class): {val1}");
+ return val1;
}
-
- try
+
+ // 2) Look for numeric followed by 'Puntate utilizzate' or similar
+ match = System.Text.RegularExpressions.Regex.Match(html,
+ "(?[0-9]{1,7})\\s*(?:Puntate utilizzate|Puntate usate|puntate utilizzate|puntate usate|puntate)\\b",
+ System.Text.RegularExpressions.RegexOptions.IgnoreCase);
+
+ if (match.Success && int.TryParse(match.Groups["n"].Value, out var val2))
{
- return await _postgresDb.BidderPerformances
- .OrderByDescending(b => b.WinRate)
- .Take(limit)
- .ToListAsync();
+ Console.WriteLine($"[StatsService] Puntate estratte (metodo 2 - pattern testo): {val2}");
+ return val2;
}
- catch (Exception ex)
+
+ // 3) Fallbacks
+ match = System.Text.RegularExpressions.Regex.Match(html,
+ "(?[0-9]+)\\s*(?:puntate|Puntate|puntate usate|puntate_usate|pt\\.?|pts)\\b",
+ System.Text.RegularExpressions.RegexOptions.IgnoreCase);
+
+ if (match.Success && int.TryParse(match.Groups["n"].Value, out var val3))
{
- Console.WriteLine($"[PostgreSQL ERROR] Failed to get competitors: {ex.Message}");
- return new List();
+ Console.WriteLine($"[StatsService] Puntate estratte (metodo 3 - fallback): {val3}");
+ return val3;
+ }
+
+ Console.WriteLine($"[StatsService] Nessun pattern trovato per le puntate nell'HTML");
+ return null;
+ }
+
+ ///
+ /// Registra il completamento di un'asta (overload semplificato per compatibilità)
+ ///
+ public async Task RecordAuctionCompletedAsync(AuctionInfo auction, bool won)
+ {
+ if (auction.LastState != null)
+ {
+ await RecordAuctionCompletedAsync(auction, auction.LastState, won);
+ }
+ else
+ {
+ Console.WriteLine("[StatsService] Cannot record auction - LastState is null");
}
}
+
+ ///
+ /// Ottiene i limiti consigliati per un prodotto
+ ///
+ public async Task GetRecommendedLimitsAsync(string productName)
+ {
+ if (_productStatsService == null) return null;
+
+ var productKey = ProductStatisticsService.GenerateProductKey(productName);
+ return await _productStatsService.GetRecommendedLimitsAsync(productKey);
+ }
+
+ ///
+ /// Ottiene tutte le statistiche prodotto
+ ///
+ public async Task> GetAllProductStatisticsAsync()
+ {
+ if (_productStatsService == null) return new List();
+ return await _productStatsService.GetAllProductStatisticsAsync();
+ }
- // Metodi esistenti per compatibilità SQLite
+ // Metodi per query statistiche
public async Task> GetDailyStatsAsync(int days = 30)
{
+ if (!IsAvailable)
+ {
+ return new List();
+ }
+
var to = DateTime.UtcNow;
var from = to.AddDays(-days);
return await _db.GetDailyStatsAsync(from, to);
@@ -319,6 +342,11 @@ namespace AutoBidder.Services
public async Task GetTotalStatsAsync()
{
+ if (!IsAvailable)
+ {
+ return new TotalStats();
+ }
+
var stats = await GetDailyStatsAsync(365);
return new TotalStats
@@ -338,13 +366,23 @@ namespace AutoBidder.Services
};
}
- public async Task> GetRecentAuctionResultsAsync(int limit = 50)
+ public async Task> GetRecentAuctionResultsAsync(int limit = 50)
{
+ if (!IsAvailable)
+ {
+ return new List();
+ }
+
return await _db.GetRecentAuctionResultsAsync(limit);
}
public async Task CalculateROIAsync()
{
+ if (!IsAvailable)
+ {
+ return 0;
+ }
+
var stats = await GetTotalStatsAsync();
if (stats.TotalMoneySpent <= 0)
@@ -355,11 +393,22 @@ namespace AutoBidder.Services
public async Task GetChartDataAsync(int days = 30)
{
+ if (!IsAvailable)
+ {
+ return new ChartData
+ {
+ Labels = new List(),
+ MoneySpent = new List(),
+ Savings = new List()
+ };
+ }
+
var stats = await GetDailyStatsAsync(days);
var allDates = new List();
var startDate = DateTime.UtcNow.AddDays(-days);
+
for (int i = 0; i < days; i++)
{
var date = startDate.AddDays(i).ToString("yyyy-MM-dd");
@@ -387,11 +436,6 @@ namespace AutoBidder.Services
Savings = allDates.Select(s => s.TotalSavings).ToList()
};
}
-
- ///
- /// Indica se il database PostgreSQL è disponibile
- ///
- public bool IsPostgresAvailable => _postgresAvailable;
}
// Classi esistenti per compatibilità
diff --git a/Mimante/Shared/NavMenu.razor b/Mimante/Shared/NavMenu.razor
index eba60e5..67e9796 100644
--- a/Mimante/Shared/NavMenu.razor
+++ b/Mimante/Shared/NavMenu.razor
@@ -24,11 +24,6 @@
Esplora Aste
-
-
public string MinLogLevel { get; set; } = "Normal";
-
- // CONFIGURAZIONE DATABASE POSTGRESQL
- ///
- /// Abilita l'uso di PostgreSQL per statistiche avanzate
- ///
- public bool UsePostgreSQL { get; set; } = true;
-
- ///
- /// Connection string PostgreSQL
- ///
- public string PostgresConnectionString { get; set; } = "Host=localhost;Port=5432;Database=autobidder_stats;Username=autobidder;Password=autobidder_password";
-
- ///
- /// Auto-crea schema database se mancante
- ///
- public bool AutoCreateDatabaseSchema { get; set; } = true;
-
- ///
- /// Fallback automatico a SQLite se PostgreSQL non disponibile
- ///
- public bool FallbackToSQLite { get; set; } = true;
}
public static class SettingsManager
diff --git a/Mimante/docker-compose.yml b/Mimante/docker-compose.yml
index bef258d..42e58ce 100644
--- a/Mimante/docker-compose.yml
+++ b/Mimante/docker-compose.yml
@@ -1,31 +1,6 @@
version: '3.8'
services:
- # ================================================
- # PostgreSQL Database (statistiche avanzate)
- # ================================================
- postgres:
- image: postgres:16-alpine
- container_name: autobidder-postgres
- environment:
- POSTGRES_DB: autobidder_stats
- POSTGRES_USER: ${POSTGRES_USER:-autobidder}
- POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-autobidder_password}
- POSTGRES_INITDB_ARGS: --encoding=UTF8
- volumes:
- - postgres-data:/var/lib/postgresql/data
- - ./postgres-backups:/backups
- ports:
- - "5432:5432"
- restart: unless-stopped
- healthcheck:
- test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-autobidder} -d autobidder_stats"]
- interval: 10s
- timeout: 5s
- retries: 5
- networks:
- - autobidder-network
-
# ================================================
# AutoBidder Application
# ================================================
@@ -37,37 +12,29 @@ services:
BUILD_CONFIGURATION: Release
image: gitea.encke-hake.ts.net/alby96/autobidder:latest
container_name: autobidder
- depends_on:
- postgres:
- condition: service_healthy
ports:
- "${APP_PORT:-5000}:8080" # Host:Container (HTTP only)
volumes:
- # Persistent data (SQLite, backups, logs)
+ # Persistent data (SQLite databases, backups, logs, keys)
+ # Tutti i dati persistenti sono salvati in questo volume
- ./Data:/app/Data
-
- # PostgreSQL backups
- - ./postgres-backups:/app/Data/backups
environment:
# ASP.NET Core
- ASPNETCORE_ENVIRONMENT=Production
- ASPNETCORE_URLS=http://+:8080
+ # ============================================
+ # DATABASE PATH - Volume persistente Docker
+ # ============================================
+ # Tutti i database SQLite e dati persistenti usano questo path
+ - DATA_PATH=/app/Data
+
# Autenticazione applicazione (SICUREZZA)
- ADMIN_USERNAME=${ADMIN_USERNAME:-admin}
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
- # PostgreSQL connection
- - ConnectionStrings__PostgreSQL=Host=postgres;Port=5432;Database=autobidder_stats;Username=${POSTGRES_USER:-autobidder};Password=${POSTGRES_PASSWORD:-autobidder_password}
-
- # Database settings
- - Database__UsePostgres=${USE_POSTGRES:-true}
- - Database__AutoCreateSchema=true
- - Database__FallbackToSQLite=true
-
# Logging
- Logging__LogLevel__Default=${LOG_LEVEL:-Information}
- - Logging__LogLevel__Microsoft.EntityFrameworkCore=Warning
# Timezone
- TZ=Europe/Rome
@@ -81,10 +48,6 @@ services:
networks:
- autobidder-network
-volumes:
- postgres-data:
- driver: local
-
networks:
autobidder-network:
driver: bridge
diff --git a/Mimante/wwwroot/css/modern-pages.css b/Mimante/wwwroot/css/modern-pages.css
index 522a4f6..7267ea7 100644
--- a/Mimante/wwwroot/css/modern-pages.css
+++ b/Mimante/wwwroot/css/modern-pages.css
@@ -1,4 +1,33 @@
-/* === MODERN PAGE STYLES (append to app-wpf.css) === */
+/* === MODERN PAGE STYLES (append to app-wpf.css) === */
+
+/* ✅ NUOVO: Stili per selezione riga e colonna puntate */
+.table-hover tbody tr {
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.table-hover tbody tr.selected-row {
+ background-color: rgba(76, 175, 80, 0.15) !important;
+ border-left: 4px solid #4CAF50;
+}
+
+.table-hover tbody tr:hover:not(.selected-row) {
+ background-color: rgba(255, 255, 255, 0.05);
+}
+
+/* Colonna Puntate - testo grassetto e leggibile */
+.bids-column {
+ font-weight: bold !important;
+ color: var(--text-primary) !important;
+}
+
+/* Larghezza colonna puntate leggermente maggiore */
+.col-click {
+ min-width: 85px;
+ width: 85px;
+ white-space: nowrap;
+}
+
.page-header {
display: flex;
align-items: center;