SelectAuction(auction)">
@TruncateName(auction.AuctionName, 30)
@@ -200,15 +200,47 @@
+
+
+
+
+
+
+
+
+ @if (!string.IsNullOrEmpty(filterProductName))
+ {
+
+
+
+ }
+
+
+
+ Clicca intestazioni per ordinare
+
+
+
+
- @if (products == null || !products.Any())
+ @if (filteredProducts == null || !filteredProducts.Any())
{
-
Nessun prodotto salvato
+
+ @if (!string.IsNullOrEmpty(filterProductName))
+ {
+ Nessun prodotto trovato per "@filterProductName"
+ }
+ else
+ {
+ Nessun prodotto salvato
+ }
+
}
else
@@ -217,25 +249,47 @@
- Prodotto
- Aste
- Win%
- Limiti
+
+
+
+
+ Range Storico
+ Limiti Consigliati
Azioni
- @foreach (var product in products)
+ @foreach (var product in filteredProducts)
{
var winRate = product.TotalAuctions > 0
? (product.WonAuctions * 100.0 / product.TotalAuctions)
: 0;
+ var isEditing = editingProductKey == product.ProductKey;
-
+ SelectProduct(product)">
- @product.ProductName
-
- @product.TotalAuctions totali (@product.WonAuctions vinte)
+
+
ToggleEditProduct(product)"
+ @onclick:stopPropagation="true"
+ title="@(isEditing ? "Chiudi editor" : "Modifica limiti default")">
+
+
+
+ @product.ProductName
+
+ @product.TotalAuctions totali (@product.WonAuctions vinte)
+
+
@product.TotalAuctions
@@ -246,10 +300,13 @@
- @if (product.RecommendedMinPrice.HasValue && product.RecommendedMaxPrice.HasValue)
+ @product.AvgFinalPrice.ToString("F2")
+
+
+ @if (product.MinFinalPrice.HasValue && product.MaxFinalPrice.HasValue)
{
- @product.RecommendedMinPrice.Value.ToString("F2") - @product.RecommendedMaxPrice.Value.ToString("F2")
+ @product.MinFinalPrice.Value.ToString("F2") - @product.MaxFinalPrice.Value.ToString("F2")
}
else
@@ -257,21 +314,130 @@
-
}
-
+
@if (product.RecommendedMinPrice.HasValue && product.RecommendedMaxPrice.HasValue)
{
- ApplyLimitsToProduct(product)"
- title="Applica limiti a tutte le aste di questo prodotto">
- Applica
-
+
+ @product.RecommendedMinPrice.Value.ToString("F2") - @product.RecommendedMaxPrice.Value.ToString("F2")
+
}
else
{
N/D
}
+
+
+ @if (product.RecommendedMinPrice.HasValue && product.RecommendedMaxPrice.HasValue)
+ {
+ ApplyLimitsToProduct(product)"
+ title="Applica limiti a tutte le aste di questo prodotto">
+
+
+ }
+ DeleteProduct(product)"
+ title="Elimina questo prodotto dalle statistiche"
+ disabled="@isDeletingProduct">
+
+
+
+
+
+
+ @if (isEditing)
+ {
+
+
+
+
+
+ Limiti Default per: @product.ProductName
+
+
+
+
+
+
+
+
+
+
+
+
+ Anticipo Puntata (ms)
+
+
+
+
+
+ Valori consigliati dall'algoritmo:
+ @product.RecommendedMinPrice?.ToString("F2") - @product.RecommendedMaxPrice?.ToString("F2")
+ Reset: @product.RecommendedMinResets - @product.RecommendedMaxResets
+
+
+
+
+
+
+ CopyRecommendedToTemp(product)"
+ disabled="@(!product.RecommendedMinPrice.HasValue)"
+ title="Copia i valori consigliati dall'algoritmo">
+
+ Usa Consigliati
+
+ SaveProductDefaults(product)"
+ disabled="@isSavingDefaults">
+
+ Salva Default
+
+ ApplyDefaultsToAllAuctions(product)"
+ disabled="@(!HasUserDefaults(product) || isSavingDefaults)">
+
+ Applica a Tutte le Aste
+
+ CancelEditProduct()">
+
+
+
+
+
+
+ }
}
@@ -421,20 +587,202 @@
}
+
+
+ @if (selectedProduct != null)
+ {
+
+
+
+
+
+ @if (isLoadingProductAuctions)
+ {
+
+
+ Caricamento...
+
+
Caricamento aste...
+
+ }
+ else if (selectedProductAuctions == null || !selectedProductAuctions.Any())
+ {
+
+
+
Nessuna asta trovata per questo prodotto
+
+ }
+ else
+ {
+
+
+
+
+
+ ID Asta
+ Prezzo Finale
+ Stato
+ Vincitore
+ Le Mie Puntate
+ Puntate Vincitore
+ Reset
+ Ora Chiusura
+ Data
+ Risparmio
+
+
+
+ @foreach (var auction in selectedProductAuctions)
+ {
+
+ @auction.AuctionId
+ @auction.FinalPrice.ToString("F2")
+
+ @if (auction.Won)
+ {
+ ? Vinta
+ }
+ else
+ {
+ ? Persa
+ }
+
+ @(auction.WinnerUsername ?? "-")
+
+ @auction.BidsUsed
+
+
+ @if (auction.WinnerBidsUsed.HasValue)
+ {
+ @auction.WinnerBidsUsed
+ }
+ else
+ {
+ -
+ }
+
+
+ @if (auction.TotalResets.HasValue)
+ {
+
+ @auction.TotalResets
+
+ }
+ else
+ {
+ -
+ }
+
+
+ @if (auction.ClosedAtHour.HasValue)
+ {
+ @auction.ClosedAtHour:00
+ }
+ else
+ {
+ -
+ }
+
+ @FormatTimestamp(auction.Timestamp)
+
+ @if (auction.Savings.HasValue)
+ {
+
+ @(auction.Savings.Value > 0 ? "+" : "")@auction.Savings.Value.ToString("F2")
+
+ }
+ else
+ {
+ -
+ }
+
+
+ }
+
+
+
+
+
+
+
+ }
+
+
+
+ }
}
@code {
private bool isLoading = true;
+private bool isDeletingProduct = false;
private List? recentAuctions;
private List? filteredAuctions;
private List? products;
+private List? filteredProducts;
-// Filtri e ordinamento
+// Filtri e ordinamento aste
private string filterName = "";
private string filterWon = "";
private AuctionResultExtended? selectedAuctionDetail;
+// Filtri e ordinamento prodotti
+private string filterProductName = "";
+private string productSortColumn = "name";
+private bool productSortDescending = false;
+
+// Prodotto selezionato e sue aste
+private ProductStatisticsRecord? selectedProduct = null;
+private List? selectedProductAuctions = null;
+private bool isLoadingProductAuctions = false;
+
+// Editing limiti default prodotto
+private string? editingProductKey = null;
+private bool isSavingDefaults = false;
+private double? tempUserDefaultMinPrice;
+private double? tempUserDefaultMaxPrice;
+private int? tempUserDefaultMinResets;
+private int? tempUserDefaultMaxResets;
+private int? tempUserDefaultMaxBids;
+private int? tempUserDefaultBidDeadline;
+
protected override async Task OnInitializedAsync()
{
await RefreshStats();
@@ -453,6 +801,7 @@ private AuctionResultExtended? selectedAuctionDetail;
// Carica prodotti con statistiche
products = await DatabaseService.GetAllProductStatisticsAsync();
+ ApplyProductFilter();
}
catch (Exception ex)
{
@@ -547,6 +896,30 @@ private AuctionResultExtended? selectedAuctionDetail;
{
selectedAuctionDetail = auction;
}
+
+ private async Task SelectProduct(ProductStatisticsRecord product)
+ {
+ selectedProduct = product;
+ selectedProductAuctions = null;
+ isLoadingProductAuctions = true;
+ StateHasChanged();
+
+ try
+ {
+ // Carica tutte le aste per questo prodotto
+ selectedProductAuctions = await DatabaseService.GetAuctionResultsByProductAsync(product.ProductKey, 1000);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[Statistics] Error loading product auctions: {ex.Message}");
+ await JSRuntime.InvokeVoidAsync("alert", $"Errore caricamento aste: {ex.Message}");
+ }
+ finally
+ {
+ isLoadingProductAuctions = false;
+ StateHasChanged();
+ }
+ }
private string GetHeatBadgeClass(int heat)
{
@@ -554,6 +927,13 @@ private AuctionResultExtended? selectedAuctionDetail;
if (heat < 60) return "bg-warning text-dark";
return "bg-danger";
}
+
+ private string GetResetBadgeClass(int resets)
+ {
+ if (resets < 10) return "bg-success";
+ if (resets < 30) return "bg-warning text-dark";
+ return "bg-danger";
+ }
private string TruncateName(string name, int maxLength)
{
@@ -610,6 +990,285 @@ private AuctionResultExtended? selectedAuctionDetail;
await JSRuntime.InvokeVoidAsync("alert", $"Errore: {ex.Message}");
}
}
+
+ private async Task DeleteProduct(ProductStatisticsRecord product)
+ {
+ try
+ {
+ // Conferma eliminazione
+ var confirmed = await JSRuntime.InvokeAsync("confirm",
+ $"Eliminare il prodotto '{product.ProductName}'?\n\n" +
+ $"Questo rimuoverΰ le statistiche di {product.TotalAuctions} aste.\n" +
+ $"L'operazione NON puς essere annullata!");
+
+ if (!confirmed)
+ return;
+
+ isDeletingProduct = true;
+ StateHasChanged();
+
+ // Elimina dal database
+ var deleted = await DatabaseService.DeleteProductStatisticsAsync(product.ProductKey);
+
+ if (deleted > 0)
+ {
+ // Rimuovi dalla lista locale
+ products?.Remove(product);
+ ApplyProductFilter();
+
+ await JSRuntime.InvokeVoidAsync("alert",
+ $"? Prodotto '{product.ProductName}' eliminato con successo!\n" +
+ $"Rimosse {deleted} righe dal database.");
+ }
+ else
+ {
+ await JSRuntime.InvokeVoidAsync("alert",
+ "Nessuna riga eliminata. Il prodotto potrebbe essere giΰ stato rimosso.");
+ }
+ }
+ catch (Exception ex)
+ {
+ await JSRuntime.InvokeVoidAsync("alert", $"Errore durante eliminazione: {ex.Message}");
+ }
+ finally
+ {
+ isDeletingProduct = false;
+ StateHasChanged();
+ }
+ }
+
+ // ???????????????????????????????????????????????????????????????
+ // METODI FILTRO E ORDINAMENTO PRODOTTI
+ // ???????????????????????????????????????????????????????????????
+
+ private void ApplyProductFilter()
+ {
+ if (products == null)
+ {
+ filteredProducts = null;
+ return;
+ }
+
+ var filtered = products.AsEnumerable();
+
+ // Filtro per nome
+ if (!string.IsNullOrWhiteSpace(filterProductName))
+ {
+ filtered = filtered.Where(p =>
+ p.ProductName.Contains(filterProductName, StringComparison.OrdinalIgnoreCase));
+ }
+
+ // Ordinamento
+ filtered = productSortColumn switch
+ {
+ "name" => productSortDescending
+ ? filtered.OrderByDescending(p => p.ProductName)
+ : filtered.OrderBy(p => p.ProductName),
+ "auctions" => productSortDescending
+ ? filtered.OrderByDescending(p => p.TotalAuctions)
+ : filtered.OrderBy(p => p.TotalAuctions),
+ "winrate" => productSortDescending
+ ? filtered.OrderByDescending(p => p.TotalAuctions > 0 ? (p.WonAuctions * 100.0 / p.TotalAuctions) : 0)
+ : filtered.OrderBy(p => p.TotalAuctions > 0 ? (p.WonAuctions * 100.0 / p.TotalAuctions) : 0),
+ "avgprice" => productSortDescending
+ ? filtered.OrderByDescending(p => p.AvgFinalPrice)
+ : filtered.OrderBy(p => p.AvgFinalPrice),
+ _ => filtered.OrderBy(p => p.ProductName) // Default alfabetico ascendente
+ };
+
+ filteredProducts = filtered.ToList();
+ }
+
+ private void ClearProductFilter()
+ {
+ filterProductName = "";
+ ApplyProductFilter();
+ }
+
+ private void SortProductsBy(string column)
+ {
+ if (productSortColumn == column)
+ {
+ // Toggle direzione se stessa colonna
+ productSortDescending = !productSortDescending;
+ }
+ else
+ {
+ // Nuova colonna
+ productSortColumn = column;
+ // Default: nome alfabetico ascendente, resto discendente
+ productSortDescending = column != "name";
+ }
+ ApplyProductFilter();
+ }
+
+ private MarkupString GetProductSortIndicator(string column)
+ {
+ if (productSortColumn != column)
+ return new MarkupString(" ");
+
+ return productSortDescending
+ ? new MarkupString(" ")
+ : new MarkupString(" ");
+ }
+
+ // ???????????????????????????????????????????????????????????????????
+ // METODI EDITING LIMITI DEFAULT PRODOTTO
+ // ???????????????????????????????????????????????????????????????????
+
+ private void ToggleEditProduct(ProductStatisticsRecord product)
+ {
+ if (editingProductKey == product.ProductKey)
+ {
+ // Chiudi editor
+ CancelEditProduct();
+ }
+ else
+ {
+ // Apri editor
+ editingProductKey = product.ProductKey;
+ LoadCurrentDefaults(product);
+ }
+ }
+
+ private void LoadCurrentDefaults(ProductStatisticsRecord product)
+ {
+ tempUserDefaultMinPrice = product.UserDefaultMinPrice;
+ tempUserDefaultMaxPrice = product.UserDefaultMaxPrice;
+ tempUserDefaultMinResets = product.UserDefaultMinResets;
+ tempUserDefaultMaxResets = product.UserDefaultMaxResets;
+ tempUserDefaultMaxBids = product.UserDefaultMaxBids;
+ tempUserDefaultBidDeadline = product.UserDefaultBidBeforeDeadlineMs;
+ }
+
+ private void CopyRecommendedToTemp(ProductStatisticsRecord product)
+ {
+ tempUserDefaultMinPrice = product.RecommendedMinPrice;
+ tempUserDefaultMaxPrice = product.RecommendedMaxPrice;
+ tempUserDefaultMinResets = product.RecommendedMinResets;
+ tempUserDefaultMaxResets = product.RecommendedMaxResets;
+ tempUserDefaultMaxBids = product.RecommendedMaxBids;
+ // Bid deadline rimane quello dell'utente o default 200ms
+ if (!tempUserDefaultBidDeadline.HasValue)
+ tempUserDefaultBidDeadline = 200;
+
+ StateHasChanged();
+ }
+
+ private void CancelEditProduct()
+ {
+ editingProductKey = null;
+ tempUserDefaultMinPrice = null;
+ tempUserDefaultMaxPrice = null;
+ tempUserDefaultMinResets = null;
+ tempUserDefaultMaxResets = null;
+ tempUserDefaultMaxBids = null;
+ tempUserDefaultBidDeadline = null;
+ }
+
+ private async Task SaveProductDefaults(ProductStatisticsRecord product)
+ {
+ try
+ {
+ isSavingDefaults = true;
+ StateHasChanged();
+
+ // Aggiorna nel database
+ await DatabaseService.UpdateProductUserDefaultsAsync(
+ product.ProductKey,
+ tempUserDefaultMinPrice,
+ tempUserDefaultMaxPrice,
+ tempUserDefaultMinResets,
+ tempUserDefaultMaxResets,
+ tempUserDefaultMaxBids,
+ tempUserDefaultBidDeadline
+ );
+
+ // Aggiorna l'oggetto locale
+ product.UserDefaultMinPrice = tempUserDefaultMinPrice;
+ product.UserDefaultMaxPrice = tempUserDefaultMaxPrice;
+ product.UserDefaultMinResets = tempUserDefaultMinResets;
+ product.UserDefaultMaxResets = tempUserDefaultMaxResets;
+ product.UserDefaultMaxBids = tempUserDefaultMaxBids;
+ product.UserDefaultBidBeforeDeadlineMs = tempUserDefaultBidDeadline;
+
+ await JSRuntime.InvokeVoidAsync("alert",
+ $"? Limiti default salvati per '{product.ProductName}'!\n\n" +
+ $"Min: {tempUserDefaultMinPrice:F2} - Max: {tempUserDefaultMaxPrice:F2}\n" +
+ $"Reset: {tempUserDefaultMinResets} - {tempUserDefaultMaxResets}\n" +
+ $"Max Puntate: {tempUserDefaultMaxBids}\n" +
+ $"Anticipo: {tempUserDefaultBidDeadline}ms");
+
+ CancelEditProduct();
+ }
+ catch (Exception ex)
+ {
+ await JSRuntime.InvokeVoidAsync("alert", $"Errore salvataggio: {ex.Message}");
+ }
+ finally
+ {
+ isSavingDefaults = false;
+ StateHasChanged();
+ }
+ }
+
+ private async Task ApplyDefaultsToAllAuctions(ProductStatisticsRecord product)
+ {
+ try
+ {
+ 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;
+ }
+
+ var confirmed = await JSRuntime.InvokeAsync("confirm",
+ $"Applicare i limiti default a {matchingAuctions.Count} aste di '{product.ProductName}'?\n\n" +
+ $"Min: {product.UserDefaultMinPrice:F2} - Max: {product.UserDefaultMaxPrice:F2}\n" +
+ $"Reset: {product.UserDefaultMinResets} - {product.UserDefaultMaxResets}\n" +
+ $"Max Puntate: {product.UserDefaultMaxBids}\n" +
+ $"Anticipo: {product.UserDefaultBidBeforeDeadlineMs}ms");
+
+ if (!confirmed) return;
+
+ // Applica i limiti
+ foreach (var auction in matchingAuctions)
+ {
+ if (product.UserDefaultMinPrice.HasValue)
+ auction.MinPrice = product.UserDefaultMinPrice.Value;
+ if (product.UserDefaultMaxPrice.HasValue)
+ auction.MaxPrice = product.UserDefaultMaxPrice.Value;
+ if (product.UserDefaultMinResets.HasValue)
+ auction.MinResets = product.UserDefaultMinResets.Value;
+ if (product.UserDefaultMaxResets.HasValue)
+ auction.MaxResets = product.UserDefaultMaxResets.Value;
+ if (product.UserDefaultMaxBids.HasValue)
+ auction.MaxClicks = product.UserDefaultMaxBids.Value;
+ if (product.UserDefaultBidBeforeDeadlineMs.HasValue)
+ auction.BidBeforeDeadlineMs = product.UserDefaultBidBeforeDeadlineMs.Value;
+ }
+
+ // Salva
+ AutoBidder.Utilities.PersistenceManager.SaveAuctions(AppState.Auctions.ToList());
+
+ await JSRuntime.InvokeVoidAsync("alert",
+ $"? Limiti applicati a {matchingAuctions.Count} aste di '{product.ProductName}'!");
+ }
+ catch (Exception ex)
+ {
+ await JSRuntime.InvokeVoidAsync("alert", $"Errore: {ex.Message}");
+ }
+ }
+
+ private bool HasUserDefaults(ProductStatisticsRecord product)
+ {
+ return product.UserDefaultMinPrice.HasValue
+ && product.UserDefaultMaxPrice.HasValue;
+ }
}
diff --git a/Mimante/Services/AuctionMonitor.cs b/Mimante/Services/AuctionMonitor.cs
index 724a680..b83d7ff 100644
--- a/Mimante/Services/AuctionMonitor.cs
+++ b/Mimante/Services/AuctionMonitor.cs
@@ -428,6 +428,22 @@ namespace AutoBidder.Services
auction.AddLog($"[ASTA TERMINATA] {statusMsg}");
OnLog?.Invoke($"[FINE] [{auction.AuctionId}] Asta {statusMsg} - Polling fermato");
+ // π‘ SUGGERIMENTO: Se persa e non abbiamo mai provato a puntare, potrebbe essere un problema di timing
+ if (!won && auction.SessionBidCount == 0)
+ {
+ var settings = Utilities.SettingsManager.Load();
+ int offsetMs = auction.BidBeforeDeadlineMs > 0
+ ? auction.BidBeforeDeadlineMs
+ : settings.DefaultBidBeforeDeadlineMs;
+
+ // Se l'offset Γ¨ <= 1000ms, il polling (~1s) potrebbe non catturare il momento giusto
+ if (offsetMs <= 1000)
+ {
+ auction.AddLog($"[π‘ SUGGERIMENTO] Asta persa senza mai puntare. Con offset={offsetMs}ms e polling~1s, " +
+ $"potresti non vedere mai il timer scendere sotto {offsetMs}ms. Considera di aumentare l'offset a 1500-2000ms nelle impostazioni.");
+ }
+ }
+
auction.BidHistory.Add(new BidHistory
{
Timestamp = DateTime.UtcNow,
@@ -531,7 +547,7 @@ namespace AutoBidder.Services
{
var settings = SettingsManager.Load();
- // Offset: millisecondi prima della scadenza
+ // Offset: millisecondi prima della scadenza (configurato dall'utente)
int offsetMs = auction.BidBeforeDeadlineMs > 0
? auction.BidBeforeDeadlineMs
: settings.DefaultBidBeforeDeadlineMs;
@@ -542,42 +558,53 @@ namespace AutoBidder.Services
// Skip se giΓ vincitore o timer scaduto
if (state.IsMyBid || timerMs <= 0) return;
- // ?? STIMA TEMPO RIMANENTE
- // L'API dΓ timer in secondi interi (1000, 2000, ecc.)
- // Quando cambia, salvo il timestamp. Poi stimo localmente.
-
- bool timerChanged = Math.Abs(auction.LastRawTimer - timerMs) > 500;
-
- if (timerChanged || !auction.LastDeadlineUpdateUtc.HasValue)
- {
- auction.LastRawTimer = timerMs;
- auction.LastDeadlineUpdateUtc = DateTime.UtcNow;
- auction.BidScheduled = false;
- }
-
- // Calcola tempo stimato rimanente
- var elapsed = (DateTime.UtcNow - auction.LastDeadlineUpdateUtc.Value).TotalMilliseconds;
- double estimatedRemaining = timerMs - elapsed;
-
// Log timing solo se abilitato
if (settings.LogTiming)
{
- auction.AddLog($"[TIMING] API={timerMs:F0}ms, Elapsed={elapsed:F0}ms, Stima={estimatedRemaining:F0}ms");
+ auction.AddLog($"[TIMING] API={timerMs:F0}ms, Offset={offsetMs}ms");
}
- // ?? Γ il momento di puntare?
- if (estimatedRemaining > offsetMs) return; // Troppo presto
- if (estimatedRemaining < -200) return; // Troppo tardi
+ // Punta quando il timer API Γ¨ <= offset configurato dall'utente
+ // NESSUNA modifica automatica - l'utente decide il timing
+ if (timerMs > offsetMs)
+ {
+ return;
+ }
- // Protezione doppia puntata
- if (auction.BidScheduled) return;
+ // Timer <= offset = Γ IL MOMENTO DI PUNTARE!
+ auction.AddLog($"[BID WINDOW] Timer={timerMs:F0}ms <= Offset={offsetMs}ms - Verifica condizioni...");
- // Cooldown 1 secondo
- if (auction.LastClickAt.HasValue && (DateTime.UtcNow - auction.LastClickAt.Value).TotalMilliseconds < 1000) return;
+ // Resetta BidScheduled se il timer Γ¨ AUMENTATO (qualcun altro ha puntato = nuovo ciclo)
+ if (timerMs > auction.LastScheduledTimerMs + 500)
+ {
+ auction.BidScheduled = false;
+ }
+
+ // Resetta anche se Γ¨ passato troppo tempo dall'ultima puntata (nuovo ciclo)
+ if (auction.LastClickAt.HasValue &&
+ (DateTime.UtcNow - auction.LastClickAt.Value).TotalSeconds > 10)
+ {
+ auction.BidScheduled = false;
+ }
+
+ // Protezione doppia puntata SOLO per lo stesso ciclo di timer
+ if (auction.BidScheduled && Math.Abs(auction.LastScheduledTimerMs - timerMs) < 100)
+ {
+ auction.AddLog($"[SKIP] Puntata giΓ schedulata per timer~={timerMs:F0}ms in questo ciclo");
+ return;
+ }
+
+ // Cooldown 1 secondo tra puntate
+ if (auction.LastClickAt.HasValue && (DateTime.UtcNow - auction.LastClickAt.Value).TotalMilliseconds < 1000)
+ {
+ auction.AddLog($"[COOLDOWN] Attesa cooldown puntata precedente");
+ return;
+ }
// π΄ CONTROLLI FONDAMENTALI (prezzo, reset, limiti, puntate residue)
if (!ShouldBid(auction, state))
{
+ // I motivi vengono ora loggati sempre dentro ShouldBid
return;
}
@@ -587,19 +614,25 @@ namespace AutoBidder.Services
if (!decision.ShouldBid)
{
+ // π₯ FIX: Logga SEMPRE il motivo del blocco strategia, non solo se LogStrategyDecisions Γ¨ attivo
+ // Questo aiuta a capire perchΓ© si perdono le aste
+ auction.AddLog($"[STRATEGY] {decision.Reason}");
+
+ // Log aggiuntivo solo se debug strategie attivo
if (settings.LogStrategyDecisions)
{
- auction.AddLog($"[STRATEGY] {decision.Reason}");
+ OnLog?.Invoke($"[{auction.Name}] STRATEGY blocked: {decision.Reason}");
}
return;
}
// ?? PUNTA!
auction.BidScheduled = true;
+ auction.LastScheduledTimerMs = timerMs;
if (settings.LogBids)
{
- auction.AddLog($"[BID] Puntata a ~{estimatedRemaining:F0}ms dalla scadenza");
+ auction.AddLog($"[BID] Puntata con timer API={timerMs:F0}ms");
}
await ExecuteBid(auction, state, token);
@@ -649,12 +682,15 @@ namespace AutoBidder.Services
if (result.Success)
{
- auction.AddLog($"[BID OK] Latenza: {result.LatencyMs}ms -> EUR{result.NewPrice:F2}");
+ // Log dettagliato con info ping per analisi timing
+ var pollingPing = auction.PollingLatencyMs;
+ auction.AddLog($"[BID OK] Latenza puntata: {result.LatencyMs}ms | Ping polling: {pollingPing}ms | Totale stimato: {result.LatencyMs + pollingPing}ms");
OnLog?.Invoke($"[OK] Puntata riuscita su {auction.Name} ({auction.AuctionId}): {result.LatencyMs}ms");
}
else
{
- auction.AddLog($"[BID FAIL] {result.Error}");
+ var pollingPing = auction.PollingLatencyMs;
+ auction.AddLog($"[BID FAIL] {result.Error} | Ping: {pollingPing}ms");
OnLog?.Invoke($"[FAIL] Puntata fallita su {auction.Name} ({auction.AuctionId}): {result.Error}");
}
@@ -667,7 +703,7 @@ namespace AutoBidder.Services
Timer = state.Timer,
LatencyMs = result.LatencyMs,
Success = result.Success,
- Notes = result.Success ? $"EUR{result.NewPrice:F2}" : (result.Error ?? "Errore sconosciuto")
+ Notes = result.Success ? $"OK" : (result.Error ?? "Errore sconosciuto")
});
}
catch (Exception ex)
@@ -698,6 +734,7 @@ namespace AutoBidder.Services
if (auction.CalculatedValue.SavingsPercentage.HasValue &&
auction.CalculatedValue.SavingsPercentage.Value < settings.MinSavingsPercentage)
{
+ // π₯ Logga SEMPRE - Γ¨ un blocco frequente e importante
auction.AddLog($"[VALUE] Puntata bloccata: risparmio {auction.CalculatedValue.SavingsPercentage.Value:F1}% < {settings.MinSavingsPercentage:F1}% richiesto");
return false;
}
@@ -705,46 +742,49 @@ namespace AutoBidder.Services
if (settings.LogTiming && settings.ValueCheckEnabled)
{
- auction.AddLog($"[DEBUG] ? Controllo convenienza OK");
+ auction.AddLog($"[DEBUG] β Controllo convenienza OK");
}
- // ?? CONTROLLO ANTI-COLLISIONE: Rileva aste troppo "affollate"
- // Se negli ultimi 10 secondi ci sono state 3+ puntate di utenti diversi, evita
- var recentBidsThreshold = 10; // secondi
- var maxActiveBidders = 3; // se 3+ bidder attivi, potrebbe essere troppo affollata
-
- try
+ // ?? CONTROLLO ANTI-COLLISIONE (OPZIONALE): Rileva aste troppo "affollate"
+ // DISABILITATO DI DEFAULT - puΓ² far perdere aste competitive!
+ if (settings.HardcodedAntiCollisionEnabled)
{
- var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
- var recentBids = auction.RecentBids
- .Where(b => now - b.Timestamp <= recentBidsThreshold)
- .ToList();
+ var recentBidsThreshold = 10; // secondi
+ var maxActiveBidders = 3; // se 3+ bidder attivi, potrebbe essere troppo affollata
- var activeBidders = recentBids
- .Select(b => b.Username)
- .Distinct(StringComparer.OrdinalIgnoreCase)
- .Count();
-
- if (settings.LogTiming)
+ try
{
- auction.AddLog($"[DEBUG] Bidder attivi ultimi {recentBidsThreshold}s: {activeBidders}/{maxActiveBidders}");
- }
-
- if (activeBidders >= maxActiveBidders)
- {
- // Controlla se l'ultimo bidder sono io - se sì, posso continuare
- var session = _apiClient.GetSession();
- var lastBid = recentBids.OrderByDescending(b => b.Timestamp).FirstOrDefault();
+ var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
+ var recentBids = auction.RecentBids
+ .Where(b => now - b.Timestamp <= recentBidsThreshold)
+ .ToList();
- if (lastBid != null &&
- !lastBid.Username.Equals(session?.Username, StringComparison.OrdinalIgnoreCase))
+ var activeBidders = recentBids
+ .Select(b => b.Username)
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .Count();
+
+ if (settings.LogTiming)
{
- auction.AddLog($"[COMPETITION] Asta affollata: {activeBidders} bidder attivi negli ultimi {recentBidsThreshold}s - SKIP");
- return false;
+ auction.AddLog($"[DEBUG] Bidder attivi ultimi {recentBidsThreshold}s: {activeBidders}/{maxActiveBidders}");
+ }
+
+ if (activeBidders >= maxActiveBidders)
+ {
+ // Controlla se l'ultimo bidder sono io - se sì, posso continuare
+ var session = _apiClient.GetSession();
+ var lastBid = recentBids.OrderByDescending(b => b.Timestamp).FirstOrDefault();
+
+ if (lastBid != null &&
+ !lastBid.Username.Equals(session?.Username, StringComparison.OrdinalIgnoreCase))
+ {
+ auction.AddLog($"[COMPETITION] Asta affollata: {activeBidders} bidder attivi negli ultimi {recentBidsThreshold}s - SKIP");
+ return false;
+ }
}
}
+ catch { /* Ignora errori nel controllo competizione */ }
}
- catch { /* Ignora errori nel controllo competizione */ }
if (settings.LogTiming)
{
@@ -757,13 +797,14 @@ namespace AutoBidder.Services
var session = _apiClient.GetSession();
if (session != null && session.RemainingBids <= settings.MinimumRemainingBids)
{
+ // π₯ Logga SEMPRE - Γ¨ un blocco importante
auction.AddLog($"[LIMIT] Puntata bloccata: puntate residue ({session.RemainingBids}) <= limite ({settings.MinimumRemainingBids})");
return false;
}
if (settings.LogTiming && session != null)
{
- auction.AddLog($"[DEBUG] ? Puntate residue OK ({session.RemainingBids} > {settings.MinimumRemainingBids})");
+ auction.AddLog($"[DEBUG] β Puntate residue OK ({session.RemainingBids} > {settings.MinimumRemainingBids})");
}
}
@@ -780,12 +821,14 @@ namespace AutoBidder.Services
// ?? CONTROLLO 3: MinPrice/MaxPrice
if (auction.MinPrice > 0 && state.Price < auction.MinPrice)
{
+ // π₯ Logga SEMPRE questo blocco - Γ¨ critico per capire perchΓ© non punta
auction.AddLog($"[PRICE] Prezzo troppo basso: β¬{state.Price:F2} < Min β¬{auction.MinPrice:F2}");
return false;
}
if (auction.MaxPrice > 0 && state.Price > auction.MaxPrice)
{
+ // π₯ Logga SEMPRE questo blocco - Γ¨ critico
auction.AddLog($"[PRICE] Prezzo troppo alto: β¬{state.Price:F2} > Max β¬{auction.MaxPrice:F2}");
return false;
}
@@ -801,12 +844,14 @@ namespace AutoBidder.Services
// ?? CONTROLLO 4: MinResets/MaxResets
if (auction.MinResets > 0 && auction.ResetCount < auction.MinResets)
{
+ // π₯ Logga SEMPRE - Γ¨ un motivo comune di aste perse
auction.AddLog($"[RESET] Reset troppo bassi: {auction.ResetCount} < Min {auction.MinResets}");
return false;
}
if (auction.MaxResets > 0 && auction.ResetCount >= auction.MaxResets)
{
+ // π₯ Logga SEMPRE - Γ¨ un motivo comune di aste perse
auction.AddLog($"[RESET] Reset massimi raggiunti: {auction.ResetCount} >= Max {auction.MaxResets}");
return false;
}
diff --git a/Mimante/Services/DatabaseService.cs b/Mimante/Services/DatabaseService.cs
index 5b58dac..1e9d0ff 100644
--- a/Mimante/Services/DatabaseService.cs
+++ b/Mimante/Services/DatabaseService.cs
@@ -732,6 +732,21 @@ namespace AutoBidder.Services
await using var cmd = conn.CreateCommand();
cmd.CommandText = sql;
await cmd.ExecuteNonQueryAsync();
+ }),
+
+ new Migration(15, "Add user-defined default limits to ProductStatistics", async (conn) => {
+ var sql = @"
+ -- Aggiungi colonne per limiti definiti dall'utente (separati dai calcolati)
+ ALTER TABLE ProductStatistics ADD COLUMN UserDefaultMinPrice REAL;
+ ALTER TABLE ProductStatistics ADD COLUMN UserDefaultMaxPrice REAL;
+ ALTER TABLE ProductStatistics ADD COLUMN UserDefaultMinResets INTEGER;
+ ALTER TABLE ProductStatistics ADD COLUMN UserDefaultMaxResets INTEGER;
+ ALTER TABLE ProductStatistics ADD COLUMN UserDefaultMaxBids INTEGER;
+ ALTER TABLE ProductStatistics ADD COLUMN UserDefaultBidBeforeDeadlineMs INTEGER;
+ ";
+ await using var cmd = conn.CreateCommand();
+ cmd.CommandText = sql;
+ await cmd.ExecuteNonQueryAsync();
})
};
@@ -1407,12 +1422,14 @@ namespace AutoBidder.Services
AvgBidsToWin, MinBidsToWin, MaxBidsToWin,
AvgResets, MinResets, MaxResets,
RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids,
+ UserDefaultMinPrice, UserDefaultMaxPrice, UserDefaultMinResets, UserDefaultMaxResets, UserDefaultMaxBids, UserDefaultBidBeforeDeadlineMs,
HourlyStatsJson, LastUpdated)
VALUES (@productKey, @productName, @totalAuctions, @wonAuctions, @lostAuctions,
@avgFinalPrice, @minFinalPrice, @maxFinalPrice,
@avgBidsToWin, @minBidsToWin, @maxBidsToWin,
@avgResets, @minResets, @maxResets,
@recMinPrice, @recMaxPrice, @recMinResets, @recMaxResets, @recMaxBids,
+ @userMinPrice, @userMaxPrice, @userMinResets, @userMaxResets, @userMaxBids, @userBidDeadline,
@hourlyJson, @lastUpdated)
ON CONFLICT(ProductKey) DO UPDATE SET
ProductName = @productName,
@@ -1433,6 +1450,12 @@ namespace AutoBidder.Services
RecommendedMinResets = @recMinResets,
RecommendedMaxResets = @recMaxResets,
RecommendedMaxBids = @recMaxBids,
+ UserDefaultMinPrice = COALESCE(@userMinPrice, UserDefaultMinPrice),
+ UserDefaultMaxPrice = COALESCE(@userMaxPrice, UserDefaultMaxPrice),
+ UserDefaultMinResets = COALESCE(@userMinResets, UserDefaultMinResets),
+ UserDefaultMaxResets = COALESCE(@userMaxResets, UserDefaultMaxResets),
+ UserDefaultMaxBids = COALESCE(@userMaxBids, UserDefaultMaxBids),
+ UserDefaultBidBeforeDeadlineMs = COALESCE(@userBidDeadline, UserDefaultBidBeforeDeadlineMs),
HourlyStatsJson = @hourlyJson,
LastUpdated = @lastUpdated;
";
@@ -1457,6 +1480,12 @@ namespace AutoBidder.Services
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("@userMinPrice", (object?)stats.UserDefaultMinPrice ?? DBNull.Value),
+ new SqliteParameter("@userMaxPrice", (object?)stats.UserDefaultMaxPrice ?? DBNull.Value),
+ new SqliteParameter("@userMinResets", (object?)stats.UserDefaultMinResets ?? DBNull.Value),
+ new SqliteParameter("@userMaxResets", (object?)stats.UserDefaultMaxResets ?? DBNull.Value),
+ new SqliteParameter("@userMaxBids", (object?)stats.UserDefaultMaxBids ?? DBNull.Value),
+ new SqliteParameter("@userBidDeadline", (object?)stats.UserDefaultBidBeforeDeadlineMs ?? DBNull.Value),
new SqliteParameter("@hourlyJson", (object?)stats.HourlyStatsJson ?? DBNull.Value),
new SqliteParameter("@lastUpdated", DateTime.UtcNow.ToString("O"))
);
@@ -1473,6 +1502,7 @@ namespace AutoBidder.Services
AvgBidsToWin, MinBidsToWin, MaxBidsToWin,
AvgResets, MinResets, MaxResets,
RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids,
+ UserDefaultMinPrice, UserDefaultMaxPrice, UserDefaultMinResets, UserDefaultMaxResets, UserDefaultMaxBids, UserDefaultBidBeforeDeadlineMs,
HourlyStatsJson, LastUpdated
FROM ProductStatistics
WHERE ProductKey = @productKey;
@@ -1507,8 +1537,14 @@ namespace AutoBidder.Services
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)
+ UserDefaultMinPrice = reader.IsDBNull(19) ? null : reader.GetDouble(19),
+ UserDefaultMaxPrice = reader.IsDBNull(20) ? null : reader.GetDouble(20),
+ UserDefaultMinResets = reader.IsDBNull(21) ? null : reader.GetInt32(21),
+ UserDefaultMaxResets = reader.IsDBNull(22) ? null : reader.GetInt32(22),
+ UserDefaultMaxBids = reader.IsDBNull(23) ? null : reader.GetInt32(23),
+ UserDefaultBidBeforeDeadlineMs = reader.IsDBNull(24) ? null : reader.GetInt32(24),
+ HourlyStatsJson = reader.IsDBNull(25) ? null : reader.GetString(25),
+ LastUpdated = reader.GetString(26)
};
}
@@ -1558,6 +1594,7 @@ namespace AutoBidder.Services
AvgBidsToWin, MinBidsToWin, MaxBidsToWin,
AvgResets, MinResets, MaxResets,
RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids,
+ UserDefaultMinPrice, UserDefaultMaxPrice, UserDefaultMinResets, UserDefaultMaxResets, UserDefaultMaxBids, UserDefaultBidBeforeDeadlineMs,
HourlyStatsJson, LastUpdated
FROM ProductStatistics
ORDER BY TotalAuctions DESC;
@@ -1593,13 +1630,66 @@ namespace AutoBidder.Services
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)
+ UserDefaultMinPrice = reader.IsDBNull(19) ? null : reader.GetDouble(19),
+ UserDefaultMaxPrice = reader.IsDBNull(20) ? null : reader.GetDouble(20),
+ UserDefaultMinResets = reader.IsDBNull(21) ? null : reader.GetInt32(21),
+ UserDefaultMaxResets = reader.IsDBNull(22) ? null : reader.GetInt32(22),
+ UserDefaultMaxBids = reader.IsDBNull(23) ? null : reader.GetInt32(23),
+ UserDefaultBidBeforeDeadlineMs = reader.IsDBNull(24) ? null : reader.GetInt32(24),
+ HourlyStatsJson = reader.IsDBNull(25) ? null : reader.GetString(25),
+ LastUpdated = reader.GetString(26)
});
}
return results;
}
+
+ ///
+ /// Elimina un prodotto dalle statistiche per ProductKey
+ ///
+ public async Task DeleteProductStatisticsAsync(string productKey)
+ {
+ var sql = @"
+ DELETE FROM ProductStatistics
+ WHERE ProductKey = @productKey;
+ ";
+
+ return await ExecuteNonQueryAsync(sql,
+ new SqliteParameter("@productKey", productKey)
+ );
+ }
+
+ ///
+ /// Aggiorna i valori di default definiti dall'utente per un prodotto
+ ///
+ public async Task UpdateProductUserDefaultsAsync(string productKey,
+ double? minPrice, double? maxPrice,
+ int? minResets, int? maxResets,
+ int? maxBids, int? bidBeforeDeadlineMs)
+ {
+ var sql = @"
+ UPDATE ProductStatistics
+ SET UserDefaultMinPrice = @minPrice,
+ UserDefaultMaxPrice = @maxPrice,
+ UserDefaultMinResets = @minResets,
+ UserDefaultMaxResets = @maxResets,
+ UserDefaultMaxBids = @maxBids,
+ UserDefaultBidBeforeDeadlineMs = @bidDeadline,
+ LastUpdated = @lastUpdated
+ WHERE ProductKey = @productKey;
+ ";
+
+ await ExecuteNonQueryAsync(sql,
+ new SqliteParameter("@productKey", productKey),
+ new SqliteParameter("@minPrice", (object?)minPrice ?? DBNull.Value),
+ new SqliteParameter("@maxPrice", (object?)maxPrice ?? DBNull.Value),
+ new SqliteParameter("@minResets", (object?)minResets ?? DBNull.Value),
+ new SqliteParameter("@maxResets", (object?)maxResets ?? DBNull.Value),
+ new SqliteParameter("@maxBids", (object?)maxBids ?? DBNull.Value),
+ new SqliteParameter("@bidDeadline", (object?)bidBeforeDeadlineMs ?? DBNull.Value),
+ new SqliteParameter("@lastUpdated", DateTime.UtcNow.ToString("O"))
+ );
+ }
private AuctionResultExtended ReadAuctionResultExtended(Microsoft.Data.Sqlite.SqliteDataReader reader)
{
diff --git a/Mimante/Shared/NavMenu.razor b/Mimante/Shared/NavMenu.razor
index 67e9796..54d140e 100644
--- a/Mimante/Shared/NavMenu.razor
+++ b/Mimante/Shared/NavMenu.razor
@@ -1,6 +1,8 @@
@using Microsoft.AspNetCore.Components.Authorization
@inject NavigationManager NavigationManager
@inject AuthenticationStateProvider AuthenticationStateProvider
+@inject AuctionMonitor AuctionMonitor
+@implements IDisposable