Gestione avanzata database e rimozione MaxClicks

Aggiunta sezione impostazioni per manutenzione database (auto-salvataggio, pulizia duplicati/incompleti, retention, ottimizzazione). Implementati metodi asincroni in DatabaseService per pulizia e statistiche. Pulizia automatica all’avvio secondo impostazioni. Rimossa la proprietà MaxClicks da modello, UI e logica. Migliorata la sicurezza thread-safe e la trasparenza nella gestione dati. Spostato il badge versione nelle info applicazione.
This commit is contained in:
2026-01-24 01:30:49 +01:00
parent a0ec72f6c0
commit 77eb9943d0
9 changed files with 607 additions and 54 deletions

View File

@@ -547,7 +547,6 @@ private bool isUpdatingInBackground = false;
CheckAuctionOpenBeforeBid = settings.DefaultCheckAuctionOpenBeforeBid,
MinPrice = settings.DefaultMinPrice,
MaxPrice = settings.DefaultMaxPrice,
MaxClicks = settings.DefaultMaxClicks,
MinResets = settings.DefaultMinResets,
MaxResets = settings.DefaultMaxResets,

View File

@@ -237,14 +237,10 @@
</div>
<div class="row">
<div class="col-md-6 info-group">
<div class="col-md-12 info-group">
<label><i class="bi bi-speedometer2"></i> Anticipo (ms):</label>
<input type="number" class="form-control" @bind="selectedAuction.BidBeforeDeadlineMs" @bind:after="SaveAuctions" />
</div>
<div class="col-md-6 info-group">
<label><i class="bi bi-hand-index-thumb"></i> Max Click:</label>
<input type="number" class="form-control" @bind="selectedAuction.MaxClicks" @bind:after="SaveAuctions" />
</div>
</div>
<div class="row">
@@ -396,7 +392,10 @@
<!-- TAB STORIA PUNTATE -->
<div class="tab-pane fade" id="content-history" role="tabpanel">
<div class="tab-panel-content">
@if (selectedAuction.RecentBids != null && selectedAuction.RecentBids.Any())
@{
var recentBidsList = GetRecentBidsSafe(selectedAuction);
}
@if (recentBidsList.Any())
{
<div class="table-responsive">
<table class="table table-sm table-striped">
@@ -409,7 +408,7 @@
</tr>
</thead>
<tbody>
@foreach (var bid in selectedAuction.RecentBids.Take(50))
@foreach (var bid in recentBidsList.Take(50))
{
<tr class="@(bid.IsMyBid ? "table-success" : "")">
<td>
@@ -440,11 +439,12 @@
<!-- TAB PUNTATORI -->
<div class="tab-pane fade" id="content-bidders" role="tabpanel">
<div class="tab-panel-content">
@if (selectedAuction.RecentBids != null && selectedAuction.RecentBids.Any())
@{
// Crea una copia thread-safe per evitare modifiche durante l'enumerazione
var recentBidsCopy = GetRecentBidsSafe(selectedAuction);
}
@if (recentBidsCopy.Any())
{
// 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)
@@ -574,8 +574,3 @@
</div>
</div>
}
<!-- Versione in basso a destra -->
<div class="version-badge">
<i class="bi bi-box-seam"></i> v1.0.0
</div>

View File

@@ -385,7 +385,6 @@ namespace AutoBidder.Pages
CheckAuctionOpenBeforeBid = settings.DefaultCheckAuctionOpenBeforeBid,
MinPrice = settings.DefaultMinPrice,
MaxPrice = settings.DefaultMaxPrice,
MaxClicks = settings.DefaultMaxClicks,
MinResets = settings.DefaultMinResets,
MaxResets = settings.DefaultMaxResets,
IsActive = isActive,
@@ -748,22 +747,8 @@ namespace AutoBidder.Pages
private string GetStatusAnimationClass(AuctionInfo auction)
{
// Animazioni per stati speciali
if (auction.LastState != null)
{
switch (auction.LastState.Status)
{
case AuctionStatus.EndedWon:
return "status-anim-won";
case AuctionStatus.Running when auction.IsAttackInProgress:
return "status-anim-attacking";
}
}
if (!auction.IsActive) return "";
if (auction.IsPaused) return "status-anim-paused";
if (auction.IsAttackInProgress) return "status-anim-attacking";
return "status-anim-active";
// Animazioni disabilitate - i colori sono sufficienti per identificare lo stato
return "";
}
private string GetPriceDisplay(AuctionInfo? auction)
@@ -990,6 +975,29 @@ namespace AutoBidder.Pages
return auction.AuctionLog.TakeLast(50);
}
/// <summary>
/// Ottiene una copia thread-safe della lista RecentBids
/// </summary>
private List<BidHistoryEntry> GetRecentBidsSafe(AuctionInfo? auction)
{
if (auction?.RecentBids == null)
return new List<BidHistoryEntry>();
try
{
// Lock per evitare modifiche durante la copia
lock (auction.RecentBids)
{
return auction.RecentBids.ToList();
}
}
catch
{
// Fallback in caso di errore
return new List<BidHistoryEntry>();
}
}
private string GetLogEntryClass(LogEntry logEntry)
{
try
@@ -1130,7 +1138,6 @@ namespace AutoBidder.Pages
selectedAuction.MaxPrice = limits.MaxPrice;
selectedAuction.MinResets = limits.MinResets;
selectedAuction.MaxResets = limits.MaxResets;
selectedAuction.MaxClicks = limits.MaxBids;
SaveAuctions();
@@ -1144,7 +1151,6 @@ namespace AutoBidder.Pages
selectedAuction.MaxPrice = limits.MaxPrice;
selectedAuction.MinResets = limits.MinResets;
selectedAuction.MaxResets = limits.MaxResets;
selectedAuction.MaxClicks = limits.MaxBids;
SaveAuctions();

View File

@@ -208,6 +208,187 @@
</div>
</div>
</div>
<!-- DATABASE MANAGEMENT -->
<div class="accordion-item">
<h2 class="accordion-header" id="heading-database">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-database" aria-expanded="false" aria-controls="collapse-database">
<i class="bi bi-database-fill me-2"></i> Gestione Database
</button>
</h2>
<div id="collapse-database" class="accordion-collapse collapse" aria-labelledby="heading-database" data-bs-parent="#settingsAccordion">
<div class="accordion-body">
<!-- Impostazioni Auto-Salvataggio -->
<h6 class="fw-bold mb-3"><i class="bi bi-floppy"></i> Salvataggio Automatico</h6>
<div class="row g-3 mb-4">
<div class="col-12">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="dbAutoSave" @bind="settings.DatabaseAutoSaveEnabled" />
<label class="form-check-label" for="dbAutoSave">
<strong>Salva aste completate automaticamente</strong>
<div class="form-text">Salva statistiche nel database quando un'asta termina (consigliato)</div>
</label>
</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-calendar-range"></i> Durata conservazione (giorni)</label>
<input type="number" class="form-control" @bind="settings.DatabaseMaxRetentionDays" min="0" />
<div class="form-text">0 = mantieni tutto | 180 = 6 mesi (consigliato)</div>
</div>
</div>
<!-- Pulizia Automatica -->
<h6 class="fw-bold mb-3"><i class="bi bi-brush"></i> Pulizia Automatica all'Avvio</h6>
<div class="row g-3 mb-4">
<div class="col-12">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="dbAutoCleanDup" @bind="settings.DatabaseAutoCleanupDuplicates" />
<label class="form-check-label" for="dbAutoCleanDup">
<strong>Rimuovi duplicati automaticamente</strong>
<div class="form-text">Elimina record duplicati all'avvio (consigliato)</div>
</label>
</div>
</div>
<div class="col-12">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="dbAutoCleanInc" @bind="settings.DatabaseAutoCleanupIncomplete" />
<label class="form-check-label" for="dbAutoCleanInc">
<strong>Rimuovi dati incompleti automaticamente</strong>
<div class="form-text">Elimina record con dati invalidi (prezzi negativi, ecc.)</div>
</label>
</div>
</div>
</div>
<!-- Statistiche e Manutenzione Manuale -->
<h6 class="fw-bold mb-3"><i class="bi bi-tools"></i> Manutenzione Manuale</h6>
<div class="alert alert-info border-0 shadow-sm mb-3">
<div class="row g-2">
<div class="col-6 col-md-3">
<strong>Duplicati:</strong>
<div class="fs-4 fw-bold text-warning">@dbDuplicatesCount</div>
</div>
<div class="col-6 col-md-3">
<strong>Incompleti:</strong>
<div class="fs-4 fw-bold text-danger">@dbIncompleteCount</div>
</div>
<div class="col-12 col-md-6 text-md-end">
<button class="btn btn-sm btn-outline-primary" @onclick="RefreshDbStats" disabled="@isLoadingDbStats">
@if (isLoadingDbStats)
{
<span class="spinner-border spinner-border-sm me-1"></span>
}
else
{
<i class="bi bi-arrow-clockwise me-1"></i>
}
Aggiorna
</button>
</div>
</div>
</div>
<div class="d-flex flex-wrap gap-2">
<button class="btn btn-warning text-dark" @onclick="CleanupDuplicates" disabled="@(isCleaningDb || dbDuplicatesCount == 0)">
<i class="bi bi-trash"></i> Rimuovi Duplicati (@dbDuplicatesCount)
</button>
<button class="btn btn-danger" @onclick="CleanupIncomplete" disabled="@(isCleaningDb || dbIncompleteCount == 0)">
<i class="bi bi-trash"></i> Rimuovi Incompleti (@dbIncompleteCount)
</button>
<button class="btn btn-primary" @onclick="CleanupDatabase" disabled="@isCleaningDb">
<i class="bi bi-stars"></i> Pulizia Completa
</button>
<button class="btn btn-info text-white" @onclick="OptimizeDatabase" disabled="@isCleaningDb">
<i class="bi bi-lightning-charge"></i> Ottimizza (VACUUM)
</button>
</div>
@if (!string.IsNullOrEmpty(dbCleanupMessage))
{
<div class="alert @(dbCleanupSuccess ? "alert-success" : "alert-warning") border-0 shadow-sm mt-3">
<i class="bi @(dbCleanupSuccess ? "bi-check-circle-fill" : "bi-exclamation-triangle-fill") me-2"></i>
@dbCleanupMessage
</div>
}
<div class="mt-4">
<button class="btn btn-success" @onclick="SaveSettings"><i class="bi bi-check-lg"></i> Salva Impostazioni</button>
</div>
</div>
</div>
</div>
<!-- INFORMAZIONI APPLICAZIONE -->
<div class="accordion-item">
<h2 class="accordion-header" id="heading-info">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-info" aria-expanded="false" aria-controls="collapse-info">
<i class="bi bi-info-circle-fill me-2"></i> Informazioni Applicazione
</button>
</h2>
<div id="collapse-info" class="accordion-collapse collapse" aria-labelledby="heading-info" data-bs-parent="#settingsAccordion">
<div class="accordion-body">
<div class="row g-3">
<div class="col-12 col-md-6">
<div class="card bg-light border-0">
<div class="card-body">
<h6 class="text-muted mb-2">Versione</h6>
<div class="d-flex align-items-center">
<i class="bi bi-box-seam text-primary me-2" style="font-size: 1.5rem;"></i>
<span class="fs-4 fw-bold">v1.0.0</span>
</div>
</div>
</div>
</div>
<div class="col-12 col-md-6">
<div class="card bg-light border-0">
<div class="card-body">
<h6 class="text-muted mb-2">Ambiente</h6>
<div class="d-flex align-items-center">
<i class="bi bi-code-slash text-success me-2" style="font-size: 1.5rem;"></i>
<span class="fs-4 fw-bold">.NET 8</span>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="card bg-light border-0">
<div class="card-body">
<h6 class="text-muted mb-2">Informazioni Sistema</h6>
<div class="row g-2">
<div class="col-12 col-md-6">
<small class="text-muted">Database:</small>
<div class="fw-bold">
@if (DbService.IsAvailable)
{
<span class="text-success"><i class="bi bi-check-circle-fill"></i> Operativo</span>
}
else
{
<span class="text-danger"><i class="bi bi-x-circle-fill"></i> Non disponibile</span>
}
</div>
</div>
<div class="col-12 col-md-6">
<small class="text-muted">Sessione:</small>
<div class="fw-bold">
@if (!string.IsNullOrEmpty(currentUsername))
{
<span class="text-success"><i class="bi bi-check-circle-fill"></i> @currentUsername</span>
}
else
{
<span class="text-warning"><i class="bi bi-exclamation-circle-fill"></i> Non connesso</span>
}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -423,6 +604,159 @@ private System.Threading.Timer? updateTimer;
return "bg-success";
}
// ???????????????????????????????????????????????????????????????
// DATABASE MANAGEMENT
// ???????????????????????????????????????????????????????????????
[Inject] private DatabaseService DbService { get; set; } = default!;
private int dbDuplicatesCount = 0;
private int dbIncompleteCount = 0;
private bool isLoadingDbStats = false;
private bool isCleaningDb = false;
private string? dbCleanupMessage = null;
private bool dbCleanupSuccess = false;
private async Task RefreshDbStats()
{
if (!DbService.IsAvailable) return;
isLoadingDbStats = true;
dbCleanupMessage = null;
StateHasChanged();
try
{
dbDuplicatesCount = await DbService.CountDuplicateAuctionResultsAsync();
dbIncompleteCount = await DbService.CountIncompleteAuctionResultsAsync();
}
catch (Exception ex)
{
dbCleanupMessage = $"Errore: {ex.Message}";
dbCleanupSuccess = false;
}
finally
{
isLoadingDbStats = false;
StateHasChanged();
}
}
private async Task CleanupDuplicates()
{
if (!DbService.IsAvailable) return;
isCleaningDb = true;
dbCleanupMessage = null;
StateHasChanged();
try
{
var removed = await DbService.RemoveDuplicateAuctionResultsAsync();
dbCleanupMessage = $"? Rimossi {removed} record duplicati";
dbCleanupSuccess = true;
await RefreshDbStats();
}
catch (Exception ex)
{
dbCleanupMessage = $"Errore: {ex.Message}";
dbCleanupSuccess = false;
}
finally
{
isCleaningDb = false;
StateHasChanged();
}
}
private async Task CleanupIncomplete()
{
if (!DbService.IsAvailable) return;
isCleaningDb = true;
dbCleanupMessage = null;
StateHasChanged();
try
{
var removed = await DbService.RemoveIncompleteAuctionResultsAsync();
dbCleanupMessage = $"? Rimossi {removed} record incompleti";
dbCleanupSuccess = true;
await RefreshDbStats();
}
catch (Exception ex)
{
dbCleanupMessage = $"Errore: {ex.Message}";
dbCleanupSuccess = false;
}
finally
{
isCleaningDb = false;
StateHasChanged();
}
}
private async Task CleanupDatabase()
{
if (!DbService.IsAvailable) return;
isCleaningDb = true;
dbCleanupMessage = null;
StateHasChanged();
try
{
var message = await DbService.CleanupDatabaseAsync();
dbCleanupMessage = $"? {message}";
dbCleanupSuccess = true;
await RefreshDbStats();
}
catch (Exception ex)
{
dbCleanupMessage = $"Errore: {ex.Message}";
dbCleanupSuccess = false;
}
finally
{
isCleaningDb = false;
StateHasChanged();
}
}
private async Task OptimizeDatabase()
{
if (!DbService.IsAvailable) return;
isCleaningDb = true;
dbCleanupMessage = null;
StateHasChanged();
try
{
await DbService.OptimizeDatabaseAsync();
dbCleanupMessage = "? Database ottimizzato (VACUUM eseguito)";
dbCleanupSuccess = true;
}
catch (Exception ex)
{
dbCleanupMessage = $"Errore: {ex.Message}";
dbCleanupSuccess = false;
}
finally
{
isCleaningDb = false;
StateHasChanged();
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && DbService.IsAvailable)
{
await RefreshDbStats();
}
}
public void Dispose()
{
updateTimer?.Dispose();

View File

@@ -277,6 +277,55 @@ using (var scope = app.Services.CreateScope())
var isHealthy = await databaseService.CheckDatabaseHealthAsync();
Console.WriteLine($"[DB] Database health check: {(isHealthy ? "OK" : "FAILED")}");
// 🔥 MANUTENZIONE AUTOMATICA DATABASE
var settings = AutoBidder.Utilities.SettingsManager.Load();
if (settings.DatabaseAutoCleanupDuplicates)
{
Console.WriteLine("[DB] Checking for duplicate records...");
var duplicateCount = await databaseService.CountDuplicateAuctionResultsAsync();
if (duplicateCount > 0)
{
Console.WriteLine($"[DB] Found {duplicateCount} duplicates - removing...");
var removed = await databaseService.RemoveDuplicateAuctionResultsAsync();
Console.WriteLine($"[DB] ✓ Removed {removed} duplicate auction results");
}
else
{
Console.WriteLine("[DB] ✓ No duplicates found");
}
}
if (settings.DatabaseAutoCleanupIncomplete)
{
Console.WriteLine("[DB] Checking for incomplete records...");
var incompleteCount = await databaseService.CountIncompleteAuctionResultsAsync();
if (incompleteCount > 0)
{
Console.WriteLine($"[DB] Found {incompleteCount} incomplete records - removing...");
var removed = await databaseService.RemoveIncompleteAuctionResultsAsync();
Console.WriteLine($"[DB] ✓ Removed {removed} incomplete auction results");
}
else
{
Console.WriteLine("[DB] ✓ No incomplete records found");
}
}
if (settings.DatabaseMaxRetentionDays > 0)
{
Console.WriteLine($"[DB] Checking for records older than {settings.DatabaseMaxRetentionDays} days...");
var oldCount = await databaseService.RemoveOldAuctionResultsAsync(settings.DatabaseMaxRetentionDays);
if (oldCount > 0)
{
Console.WriteLine($"[DB] ✓ Removed {oldCount} old auction results");
}
else
{
Console.WriteLine($"[DB] ✓ No old records to remove");
}
}
// 🆕 Esegui diagnostica completa se ci sono problemi o se richiesto
var runDiagnostics = Environment.GetEnvironmentVariable("DB_DIAGNOSTICS")?.ToLower() == "true";
if (!isHealthy || runDiagnostics)

View File

@@ -176,9 +176,8 @@ namespace AutoBidder.Services
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}");
OnLog?.Invoke($"[LIMITS] Aggiornati limiti per {auction.Name}: MinPrice={minPrice:F2}, MaxPrice={maxPrice:F2}, MinResets={minResets}, MaxResets={maxResets}");
return true;
}
}
@@ -202,7 +201,6 @@ namespace AutoBidder.Services
auction.MaxPrice = maxPrice;
auction.MinResets = minResets;
auction.MaxResets = maxResets;
auction.MaxClicks = maxBids;
count++;
OnLog?.Invoke($"[LIMITS] Aggiornati limiti per {auction.Name}");
@@ -653,14 +651,6 @@ namespace AutoBidder.Services
return false;
}
// ??? CONTROLLO 5: MaxClicks
int myBidsCount = auction.BidHistory.Count(b => b.EventType == BidEventType.MyBid);
if (auction.MaxClicks > 0 && myBidsCount >= auction.MaxClicks)
{
auction.AddLog($"[CLICKS] Click massimi raggiunti: {myBidsCount} >= Max {auction.MaxClicks}");
return false;
}
// ?? CONTROLLO 6: Cooldown (evita puntate multiple ravvicinate)
if (auction.LastClickAt.HasValue)
{

View File

@@ -219,15 +219,20 @@ 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)
Console.WriteLine($"[BidooBrowser] Trovate {auctions.Count} aste nella categoria {category.DisplayName}");
// ?? DEBUG: Verifica quante aste hanno IsCreditAuction = true
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)");
}
var creditCount = auctions.Count(a => a.IsCreditAuction);
Console.WriteLine($"[BidooBrowser] DEBUG Aste di Puntate: {creditCount}/{auctions.Count} hanno IsCreditAuction=true");
Console.WriteLine($"[BidooBrowser] Trovate {auctions.Count} aste nella categoria {category.DisplayName}");
// Log primi 3 nomi per debug
foreach (var a in auctions.Take(3))
{
Console.WriteLine($"[BidooBrowser] - {a.Name} (ID: {a.AuctionId}, IsCreditAuction: {a.IsCreditAuction})");
}
}
}
catch (Exception ex)
{

View File

@@ -1037,6 +1037,7 @@ namespace AutoBidder.Services
/// <summary>
/// Salva risultato asta con dati completi per analytics
/// 🔥 ANTI-DUPLICAZIONE: Usa UPSERT basato su AuctionId per evitare duplicati
/// </summary>
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,
@@ -1044,6 +1045,22 @@ namespace AutoBidder.Services
{
var closedAtHour = DateTime.UtcNow.Hour;
// 🔥 FIX DUPLICAZIONE: Prima controlla se esiste già
var checkSql = "SELECT COUNT(*) FROM AuctionResults WHERE AuctionId = @auctionId;";
await using var checkConn = await GetConnectionAsync();
await using var checkCmd = checkConn.CreateCommand();
checkCmd.CommandText = checkSql;
checkCmd.Parameters.AddWithValue("@auctionId", auctionId);
var countResult = await checkCmd.ExecuteScalarAsync();
var exists = Convert.ToInt64(countResult ?? 0) > 0;
if (exists)
{
Console.WriteLine($"[DatabaseService] ⚠ Asta {auctionId} già presente in AuctionResults - Skipping INSERT to prevent duplicate");
return;
}
var sql = @"
INSERT INTO AuctionResults
(AuctionId, AuctionName, FinalPrice, BidsUsed, Won, BuyNowPrice, ShippingCost, TotalCost, Savings, Timestamp,
@@ -1368,6 +1385,136 @@ namespace AutoBidder.Services
return results;
}
// ═══════════════════════════════════════════════════════════════════
// MANUTENZIONE DATABASE
// ═══════════════════════════════════════════════════════════════════
/// <summary>
/// Rimuove record duplicati dalla tabella AuctionResults
/// Mantiene solo il record più recente per ogni AuctionId
/// </summary>
public async Task<int> RemoveDuplicateAuctionResultsAsync()
{
var sql = @"
DELETE FROM AuctionResults
WHERE Id NOT IN (
SELECT MAX(Id)
FROM AuctionResults
GROUP BY AuctionId
);
";
await using var connection = await GetConnectionAsync();
await using var cmd = connection.CreateCommand();
cmd.CommandText = sql;
var deletedRows = await cmd.ExecuteNonQueryAsync();
Console.WriteLine($"[DatabaseService] Rimossi {deletedRows} record duplicati da AuctionResults");
return deletedRows;
}
/// <summary>
/// Rimuove aste con dati incompleti o invalidi
/// </summary>
public async Task<int> RemoveIncompleteAuctionResultsAsync()
{
var sql = @"
DELETE FROM AuctionResults
WHERE
AuctionId IS NULL OR AuctionId = '' OR
AuctionName IS NULL OR AuctionName = '' OR
FinalPrice < 0 OR FinalPrice > 10000 OR
BidsUsed < 0 OR BidsUsed > 50000 OR
(BuyNowPrice IS NOT NULL AND BuyNowPrice <= 0) OR
(TotalCost IS NOT NULL AND TotalCost < 0) OR
(Savings IS NOT NULL AND Savings < -1000 OR Savings > 1000);
";
await using var connection = await GetConnectionAsync();
await using var cmd = connection.CreateCommand();
cmd.CommandText = sql;
var deletedRows = await cmd.ExecuteNonQueryAsync();
Console.WriteLine($"[DatabaseService] Rimossi {deletedRows} record con dati incompleti/invalidi");
return deletedRows;
}
/// <summary>
/// Esegue pulizia completa del database (duplicati + incompleti + VACUUM)
/// </summary>
public async Task<string> CleanupDatabaseAsync()
{
var duplicates = await RemoveDuplicateAuctionResultsAsync();
var incomplete = await RemoveIncompleteAuctionResultsAsync();
// VACUUM per recuperare spazio
await OptimizeDatabaseAsync();
return $"Rimossi {duplicates} duplicati e {incomplete} record incompleti. Database ottimizzato.";
}
/// <summary>
/// Conta i record duplicati senza rimuoverli
/// </summary>
public async Task<int> CountDuplicateAuctionResultsAsync()
{
var sql = @"
SELECT COUNT(*) - COUNT(DISTINCT AuctionId)
FROM AuctionResults;
";
await using var connection = await GetConnectionAsync();
await using var cmd = connection.CreateCommand();
cmd.CommandText = sql;
var result = await cmd.ExecuteScalarAsync();
return Convert.ToInt32(result ?? 0);
}
/// <summary>
/// Conta record con dati incompleti
/// </summary>
public async Task<int> CountIncompleteAuctionResultsAsync()
{
var sql = @"
SELECT COUNT(*)
FROM AuctionResults
WHERE
AuctionId IS NULL OR AuctionId = '' OR
AuctionName IS NULL OR AuctionName = '' OR
FinalPrice < 0 OR FinalPrice > 10000 OR
BidsUsed < 0 OR BidsUsed > 50000 OR
(BuyNowPrice IS NOT NULL AND BuyNowPrice <= 0) OR
(TotalCost IS NOT NULL AND TotalCost < 0) OR
(Savings IS NOT NULL AND Savings < -1000 OR Savings > 1000);
";
await using var connection = await GetConnectionAsync();
await using var cmd = connection.CreateCommand();
cmd.CommandText = sql;
var result = await cmd.ExecuteScalarAsync();
return Convert.ToInt32(result ?? 0);
}
/// <summary>
/// Rimuove record più vecchi di N giorni
/// </summary>
public async Task<int> RemoveOldAuctionResultsAsync(int daysToKeep)
{
if (daysToKeep <= 0) return 0;
var cutoffDate = DateTime.UtcNow.AddDays(-daysToKeep).ToString("O");
var sql = "DELETE FROM AuctionResults WHERE Timestamp < @cutoffDate;";
await using var connection = await GetConnectionAsync();
await using var cmd = connection.CreateCommand();
cmd.CommandText = sql;
cmd.Parameters.AddWithValue("@cutoffDate", cutoffDate);
var deletedRows = await cmd.ExecuteNonQueryAsync();
Console.WriteLine($"[DatabaseService] Rimossi {deletedRows} record più vecchi di {daysToKeep} giorni");
return deletedRows;
}
public void Dispose()
{
// Non ci sono risorse da rilasciare - le connessioni sono gestite con using
@@ -1381,7 +1528,6 @@ namespace AutoBidder.Services
public int Version { get; }
public string Description { get; }
private readonly Func<SqliteConnection, Task> _execute;
private string? _lastSqlExecuted;
public Migration(int version, string description, Func<SqliteConnection, Task> execute)
{

View File

@@ -70,6 +70,35 @@ namespace AutoBidder.Utilities
/// Default: "Normal" (uso giornaliero - errori e warning)
/// </summary>
public string MinLogLevel { get; set; } = "Normal";
// ???????????????????????????????????????????????????????????????
// IMPOSTAZIONI DATABASE
// ???????????????????????????????????????????????????????????????
/// <summary>
/// Abilita il salvataggio automatico delle aste completate nel database.
/// Default: true (consigliato per statistiche)
/// </summary>
public bool DatabaseAutoSaveEnabled { get; set; } = true;
/// <summary>
/// Esegue pulizia automatica duplicati all'avvio dell'applicazione.
/// Default: true (consigliato per mantenere database pulito)
/// </summary>
public bool DatabaseAutoCleanupDuplicates { get; set; } = true;
/// <summary>
/// Esegue pulizia automatica record incompleti all'avvio.
/// Default: false (può rimuovere dati utili in caso di errori temporanei)
/// </summary>
public bool DatabaseAutoCleanupIncomplete { get; set; } = false;
/// <summary>
/// Numero massimo di giorni da mantenere nei risultati aste.
/// Record più vecchi vengono eliminati automaticamente.
/// Default: 180 (6 mesi), 0 = disabilitato
/// </summary>
public int DatabaseMaxRetentionDays { get; set; } = 180;
}
public static class SettingsManager