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:
@@ -547,7 +547,6 @@ private bool isUpdatingInBackground = false;
|
|||||||
CheckAuctionOpenBeforeBid = settings.DefaultCheckAuctionOpenBeforeBid,
|
CheckAuctionOpenBeforeBid = settings.DefaultCheckAuctionOpenBeforeBid,
|
||||||
MinPrice = settings.DefaultMinPrice,
|
MinPrice = settings.DefaultMinPrice,
|
||||||
MaxPrice = settings.DefaultMaxPrice,
|
MaxPrice = settings.DefaultMaxPrice,
|
||||||
MaxClicks = settings.DefaultMaxClicks,
|
|
||||||
MinResets = settings.DefaultMinResets,
|
MinResets = settings.DefaultMinResets,
|
||||||
MaxResets = settings.DefaultMaxResets,
|
MaxResets = settings.DefaultMaxResets,
|
||||||
|
|
||||||
|
|||||||
@@ -237,14 +237,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<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>
|
<label><i class="bi bi-speedometer2"></i> Anticipo (ms):</label>
|
||||||
<input type="number" class="form-control" @bind="selectedAuction.BidBeforeDeadlineMs" @bind:after="SaveAuctions" />
|
<input type="number" class="form-control" @bind="selectedAuction.BidBeforeDeadlineMs" @bind:after="SaveAuctions" />
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -396,7 +392,10 @@
|
|||||||
<!-- TAB STORIA PUNTATE -->
|
<!-- TAB STORIA PUNTATE -->
|
||||||
<div class="tab-pane fade" id="content-history" role="tabpanel">
|
<div class="tab-pane fade" id="content-history" role="tabpanel">
|
||||||
<div class="tab-panel-content">
|
<div class="tab-panel-content">
|
||||||
@if (selectedAuction.RecentBids != null && selectedAuction.RecentBids.Any())
|
@{
|
||||||
|
var recentBidsList = GetRecentBidsSafe(selectedAuction);
|
||||||
|
}
|
||||||
|
@if (recentBidsList.Any())
|
||||||
{
|
{
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-sm table-striped">
|
<table class="table table-sm table-striped">
|
||||||
@@ -409,7 +408,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach (var bid in selectedAuction.RecentBids.Take(50))
|
@foreach (var bid in recentBidsList.Take(50))
|
||||||
{
|
{
|
||||||
<tr class="@(bid.IsMyBid ? "table-success" : "")">
|
<tr class="@(bid.IsMyBid ? "table-success" : "")">
|
||||||
<td>
|
<td>
|
||||||
@@ -440,11 +439,12 @@
|
|||||||
<!-- TAB PUNTATORI -->
|
<!-- TAB PUNTATORI -->
|
||||||
<div class="tab-pane fade" id="content-bidders" role="tabpanel">
|
<div class="tab-pane fade" id="content-bidders" role="tabpanel">
|
||||||
<div class="tab-panel-content">
|
<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
|
// Calcola statistiche puntatori
|
||||||
var bidderStats = recentBidsCopy
|
var bidderStats = recentBidsCopy
|
||||||
.GroupBy(b => b.Username)
|
.GroupBy(b => b.Username)
|
||||||
@@ -574,8 +574,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Versione in basso a destra -->
|
|
||||||
<div class="version-badge">
|
|
||||||
<i class="bi bi-box-seam"></i> v1.0.0
|
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -385,7 +385,6 @@ namespace AutoBidder.Pages
|
|||||||
CheckAuctionOpenBeforeBid = settings.DefaultCheckAuctionOpenBeforeBid,
|
CheckAuctionOpenBeforeBid = settings.DefaultCheckAuctionOpenBeforeBid,
|
||||||
MinPrice = settings.DefaultMinPrice,
|
MinPrice = settings.DefaultMinPrice,
|
||||||
MaxPrice = settings.DefaultMaxPrice,
|
MaxPrice = settings.DefaultMaxPrice,
|
||||||
MaxClicks = settings.DefaultMaxClicks,
|
|
||||||
MinResets = settings.DefaultMinResets,
|
MinResets = settings.DefaultMinResets,
|
||||||
MaxResets = settings.DefaultMaxResets,
|
MaxResets = settings.DefaultMaxResets,
|
||||||
IsActive = isActive,
|
IsActive = isActive,
|
||||||
@@ -748,22 +747,8 @@ namespace AutoBidder.Pages
|
|||||||
|
|
||||||
private string GetStatusAnimationClass(AuctionInfo auction)
|
private string GetStatusAnimationClass(AuctionInfo auction)
|
||||||
{
|
{
|
||||||
// Animazioni per stati speciali
|
// Animazioni disabilitate - i colori sono sufficienti per identificare lo stato
|
||||||
if (auction.LastState != null)
|
return "";
|
||||||
{
|
|
||||||
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";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetPriceDisplay(AuctionInfo? auction)
|
private string GetPriceDisplay(AuctionInfo? auction)
|
||||||
@@ -990,6 +975,29 @@ namespace AutoBidder.Pages
|
|||||||
return auction.AuctionLog.TakeLast(50);
|
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)
|
private string GetLogEntryClass(LogEntry logEntry)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -1130,7 +1138,6 @@ namespace AutoBidder.Pages
|
|||||||
selectedAuction.MaxPrice = limits.MaxPrice;
|
selectedAuction.MaxPrice = limits.MaxPrice;
|
||||||
selectedAuction.MinResets = limits.MinResets;
|
selectedAuction.MinResets = limits.MinResets;
|
||||||
selectedAuction.MaxResets = limits.MaxResets;
|
selectedAuction.MaxResets = limits.MaxResets;
|
||||||
selectedAuction.MaxClicks = limits.MaxBids;
|
|
||||||
|
|
||||||
SaveAuctions();
|
SaveAuctions();
|
||||||
|
|
||||||
@@ -1144,7 +1151,6 @@ namespace AutoBidder.Pages
|
|||||||
selectedAuction.MaxPrice = limits.MaxPrice;
|
selectedAuction.MaxPrice = limits.MaxPrice;
|
||||||
selectedAuction.MinResets = limits.MinResets;
|
selectedAuction.MinResets = limits.MinResets;
|
||||||
selectedAuction.MaxResets = limits.MaxResets;
|
selectedAuction.MaxResets = limits.MaxResets;
|
||||||
selectedAuction.MaxClicks = limits.MaxBids;
|
|
||||||
|
|
||||||
SaveAuctions();
|
SaveAuctions();
|
||||||
|
|
||||||
|
|||||||
@@ -208,6 +208,187 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -423,6 +604,159 @@ private System.Threading.Timer? updateTimer;
|
|||||||
return "bg-success";
|
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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
updateTimer?.Dispose();
|
updateTimer?.Dispose();
|
||||||
|
|||||||
@@ -277,6 +277,55 @@ using (var scope = app.Services.CreateScope())
|
|||||||
var isHealthy = await databaseService.CheckDatabaseHealthAsync();
|
var isHealthy = await databaseService.CheckDatabaseHealthAsync();
|
||||||
Console.WriteLine($"[DB] Database health check: {(isHealthy ? "OK" : "FAILED")}");
|
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
|
// 🆕 Esegui diagnostica completa se ci sono problemi o se richiesto
|
||||||
var runDiagnostics = Environment.GetEnvironmentVariable("DB_DIAGNOSTICS")?.ToLower() == "true";
|
var runDiagnostics = Environment.GetEnvironmentVariable("DB_DIAGNOSTICS")?.ToLower() == "true";
|
||||||
if (!isHealthy || runDiagnostics)
|
if (!isHealthy || runDiagnostics)
|
||||||
|
|||||||
@@ -176,9 +176,8 @@ namespace AutoBidder.Services
|
|||||||
auction.MaxPrice = maxPrice;
|
auction.MaxPrice = maxPrice;
|
||||||
auction.MinResets = minResets;
|
auction.MinResets = minResets;
|
||||||
auction.MaxResets = maxResets;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -202,7 +201,6 @@ namespace AutoBidder.Services
|
|||||||
auction.MaxPrice = maxPrice;
|
auction.MaxPrice = maxPrice;
|
||||||
auction.MinResets = minResets;
|
auction.MinResets = minResets;
|
||||||
auction.MaxResets = maxResets;
|
auction.MaxResets = maxResets;
|
||||||
auction.MaxClicks = maxBids;
|
|
||||||
count++;
|
count++;
|
||||||
|
|
||||||
OnLog?.Invoke($"[LIMITS] Aggiornati limiti per {auction.Name}");
|
OnLog?.Invoke($"[LIMITS] Aggiornati limiti per {auction.Name}");
|
||||||
@@ -653,14 +651,6 @@ namespace AutoBidder.Services
|
|||||||
return false;
|
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)
|
// ?? CONTROLLO 6: Cooldown (evita puntate multiple ravvicinate)
|
||||||
if (auction.LastClickAt.HasValue)
|
if (auction.LastClickAt.HasValue)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -219,15 +219,20 @@ namespace AutoBidder.Services
|
|||||||
// Parse aste dall'HTML (fragment AJAX)
|
// Parse aste dall'HTML (fragment AJAX)
|
||||||
auctions = ParseAuctionsFromHtml(html);
|
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)
|
if (category.IsSpecialCategory && category.TabId == 1)
|
||||||
{
|
{
|
||||||
var before = auctions.Count;
|
var creditCount = auctions.Count(a => a.IsCreditAuction);
|
||||||
auctions = auctions.Where(a => a.IsCreditAuction).ToList();
|
Console.WriteLine($"[BidooBrowser] DEBUG Aste di Puntate: {creditCount}/{auctions.Count} hanno IsCreditAuction=true");
|
||||||
Console.WriteLine($"[BidooBrowser] Filtrate {before} -> {auctions.Count} aste di puntate (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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1037,6 +1037,7 @@ namespace AutoBidder.Services
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Salva risultato asta con dati completi per analytics
|
/// Salva risultato asta con dati completi per analytics
|
||||||
|
/// 🔥 ANTI-DUPLICAZIONE: Usa UPSERT basato su AuctionId per evitare duplicati
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task SaveAuctionResultAsync(string auctionId, string auctionName, double finalPrice, int bidsUsed, bool won,
|
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,
|
||||||
@@ -1044,6 +1045,22 @@ namespace AutoBidder.Services
|
|||||||
{
|
{
|
||||||
var closedAtHour = DateTime.UtcNow.Hour;
|
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 = @"
|
var sql = @"
|
||||||
INSERT INTO AuctionResults
|
INSERT INTO AuctionResults
|
||||||
(AuctionId, AuctionName, FinalPrice, BidsUsed, Won, BuyNowPrice, ShippingCost, TotalCost, Savings, Timestamp,
|
(AuctionId, AuctionName, FinalPrice, BidsUsed, Won, BuyNowPrice, ShippingCost, TotalCost, Savings, Timestamp,
|
||||||
@@ -1368,6 +1385,136 @@ namespace AutoBidder.Services
|
|||||||
return results;
|
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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
// Non ci sono risorse da rilasciare - le connessioni sono gestite con using
|
// Non ci sono risorse da rilasciare - le connessioni sono gestite con using
|
||||||
@@ -1381,7 +1528,6 @@ namespace AutoBidder.Services
|
|||||||
public int Version { get; }
|
public int Version { get; }
|
||||||
public string Description { get; }
|
public string Description { get; }
|
||||||
private readonly Func<SqliteConnection, Task> _execute;
|
private readonly Func<SqliteConnection, Task> _execute;
|
||||||
private string? _lastSqlExecuted;
|
|
||||||
|
|
||||||
public Migration(int version, string description, Func<SqliteConnection, Task> execute)
|
public Migration(int version, string description, Func<SqliteConnection, Task> execute)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -70,6 +70,35 @@ namespace AutoBidder.Utilities
|
|||||||
/// Default: "Normal" (uso giornaliero - errori e warning)
|
/// Default: "Normal" (uso giornaliero - errori e warning)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string MinLogLevel { get; set; } = "Normal";
|
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
|
public static class SettingsManager
|
||||||
|
|||||||
Reference in New Issue
Block a user