Migliorie UI, log aste, strategie e statistiche puntatori

- Ordinamento colonne griglia aste e indicatori visivi
- Nuovo pulsante per rimozione rapida aste terminate
- Log aste con deduplicazione e contatore
- Statistiche puntatori cumulative e più affidabili
- Cronologia puntate senza duplicati consecutivi
- Strategie di puntata semplificate: entry point, anti-bot, user exhaustion
- UI più compatta, hover moderni, evidenziazione puntate utente
- Correzioni internazionalizzazione e pulizia codice
This commit is contained in:
2026-02-03 00:00:33 +01:00
parent ae861e78d2
commit 89aed8a458
13 changed files with 645 additions and 204 deletions

View File

@@ -128,20 +128,56 @@ namespace AutoBidder.Models
[JsonIgnore] [JsonIgnore]
public AuctionState? LastState { get; set; } public AuctionState? LastState { get; set; }
/// <summary> /// <summary>
/// Aggiunge una voce al log dell'asta con limite automatico di righe /// Aggiunge una voce al log dell'asta con deduplicazione e limite automatico di righe.
/// Se il messaggio è identico all'ultimo, incrementa un contatore invece di duplicare.
/// </summary> /// </summary>
/// <param name="message">Messaggio da aggiungere al log</param> /// <param name="message">Messaggio da aggiungere al log</param>
/// <param name="maxLines">Numero massimo di righe da mantenere (default: 500)</param> /// <param name="maxLines">Numero massimo di righe da mantenere (default: 500)</param>
public void AddLog(string message, int maxLines = 500) public void AddLog(string message, int maxLines = 500)
{ {
var entry = $"{DateTime.Now:HH:mm:ss.fff} - {message}"; var timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
// ?? DEDUPLICAZIONE: Se l'ultimo messaggio è uguale, incrementa contatore
if (AuctionLog.Count > 0)
{
var lastEntry = AuctionLog[^1]; // Ultimo elemento
// Estrai il messaggio senza timestamp e contatore
var lastMessageStart = lastEntry.IndexOf(" - ");
if (lastMessageStart > 0)
{
var lastMessage = lastEntry.Substring(lastMessageStart + 3);
// Rimuovi eventuale contatore esistente (es: " (x5)")
var counterMatch = System.Text.RegularExpressions.Regex.Match(lastMessage, @" \(x(\d+)\)$");
if (counterMatch.Success)
{
lastMessage = lastMessage.Substring(0, lastMessage.Length - counterMatch.Length);
}
// Se il messaggio è identico, aggiorna contatore
if (lastMessage == message)
{
int newCount = counterMatch.Success
? int.Parse(counterMatch.Groups[1].Value) + 1
: 2;
// Aggiorna l'ultimo entry con il nuovo contatore
AuctionLog[^1] = $"{timestamp} - {message} (x{newCount})";
return;
}
}
}
// Nuovo messaggio diverso dall'ultimo
var entry = $"{timestamp} - {message}";
AuctionLog.Add(entry); AuctionLog.Add(entry);
// Mantieni solo gli ultimi maxLines log // Mantieni solo gli ultimi maxLines log
if (AuctionLog.Count > maxLines) if (AuctionLog.Count > maxLines)
{ {
// Rimuovi i log più vecchi per mantenere la dimensione sotto controllo
int excessCount = AuctionLog.Count - maxLines; int excessCount = AuctionLog.Count - maxLines;
AuctionLog.RemoveRange(0, excessCount); AuctionLog.RemoveRange(0, excessCount);
} }

View File

@@ -3,12 +3,25 @@ using System;
namespace AutoBidder.Models namespace AutoBidder.Models
{ {
/// <summary> /// <summary>
/// Informazioni su un utente che ha piazzato puntate /// Informazioni su un utente che ha piazzato puntate.
/// Il conteggio è CUMULATIVO dall'inizio del monitoraggio (non limitato come RecentBids).
/// </summary> /// </summary>
public class BidderInfo public class BidderInfo
{ {
public string Username { get; set; } = ""; public string Username { get; set; } = "";
/// <summary>
/// Conteggio CUMULATIVO delle puntate dall'inizio del monitoraggio.
/// Questo valore non viene mai decrementato anche se RecentBids viene troncato.
/// </summary>
public int BidCount { get; set; } = 0; public int BidCount { get; set; } = 0;
/// <summary>
/// Conteggio puntate visibili nell'attuale finestra RecentBids (per UI).
/// Può essere inferiore a BidCount se RecentBids è stato troncato.
/// </summary>
public int RecentBidCount { get; set; } = 0;
public DateTime LastBidTime { get; set; } = DateTime.MinValue; public DateTime LastBidTime { get; set; } = DateTime.MinValue;
public string LastBidTimeDisplay => LastBidTime == DateTime.MinValue public string LastBidTimeDisplay => LastBidTime == DateTime.MinValue

View File

@@ -63,6 +63,9 @@
<button class="btn btn-secondary hover-lift" @onclick="RemoveSelectedAuction" disabled="@(selectedAuction == null)"> <button class="btn btn-secondary hover-lift" @onclick="RemoveSelectedAuction" disabled="@(selectedAuction == null)">
<i class="bi bi-trash"></i> Rimuovi <i class="bi bi-trash"></i> Rimuovi
</button> </button>
<button class="btn btn-outline-warning hover-lift" @onclick="RemoveCompletedAuctions" disabled="@(!HasCompletedAuctions())" title="Rimuovi aste terminate (vengono salvate nel database)">
<i class="bi bi-check2-all"></i> Rimuovi Terminate
</button>
<button class="btn btn-outline-danger hover-lift" @onclick="RemoveAllAuctions" disabled="@(auctions.Count == 0)" title="Rimuovi tutte le aste (quelle terminate verranno salvate)"> <button class="btn btn-outline-danger hover-lift" @onclick="RemoveAllAuctions" disabled="@(auctions.Count == 0)" title="Rimuovi tutte le aste (quelle terminate verranno salvate)">
<i class="bi bi-trash-fill"></i> Rimuovi Tutte <i class="bi bi-trash-fill"></i> Rimuovi Tutte
</button> </button>
@@ -85,18 +88,18 @@
<table class="table table-striped table-hover mb-0 table-fixed"> <table class="table table-striped table-hover mb-0 table-fixed">
<thead> <thead>
<tr> <tr>
<th class="col-stato"><i class="bi bi-toggle-on"></i> Stato</th> <th class="col-stato sortable-header" @onclick='() => SortAuctionsBy("stato")'><i class="bi bi-toggle-on"></i> Stato @GetSortIndicator("stato")</th>
<th class="col-nome"><i class="bi bi-tag"></i> Nome</th> <th class="col-nome sortable-header" @onclick='() => SortAuctionsBy("nome")'><i class="bi bi-tag"></i> Nome @GetSortIndicator("nome")</th>
<th class="col-prezzo"><i class="bi bi-currency-euro"></i> Prezzo</th> <th class="col-prezzo sortable-header" @onclick='() => SortAuctionsBy("prezzo")'><i class="bi bi-currency-euro"></i> Prezzo @GetSortIndicator("prezzo")</th>
<th class="col-timer"><i class="bi bi-clock"></i> Timer</th> <th class="col-timer sortable-header" @onclick='() => SortAuctionsBy("timer")'><i class="bi bi-clock"></i> Timer @GetSortIndicator("timer")</th>
<th class="col-ultimo"><i class="bi bi-person"></i> Ultimo</th> <th class="col-ultimo"><i class="bi bi-person"></i> Ultimo</th>
<th class="col-click"><i class="bi bi-hand-index" style="font-size: 0.85rem;"></i> Puntate</th> <th class="col-click sortable-header" @onclick='() => SortAuctionsBy("puntate")'><i class="bi bi-hand-index" style="font-size: 0.85rem;"></i> Puntate @GetSortIndicator("puntate")</th>
<th class="col-ping"><i class="bi bi-speedometer"></i> Ping</th> <th class="col-ping sortable-header" @onclick='() => SortAuctionsBy("ping")'><i class="bi bi-speedometer"></i> Ping @GetSortIndicator("ping")</th>
<th class="col-azioni"><i class="bi bi-gear"></i> Azioni</th> <th class="col-azioni"><i class="bi bi-gear"></i> Azioni</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach (var auction in auctions) @foreach (var auction in GetSortedAuctions())
{ {
<tr class="@GetRowClass(auction) @(selectedAuction == auction ? "selected-row" : "") table-row-enter transition-all" <tr class="@GetRowClass(auction) @(selectedAuction == auction ? "selected-row" : "") table-row-enter transition-all"
@onclick="() => SelectAuction(auction)" @onclick="() => SelectAuction(auction)"
@@ -387,9 +390,24 @@
<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">
@{ @{
// ?? FIX: Rimuovi duplicati consecutivi (stesso prezzo + stesso utente)
var recentBidsList = GetRecentBidsSafe(selectedAuction); var recentBidsList = GetRecentBidsSafe(selectedAuction);
var filteredBids = new List<BidHistoryEntry>();
BidHistoryEntry? lastBid = null;
foreach (var bid in recentBidsList)
{
// Salta se è un duplicato del precedente (stesso prezzo E stesso utente)
if (lastBid != null &&
Math.Abs(bid.Price - lastBid.Price) < 0.001m &&
bid.Username.Equals(lastBid.Username, StringComparison.OrdinalIgnoreCase))
{
continue;
}
filteredBids.Add(bid);
lastBid = bid;
}
} }
@if (recentBidsList.Any()) @if (filteredBids.Any())
{ {
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-sm table-striped"> <table class="table table-sm table-striped">
@@ -402,15 +420,19 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach (var bid in recentBidsList.Take(50)) @foreach (var bid in filteredBids.Take(50))
{ {
<tr class="@(bid.IsMyBid ? "table-success" : "")"> <tr class="@(bid.IsMyBid ? "my-bid-row" : "")">
<td> <td>
@bid.Username
@if (bid.IsMyBid) @if (bid.IsMyBid)
{ {
<strong class="text-success">@bid.Username</strong>
<span class="badge bg-success ms-1">TU</span> <span class="badge bg-success ms-1">TU</span>
} }
else
{
@bid.Username
}
</td> </td>
<td class="fw-bold">€@bid.PriceFormatted</td> <td class="fw-bold">€@bid.PriceFormatted</td>
<td class="text-muted small">@bid.TimeFormatted</td> <td class="text-muted small">@bid.TimeFormatted</td>
@@ -434,29 +456,29 @@
<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">
@{ @{
// Crea una copia thread-safe per evitare modifiche durante l'enumerazione // ?? FIX: Usa BidderStats che contiene i conteggi CUMULATIVI (non limitati)
var recentBidsCopy = GetRecentBidsSafe(selectedAuction); var bidderStatsCopy = selectedAuction.BidderStats
.Values
.OrderByDescending(b => b.BidCount)
.ToList();
// ?? FIX: Per l'utente corrente, usa BidsUsedOnThisAuction (valore ufficiale dal server) // Per l'utente corrente, usa BidsUsedOnThisAuction (valore ufficiale dal server)
var myOfficialBidsCount = selectedAuction.BidsUsedOnThisAuction ?? 0; var myOfficialBidsCount = selectedAuction.BidsUsedOnThisAuction ?? 0;
var currentUsername = GetCurrentUsername(); var currentUsername = GetCurrentUsername();
} }
@if (recentBidsCopy.Any()) @if (bidderStatsCopy.Any())
{ {
// Calcola statistiche puntatori // Calcola il totale CUMULATIVO
var bidderStats = recentBidsCopy var totalBidsCumulative = bidderStatsCopy.Sum(b => b.BidCount);
.GroupBy(b => b.Username)
.Select(g => new {
Username = g.Key,
// Per l'utente corrente usa il conteggio ufficiale, per gli altri conta dalla lista
Count = g.First().IsMyBid && myOfficialBidsCount > 0 ? myOfficialBidsCount : g.Count(),
IsMe = g.First().IsMyBid
})
.OrderByDescending(s => s.Count)
.ToList();
// Ricalcola il totale includendo il conteggio corretto per l'utente // Correggi il conteggio per l'utente corrente se disponibile
var totalBids = bidderStats.Sum(b => b.Count); var myBidder = bidderStatsCopy.FirstOrDefault(b =>
b.Username.Equals(currentUsername, StringComparison.OrdinalIgnoreCase));
if (myBidder != null && myOfficialBidsCount > myBidder.BidCount)
{
// Usa il valore ufficiale se maggiore
totalBidsCumulative = totalBidsCumulative - myBidder.BidCount + myOfficialBidsCount;
}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-sm table-striped"> <table class="table table-sm table-striped">
@@ -469,25 +491,32 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@for (int i = 0; i < bidderStats.Count; i++) @for (int i = 0; i < bidderStatsCopy.Count; i++)
{ {
var bidder = bidderStats[i]; var bidder = bidderStatsCopy[i];
var percentage = (bidder.Count * 100.0 / totalBids); var isMe = bidder.Username.Equals(currentUsername, StringComparison.OrdinalIgnoreCase);
<tr class="@(bidder.IsMe ? "table-success" : "")"> // Per l'utente corrente usa il conteggio ufficiale
var displayCount = isMe && myOfficialBidsCount > bidder.BidCount
? myOfficialBidsCount
: bidder.BidCount;
var percentage = totalBidsCumulative > 0
? (displayCount * 100.0 / totalBidsCumulative)
: 0;
<tr class="@(isMe ? "table-success" : "")">
<td><span class="badge bg-primary">#@(i + 1)</span></td> <td><span class="badge bg-primary">#@(i + 1)</span></td>
<td> <td>
@bidder.Username @bidder.Username
@if (bidder.IsMe) @if (isMe)
{ {
<span class="badge bg-success ms-1">TU</span> <span class="badge bg-success ms-1">TU</span>
} }
</td> </td>
<td class="fw-bold">@bidder.Count</td> <td class="fw-bold">@displayCount</td>
<td> <td>
<div class="progress" style="height: 20px;"> <div class="progress" style="height: 20px;">
<div class="progress-bar @(bidder.IsMe ? "bg-success" : "bg-primary")" <div class="progress-bar @(isMe ? "bg-success" : "bg-primary")"
role="progressbar" role="progressbar"
style="width: @percentage.ToString("F1")%"> style="width: @percentage.ToString("F1", System.Globalization.CultureInfo.InvariantCulture)%">
@percentage.ToString("F1")% @percentage.ToString("F1")%
</div> </div>
</div> </div>

View File

@@ -28,6 +28,7 @@ namespace AutoBidder.Pages
set => AppState.IsMonitoringActive = value; set => AppState.IsMonitoringActive = value;
} }
private System.Threading.Timer? refreshTimer; private System.Threading.Timer? refreshTimer;
private System.Threading.Timer? sessionTimer; private System.Threading.Timer? sessionTimer;
@@ -50,6 +51,10 @@ namespace AutoBidder.Pages
// Auto-scroll log // Auto-scroll log
private ElementReference globalLogRef; private ElementReference globalLogRef;
private int lastLogCount = 0; private int lastLogCount = 0;
// ?? Sorting griglia aste
private string auctionSortColumn = "nome";
private bool auctionSortAscending = true;
protected override void OnInitialized() protected override void OnInitialized()
{ {
@@ -137,7 +142,8 @@ namespace AutoBidder.Pages
private void SaveAuctions() private void SaveAuctions()
{ {
AutoBidder.Utilities.PersistenceManager.SaveAuctions(auctions); // ?? FIX: Usa il metodo dedicato che salva la lista originale, non una copia
AppState.PersistAuctions();
AddLog("Aste salvate"); AddLog("Aste salvate");
} }
@@ -299,6 +305,11 @@ namespace AutoBidder.Pages
{ {
auction.BidsUsedOnThisAuction = result.BidsUsedOnThisAuction.Value; auction.BidsUsedOnThisAuction = result.BidsUsedOnThisAuction.Value;
} }
else
{
// Incrementa contatore locale se server non risponde
auction.BidsUsedOnThisAuction = (auction.BidsUsedOnThisAuction ?? 0) + 1;
}
SaveAuctions(); SaveAuctions();
} }
@@ -618,6 +629,61 @@ namespace AutoBidder.Pages
await JSRuntime.InvokeVoidAsync("alert", $"Errore durante la rimozione:\n{ex.Message}"); await JSRuntime.InvokeVoidAsync("alert", $"Errore durante la rimozione:\n{ex.Message}");
} }
} }
/// <summary>
/// Rimuove tutte le aste terminate (salvandole prima nel database)
/// </summary>
private async Task RemoveCompletedAuctions()
{
var completedAuctions = auctions.Where(a => !a.IsActive).ToList();
if (completedAuctions.Count == 0)
{
await JSRuntime.InvokeVoidAsync("alert", "Nessuna asta terminata da rimuovere.");
return;
}
var confirmed = await JSRuntime.InvokeAsync<bool>("confirm",
$"Rimuovere {completedAuctions.Count} aste terminate?\n\n" +
"? Verranno salvate automaticamente nelle statistiche.");
if (!confirmed) return;
try
{
int removed = 0;
foreach (var auction in completedAuctions)
{
// RemoveAuction salva automaticamente l'asta nel database se terminata
AuctionMonitor.RemoveAuction(auction.AuctionId);
AppState.RemoveAuction(auction);
removed++;
}
// Deseleziona se l'asta selezionata era tra quelle rimosse
if (selectedAuction != null && !selectedAuction.IsActive)
{
selectedAuction = null;
}
SaveAuctions();
AddLog($"[CLEANUP] Rimosse {removed} aste terminate");
await JSRuntime.InvokeVoidAsync("alert", $"? Rimosse {removed} aste terminate.\nSono state salvate nelle statistiche.");
}
catch (Exception ex)
{
AddLog($"Errore rimozione aste terminate: {ex.Message}");
await JSRuntime.InvokeVoidAsync("alert", $"Errore:\n{ex.Message}");
}
}
/// <summary>
/// Verifica se ci sono aste terminate
/// </summary>
private bool HasCompletedAuctions()
{
return auctions.Any(a => !a.IsActive);
}
private async Task RemoveSelectedAuctionWithConfirm() private async Task RemoveSelectedAuctionWithConfirm()
{ {
@@ -909,6 +975,57 @@ namespace AutoBidder.Pages
return sessionUsername ?? ""; return sessionUsername ?? "";
} }
// ?? SORTING GRIGLIA ASTE
private void SortAuctionsBy(string column)
{
if (auctionSortColumn == column)
{
auctionSortAscending = !auctionSortAscending;
}
else
{
auctionSortColumn = column;
auctionSortAscending = true;
}
}
private MarkupString GetSortIndicator(string column)
{
if (auctionSortColumn != column) return new MarkupString("");
return new MarkupString(auctionSortAscending ? " ?" : " ?");
}
private IEnumerable<AuctionInfo> GetSortedAuctions()
{
var list = auctions.AsEnumerable();
list = auctionSortColumn switch
{
"stato" => auctionSortAscending
? list.OrderBy(a => a.IsActive).ThenBy(a => a.IsPaused)
: list.OrderByDescending(a => a.IsActive).ThenByDescending(a => a.IsPaused),
"nome" => auctionSortAscending
? list.OrderBy(a => a.Name)
: list.OrderByDescending(a => a.Name),
"prezzo" => auctionSortAscending
? list.OrderBy(a => a.LastState?.Price ?? 0)
: list.OrderByDescending(a => a.LastState?.Price ?? 0),
"timer" => auctionSortAscending
? list.OrderBy(a => a.LastState?.Timer ?? 999)
: list.OrderByDescending(a => a.LastState?.Timer ?? 999),
"puntate" => auctionSortAscending
? list.OrderBy(a => a.BidsUsedOnThisAuction ?? 0)
: list.OrderByDescending(a => a.BidsUsedOnThisAuction ?? 0),
"ping" => auctionSortAscending
? list.OrderBy(a => a.PollingLatencyMs)
: list.OrderByDescending(a => a.PollingLatencyMs),
_ => list
};
return list;
}
// ?? NUOVI METODI: Visualizzazione valori prodotto // ?? NUOVI METODI: Visualizzazione valori prodotto
private string GetTotalCostDisplay(AuctionInfo? auction) private string GetTotalCostDisplay(AuctionInfo? auction)

View File

@@ -1,4 +1,5 @@
using AutoBidder.Models; using AutoBidder.Models;
using AutoBidder.Utilities;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -52,6 +53,29 @@ namespace AutoBidder.Services
} }
} }
/// <summary>
/// Ottiene la lista originale delle aste per il salvataggio.
/// ATTENZIONE: Usare solo per persistenza, non per iterazione durante modifiche!
/// </summary>
public List<AuctionInfo> GetAuctionsForPersistence()
{
lock (_lock)
{
return _auctions;
}
}
/// <summary>
/// Forza il salvataggio delle aste correnti su disco.
/// </summary>
public void PersistAuctions()
{
lock (_lock)
{
AutoBidder.Utilities.PersistenceManager.SaveAuctions(_auctions);
}
}
public AuctionInfo? SelectedAuction public AuctionInfo? SelectedAuction
{ {
get get

View File

@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@@ -98,7 +98,7 @@ namespace AutoBidder.Services
if (!_auctions.Any(a => a.AuctionId == auction.AuctionId)) if (!_auctions.Any(a => a.AuctionId == auction.AuctionId))
{ {
_auctions.Add(auction); _auctions.Add(auction);
// ? RIMOSSO: Log ridondante - viene già loggato da MainWindow con defaults e stato // ? RIMOSSO: Log ridondante - viene già loggato da MainWindow con defaults e stato
// OnLog?.Invoke($"[+] Asta aggiunta: {auction.Name} (ID: {auction.AuctionId})"); // OnLog?.Invoke($"[+] Asta aggiunta: {auction.Name} (ID: {auction.AuctionId})");
} }
} }
@@ -111,20 +111,20 @@ namespace AutoBidder.Services
var auction = _auctions.FirstOrDefault(a => a.AuctionId == auctionId); var auction = _auctions.FirstOrDefault(a => a.AuctionId == auctionId);
if (auction != null) if (auction != null)
{ {
// ?? Se l'asta è terminata, salva le statistiche prima di rimuoverla // ?? Se l'asta è terminata, salva le statistiche prima di rimuoverla
if (!auction.IsActive && auction.LastState != null) if (!auction.IsActive && auction.LastState != null)
{ {
OnLog?.Invoke($"[STATS] Asta terminata rilevata: {auction.Name} - Salvataggio statistiche in corso..."); OnLog?.Invoke($"[STATS] Asta terminata rilevata: {auction.Name} - Salvataggio statistiche in corso...");
try try
{ {
// Determina se è stata vinta dall'utente // Determina se è stata vinta dall'utente
var won = IsAuctionWonByUser(auction); var won = IsAuctionWonByUser(auction);
OnLog?.Invoke($"[STATS] Asta {auction.Name} - Stato: {(won ? "VINTA" : "PERSA")}"); OnLog?.Invoke($"[STATS] Asta {auction.Name} - Stato: {(won ? "VINTA" : "PERSA")}");
// Emetti evento per salvare le statistiche // Emetti evento per salvare le statistiche
// Questo trigger sarà gestito in Program.cs con scraping HTML // Questo trigger sarà gestito in Program.cs con scraping HTML
OnAuctionCompleted?.Invoke(auction, auction.LastState, won); OnAuctionCompleted?.Invoke(auction, auction.LastState, won);
} }
catch (Exception ex) catch (Exception ex)
@@ -143,7 +143,7 @@ namespace AutoBidder.Services
} }
/// <summary> /// <summary>
/// Determina se l'asta è stata vinta dall'utente corrente /// Determina se l'asta è stata vinta dall'utente corrente
/// </summary> /// </summary>
private bool IsAuctionWonByUser(AuctionInfo auction) private bool IsAuctionWonByUser(AuctionInfo auction)
{ {
@@ -154,7 +154,7 @@ namespace AutoBidder.Services
if (string.IsNullOrEmpty(username)) return false; if (string.IsNullOrEmpty(username)) return false;
// Controlla se l'ultimo puntatore è l'utente // Controlla se l'ultimo puntatore è l'utente
return auction.LastState.LastBidder?.Equals(username, StringComparison.OrdinalIgnoreCase) == true; return auction.LastState.LastBidder?.Equals(username, StringComparison.OrdinalIgnoreCase) == true;
} }
@@ -288,7 +288,7 @@ namespace AutoBidder.Services
continue; continue;
} }
// Delay adattivo OTTIMIZZATO basato su timer più basso // Delay adattivo OTTIMIZZATO basato su timer più basso
var lowestTimer = activeAuctions var lowestTimer = activeAuctions
.Select(a => GetLastTimer(a)) .Select(a => GetLastTimer(a))
.Where(t => t > 0) .Where(t => t > 0)
@@ -355,7 +355,7 @@ namespace AutoBidder.Services
// ?? Aggiorna latenza con storico // ?? Aggiorna latenza con storico
auction.AddLatencyMeasurement(state.PollingLatencyMs); auction.AddLatencyMeasurement(state.PollingLatencyMs);
// ?? Segna tracking dall'inizio se è la prima volta // ?? Segna tracking dall'inizio se è la prima volta
if (!auction.IsTrackedFromStart && auction.BidHistory.Count == 0) if (!auction.IsTrackedFromStart && auction.BidHistory.Count == 0)
{ {
auction.IsTrackedFromStart = true; auction.IsTrackedFromStart = true;
@@ -367,6 +367,13 @@ namespace AutoBidder.Services
{ {
MergeBidHistory(auction, state.RecentBidsHistory); MergeBidHistory(auction, state.RecentBidsHistory);
} }
// 🔥 FIX: Aggiungi SEMPRE l'ultima puntata corrente allo storico (durante il monitoraggio)
// Questo assicura che la puntata vincente sia sempre inclusa
if (!string.IsNullOrEmpty(state.LastBidder) && state.Price > 0)
{
EnsureCurrentBidInHistory(auction, state);
}
if (state.Status == AuctionStatus.EndedWon || if (state.Status == AuctionStatus.EndedWon ||
state.Status == AuctionStatus.EndedLost || state.Status == AuctionStatus.EndedLost ||
@@ -384,7 +391,7 @@ namespace AutoBidder.Services
var lastBidTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); var lastBidTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var statePrice = (decimal)state.Price; var statePrice = (decimal)state.Price;
// Verifica se questa puntata non è già presente // Verifica se questa puntata non è già presente
var alreadyExists = auction.RecentBids.Any(b => var alreadyExists = auction.RecentBids.Any(b =>
Math.Abs(b.Price - statePrice) < 0.001m && Math.Abs(b.Price - statePrice) < 0.001m &&
b.Username.Equals(state.LastBidder, StringComparison.OrdinalIgnoreCase)); b.Username.Equals(state.LastBidder, StringComparison.OrdinalIgnoreCase));
@@ -399,7 +406,7 @@ namespace AutoBidder.Services
BidType = "Auto" BidType = "Auto"
}); });
auction.AddLog($"[FIX] Aggiunta ultima puntata mancante: {state.LastBidder} €{state.Price:F2}"); auction.AddLog($"[FIX] Aggiunta ultima puntata mancante: {state.LastBidder} €{state.Price:F2}");
} }
} }
@@ -436,7 +443,7 @@ namespace AutoBidder.Services
if (state.Status == AuctionStatus.Running) if (state.Status == AuctionStatus.Running)
{ {
// Log RIMOSSO per ridurre verbosità - polling continuo non necessita log // Log RIMOSSO per ridurre verbosità - polling continuo non necessita log
// Solo eventi importanti (bid, reset, errori) vengono loggati // Solo eventi importanti (bid, reset, errori) vengono loggati
} }
else if (state.Status == AuctionStatus.Paused) else if (state.Status == AuctionStatus.Paused)
@@ -502,8 +509,9 @@ namespace AutoBidder.Services
} }
/// <summary> /// <summary>
/// Strategia di puntata ottimizzata con BidStrategyService /// Strategia di puntata SEMPLIFICATA
/// Usa: adaptive latency, jitter, dynamic offset, heat metric, competition detection /// Le strategie restituiscono solo un booleano (posso puntare?).
/// Il timing è sempre fisso: offset globale o override per asta.
/// </summary> /// </summary>
private async Task ExecuteBidStrategy(AuctionInfo auction, AuctionState state, CancellationToken token) private async Task ExecuteBidStrategy(AuctionInfo auction, AuctionState state, CancellationToken token)
{ {
@@ -512,58 +520,46 @@ namespace AutoBidder.Services
// Calcola il tempo rimanente in millisecondi // Calcola il tempo rimanente in millisecondi
double timerMs = state.Timer * 1000; double timerMs = state.Timer * 1000;
// ??? CONTROLLO: Se sono già il vincitore, non fare nulla // 🛑 CONTROLLO: Se sono già il vincitore, non fare nulla
if (state.IsMyBid) if (state.IsMyBid)
{ {
return; return;
} }
// ?? AGGIORNA METRICHE (solo se strategie avanzate abilitate) // 🎯 OFFSET FISSO: Usa override asta se impostato, altrimenti globale
if (auction.AdvancedStrategiesEnabled != false) int fixedOffsetMs = auction.BidBeforeDeadlineMs > 0
? auction.BidBeforeDeadlineMs
: settings.DefaultBidBeforeDeadlineMs;
// 🧠 VALUTAZIONE STRATEGIE (restituiscono solo bool)
var session = _apiClient.GetSession();
var currentUsername = session?.Username ?? "";
// Verifica se le strategie permettono di puntare
var decision = _bidStrategy.ShouldPlaceBid(auction, state, settings, currentUsername);
if (!decision.ShouldBid)
{ {
var session = _apiClient.GetSession(); auction.AddLog($"[STRATEGY] {decision.Reason}");
var currentUsername = session?.Username ?? ""; return;
_bidStrategy.UpdateHeatMetric(auction, settings, currentUsername);
// Verifica strategie avanzate (soft retreat, competition, probabilistic, etc.)
var decision = _bidStrategy.ShouldPlaceBid(auction, state, settings, currentUsername);
if (!decision.ShouldBid)
{
auction.AddLog($"[STRATEGY] {decision.Reason}");
return;
}
} }
// ?? CALCOLA TIMING OTTIMALE // 🕐 TIMER-BASED SCHEDULING (offset fisso, niente calcoli dinamici)
var timing = _bidStrategy.CalculateOptimalTiming(auction, settings); if (timerMs > fixedOffsetMs)
int effectiveOffset = timing.FinalOffsetMs;
// ?? TIMER-BASED SCHEDULING
if (timerMs > effectiveOffset)
{ {
// Timer ancora alto ? Schedula puntata futura // Timer ancora alto - Schedula puntata futura
double delayMs = timerMs - effectiveOffset; double delayMs = timerMs - fixedOffsetMs;
// Non schedulare se già c'è un task attivo per questa asta // Non schedulare se già c'è un task attivo per questa asta
if (auction.IsAttackInProgress) if (auction.IsAttackInProgress)
{ {
return; // Task già schedulato return;
} }
auction.IsAttackInProgress = true; auction.IsAttackInProgress = true;
auction.LastUsedOffsetMs = effectiveOffset; auction.LastUsedOffsetMs = fixedOffsetMs;
// Log con dettagli timing (solo se logging avanzato) auction.AddLog($"[TIMING] Timer={timerMs:F0}ms - Puntata tra {delayMs:F0}ms (offset fisso={fixedOffsetMs}ms)");
if (settings.AdvancedLoggingEnabled)
{
auction.AddLog($"[TIMING] Timer={timerMs:F0}ms, Offset={effectiveOffset}ms (base={timing.BaseOffsetMs}+lat={timing.LatencyCompensationMs}+dyn={timing.DynamicAdjustmentMs}+jit={timing.JitterMs}) ? Delay={delayMs:F0}ms");
}
else
{
auction.AddLog($"[STRATEGIA] Timer={timerMs:F0}ms ? Puntata tra {delayMs:F0}ms (offset={effectiveOffset}ms)");
}
// Avvia task asincrono che attende e poi punta // Avvia task asincrono che attende e poi punta
_ = Task.Run(async () => _ = Task.Run(async () =>
@@ -576,42 +572,22 @@ namespace AutoBidder.Services
// Verifica che l'asta sia ancora attiva e non in pausa // Verifica che l'asta sia ancora attiva e non in pausa
if (!auction.IsActive || auction.IsPaused || token.IsCancellationRequested) if (!auction.IsActive || auction.IsPaused || token.IsCancellationRequested)
{ {
auction.AddLog($"[STRATEGIA] Task annullato (asta inattiva/pausa)"); auction.AddLog($"[TIMING] Task annullato (asta inattiva/pausa)");
return; return;
} }
// Verifica soft retreat auction.AddLog($"[TIMING] Eseguo puntata!");
if (auction.IsInSoftRetreat)
{
auction.AddLog($"[STRATEGIA] Task annullato (soft retreat attivo)");
return;
}
// Controlla se qualcun altro ha puntato di recente
var lastBidTime = GetLastBidTime(auction, state.LastBidder);
if (lastBidTime.HasValue)
{
var timeSinceLastBid = DateTime.UtcNow - lastBidTime.Value;
if (timeSinceLastBid.TotalMilliseconds < 500)
{
auction.AddLog($"[COLLISION] Puntata recente di {state.LastBidder} ({timeSinceLastBid.TotalMilliseconds:F0}ms fa)");
_bidStrategy.RecordBidAttempt(auction, false, collision: true);
return;
}
}
auction.AddLog($"[STRATEGIA] Task eseguito ? PUNTA ORA!");
// Esegui la puntata // Esegui la puntata
await ExecuteBid(auction, state, token); await ExecuteBid(auction, state, token);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
auction.AddLog($"[STRATEGIA] Task cancellato"); auction.AddLog($"[TIMING] Task cancellato");
} }
catch (Exception ex) catch (Exception ex)
{ {
auction.AddLog($"[STRATEGIA ERROR] {ex.Message}"); auction.AddLog($"[TIMING ERROR] {ex.Message}");
} }
finally finally
{ {
@@ -619,33 +595,20 @@ namespace AutoBidder.Services
} }
}, token); }, token);
} }
else if (timerMs > 0 && timerMs <= effectiveOffset) else if (timerMs > 0 && timerMs <= fixedOffsetMs)
{ {
// Timer già nella finestra ? Punta SUBITO senza delay // Timer già nella finestra - Punta SUBITO senza delay
if (auction.IsAttackInProgress) if (auction.IsAttackInProgress)
{ {
return; // Già in corso return;
} }
auction.IsAttackInProgress = true; auction.IsAttackInProgress = true;
auction.LastUsedOffsetMs = effectiveOffset; auction.LastUsedOffsetMs = fixedOffsetMs;
try try
{ {
auction.AddLog($"[STRATEGIA] Timer già in finestra ({timerMs:F0}ms <= {effectiveOffset}ms) ? PUNTA SUBITO!"); auction.AddLog($"[TIMING] Timer in finestra ({timerMs:F0}ms <= {fixedOffsetMs}ms) - PUNTA SUBITO!");
// Controlla se qualcun altro ha puntato di recente
var lastBidTime = GetLastBidTime(auction, state.LastBidder);
if (lastBidTime.HasValue)
{
var timeSinceLastBid = DateTime.UtcNow - lastBidTime.Value;
if (timeSinceLastBid.TotalMilliseconds < 500)
{
auction.AddLog($"[COLLISION] Puntata recente di {state.LastBidder} ({timeSinceLastBid.TotalMilliseconds:F0}ms fa)");
_bidStrategy.RecordBidAttempt(auction, false, collision: true);
return;
}
}
// Esegui la puntata // Esegui la puntata
await ExecuteBid(auction, state, token); await ExecuteBid(auction, state, token);
@@ -655,7 +618,7 @@ namespace AutoBidder.Services
auction.IsAttackInProgress = false; auction.IsAttackInProgress = false;
} }
} }
// Se timer <= 0, asta già scaduta ? Non fare nulla // Se timer <= 0, asta già scaduta - Non fare nulla
} }
/// <summary> /// <summary>
@@ -678,17 +641,24 @@ namespace AutoBidder.Services
_bidStrategy.RecordTimerExpired(auction); _bidStrategy.RecordTimerExpired(auction);
} }
// Aggiorna dati puntate da risposta server // 🔥 FIX: Aggiorna contatore puntate
if (result.Success) if (result.Success)
{ {
if (result.RemainingBids.HasValue) if (result.RemainingBids.HasValue)
{ {
auction.RemainingBids = result.RemainingBids.Value; auction.RemainingBids = result.RemainingBids.Value;
} }
// Usa valore server se disponibile, altrimenti incrementa localmente
if (result.BidsUsedOnThisAuction.HasValue) if (result.BidsUsedOnThisAuction.HasValue)
{ {
auction.BidsUsedOnThisAuction = result.BidsUsedOnThisAuction.Value; auction.BidsUsedOnThisAuction = result.BidsUsedOnThisAuction.Value;
} }
else
{
// Incrementa contatore locale
auction.BidsUsedOnThisAuction = (auction.BidsUsedOnThisAuction ?? 0) + 1;
}
} }
OnBidExecuted?.Invoke(auction, result); OnBidExecuted?.Invoke(auction, result);
@@ -727,11 +697,11 @@ namespace AutoBidder.Services
{ {
var settings = Utilities.SettingsManager.Load(); var settings = Utilities.SettingsManager.Load();
// ?? CONTROLLO ANTI-AUTOBID BIDOO (PRIORITÀ MASSIMA) // ?? CONTROLLO ANTI-AUTOBID BIDOO (PRIORITÀ MASSIMA)
// Bidoo ha un sistema di auto-puntata che si attiva a ~2 secondi. // Bidoo ha un sistema di auto-puntata che si attiva a ~2 secondi.
// Aspettiamo che il timer scenda sotto la soglia per lasciare che // Aspettiamo che il timer scenda sotto la soglia per lasciare che
// gli altri utenti con auto-puntata attiva puntino prima di noi. // gli altri utenti con auto-puntata attiva puntino prima di noi.
// Questo ci fa risparmiare puntate perché non puntiamo "troppo presto". // Questo ci fa risparmiare puntate perché non puntiamo "troppo presto".
if (settings.WaitForAutoBidEnabled && state.Timer > settings.WaitForAutoBidThresholdSeconds) if (settings.WaitForAutoBidEnabled && state.Timer > settings.WaitForAutoBidThresholdSeconds)
{ {
// Timer ancora sopra la soglia - aspetta che le auto-puntate si attivino // Timer ancora sopra la soglia - aspetta che le auto-puntate si attivino
@@ -743,7 +713,7 @@ namespace AutoBidder.Services
} }
// ?? CONTROLLO 0: Verifica convenienza (se dati disponibili) // ?? CONTROLLO 0: Verifica convenienza (se dati disponibili)
// IMPORTANTE: Applica solo se BuyNowPrice è valido (> 0) // IMPORTANTE: Applica solo se BuyNowPrice è valido (> 0)
// Se BuyNowPrice == 0, significa errore scraping - non bloccare le puntate // Se BuyNowPrice == 0, significa errore scraping - non bloccare le puntate
if (auction.BuyNowPrice.HasValue && if (auction.BuyNowPrice.HasValue &&
auction.BuyNowPrice.Value > 0 && auction.BuyNowPrice.Value > 0 &&
@@ -751,7 +721,7 @@ namespace AutoBidder.Services
auction.CalculatedValue.Savings.HasValue && auction.CalculatedValue.Savings.HasValue &&
!auction.CalculatedValue.IsWorthIt) !auction.CalculatedValue.IsWorthIt)
{ {
// Permetti comunque di puntare se il risparmio è ancora positivo (anche se piccolo) // Permetti comunque di puntare se il risparmio è ancora positivo (anche se piccolo)
// Blocca solo se sta andando in perdita significativa (< -5%) // Blocca solo se sta andando in perdita significativa (< -5%)
if (auction.CalculatedValue.SavingsPercentage.HasValue && if (auction.CalculatedValue.SavingsPercentage.HasValue &&
auction.CalculatedValue.SavingsPercentage.Value < -5) auction.CalculatedValue.SavingsPercentage.Value < -5)
@@ -780,7 +750,7 @@ namespace AutoBidder.Services
if (activeBidders >= maxActiveBidders) if (activeBidders >= maxActiveBidders)
{ {
// Controlla se l'ultimo bidder sono io - se sì, posso continuare // Controlla se l'ultimo bidder sono io - se sì, posso continuare
var session = _apiClient.GetSession(); var session = _apiClient.GetSession();
var lastBid = recentBids.OrderByDescending(b => b.Timestamp).FirstOrDefault(); var lastBid = recentBids.OrderByDescending(b => b.Timestamp).FirstOrDefault();
@@ -805,7 +775,7 @@ namespace AutoBidder.Services
} }
} }
// ? CONTROLLO 2: Non puntare se sono già il vincitore corrente // ? CONTROLLO 2: Non puntare se sono già il vincitore corrente
if (state.IsMyBid) if (state.IsMyBid)
{ {
return false; return false;
@@ -814,13 +784,13 @@ namespace AutoBidder.Services
// ?? CONTROLLO 3: MinPrice/MaxPrice // ?? CONTROLLO 3: MinPrice/MaxPrice
if (auction.MinPrice > 0 && state.Price < auction.MinPrice) if (auction.MinPrice > 0 && state.Price < auction.MinPrice)
{ {
auction.AddLog($"[PRICE] Prezzo troppo basso: €{state.Price:F2} < Min €{auction.MinPrice:F2}"); auction.AddLog($"[PRICE] Prezzo troppo basso: €{state.Price:F2} < Min €{auction.MinPrice:F2}");
return false; return false;
} }
if (auction.MaxPrice > 0 && state.Price > auction.MaxPrice) if (auction.MaxPrice > 0 && state.Price > auction.MaxPrice)
{ {
auction.AddLog($"[PRICE] Prezzo troppo alto: €{state.Price:F2} > Max €{auction.MaxPrice:F2}"); auction.AddLog($"[PRICE] Prezzo troppo alto: €{state.Price:F2} > Max €{auction.MaxPrice:F2}");
return false; return false;
} }
@@ -926,8 +896,8 @@ namespace AutoBidder.Services
/// <summary> /// <summary>
/// Unisce la storia puntate ricevuta dall'API con quella esistente, /// Unisce la storia puntate ricevuta dall'API con quella esistente,
/// mantenendo le puntate più vecchie e aggiungendo solo le nuove. /// mantenendo le puntate più vecchie e aggiungendo solo le nuove.
/// Le puntate sono ordinate con le più RECENTI in CIMA. /// Le puntate sono ordinate con le più RECENTI in CIMA.
/// </summary> /// </summary>
private void MergeBidHistory(AuctionInfo auction, List<BidHistoryEntry> newBids) private void MergeBidHistory(AuctionInfo auction, List<BidHistoryEntry> newBids)
{ {
@@ -940,7 +910,7 @@ namespace AutoBidder.Services
// ?? FIX: Usa lock per thread-safety // ?? FIX: Usa lock per thread-safety
lock (auction.RecentBids) lock (auction.RecentBids)
{ {
// Se la lista esistente è vuota, semplicemente copia le nuove // Se la lista esistente è vuota, semplicemente copia le nuove
if (auction.RecentBids.Count == 0) if (auction.RecentBids.Count == 0)
{ {
auction.RecentBids = newBids.ToList(); auction.RecentBids = newBids.ToList();
@@ -985,7 +955,7 @@ namespace AutoBidder.Services
.ThenByDescending(b => b.Price) .ThenByDescending(b => b.Price)
.ToList(); .ToList();
// Limita al numero massimo di puntate (mantieni le più recenti = prime della lista) // Limita al numero massimo di puntate (mantieni le più recenti = prime della lista)
if (maxEntries > 0 && auction.RecentBids.Count > maxEntries) if (maxEntries > 0 && auction.RecentBids.Count > maxEntries)
{ {
auction.RecentBids = auction.RecentBids auction.RecentBids = auction.RecentBids
@@ -1005,21 +975,74 @@ namespace AutoBidder.Services
} }
/// <summary> /// <summary>
/// Aggiorna le statistiche dei bidder basandosi sulla lista RecentBids (fonte ufficiale). /// Assicura che la puntata corrente (quella vincente) sia sempre presente nello storico.
/// Raggruppa le puntate per utente e conta il numero di puntate per ciascuno. /// Questo risolve il problema dell'API che a volte non include l'ultima puntata.
/// </summary>
private void EnsureCurrentBidInHistory(AuctionInfo auction, AuctionState state)
{
try
{
var statePrice = (decimal)state.Price;
var currentBidder = state.LastBidder;
// Verifica se questa puntata non è già presente
var alreadyExists = auction.RecentBids.Any(b =>
Math.Abs(b.Price - statePrice) < 0.001m &&
b.Username.Equals(currentBidder, StringComparison.OrdinalIgnoreCase));
if (!alreadyExists)
{
var lastBidTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
auction.RecentBids.Insert(0, new BidHistoryEntry
{
Username = currentBidder,
Price = statePrice,
Timestamp = lastBidTimestamp,
BidType = "Auto"
});
// Aggiorna anche le statistiche bidder
if (!auction.BidderStats.ContainsKey(currentBidder))
{
auction.BidderStats[currentBidder] = new BidderInfo
{
Username = currentBidder,
BidCount = 1,
RecentBidCount = 1,
LastBidTime = DateTime.UtcNow
};
}
else
{
var bidder = auction.BidderStats[currentBidder];
bidder.BidCount++;
bidder.RecentBidCount++;
bidder.LastBidTime = DateTime.UtcNow;
}
}
}
catch { /* Silenzioso */ }
}
/// <summary>
/// Aggiorna le statistiche dei bidder in modo CUMULATIVO.
/// BidCount è il totale dall'inizio del monitoraggio.
/// RecentBidCount è il conteggio nella finestra corrente di RecentBids.
/// I bidder NON vengono mai rimossi, il contatore è infinito.
/// </summary> /// </summary>
private void UpdateBidderStatsFromRecentBids(AuctionInfo auction) private void UpdateBidderStatsFromRecentBids(AuctionInfo auction)
{ {
try try
{ {
// Raggruppa puntate per username // Raggruppa puntate per username nella finestra corrente
var bidsByUser = auction.RecentBids var bidsByUser = auction.RecentBids
.GroupBy(b => b.Username, StringComparer.OrdinalIgnoreCase) .GroupBy(b => b.Username, StringComparer.OrdinalIgnoreCase)
.ToDictionary( .ToDictionary(
g => g.Key, g => g.Key,
g => new g => new
{ {
Count = g.Count(), RecentCount = g.Count(),
LastBidTime = DateTimeOffset.FromUnixTimeSeconds(g.Max(b => b.Timestamp)).DateTime LastBidTime = DateTimeOffset.FromUnixTimeSeconds(g.Max(b => b.Timestamp)).DateTime
}, },
StringComparer.OrdinalIgnoreCase StringComparer.OrdinalIgnoreCase
@@ -1033,36 +1056,37 @@ namespace AutoBidder.Services
if (!auction.BidderStats.ContainsKey(username)) if (!auction.BidderStats.ContainsKey(username))
{ {
// Nuovo bidder - inizializza con i conteggi attuali
auction.BidderStats[username] = new BidderInfo auction.BidderStats[username] = new BidderInfo
{ {
Username = username, Username = username,
BidCount = stats.Count, BidCount = stats.RecentCount, // Primo conteggio
RecentBidCount = stats.RecentCount,
LastBidTime = stats.LastBidTime LastBidTime = stats.LastBidTime
}; };
} }
else else
{ {
// Aggiorna statistiche esistenti
var existing = auction.BidderStats[username]; var existing = auction.BidderStats[username];
existing.BidCount = stats.Count;
// Calcola delta: quante nuove puntate rispetto all'ultimo check
int previousRecent = existing.RecentBidCount;
int currentRecent = stats.RecentCount;
// Se il conteggio è aumentato, aggiungi la differenza al totale cumulativo
if (currentRecent > previousRecent)
{
existing.BidCount += (currentRecent - previousRecent);
}
// Aggiorna conteggio finestra corrente e ultimo timestamp
existing.RecentBidCount = currentRecent;
existing.LastBidTime = stats.LastBidTime; existing.LastBidTime = stats.LastBidTime;
} }
} }
// Rimuovi bidder che non sono più in RecentBids // NON rimuovere i bidder che non sono più in RecentBids!
var usersInRecentBids = new HashSet<string>( // Il conteggio totale è cumulativo e persistente.
auction.RecentBids.Select(b => b.Username),
StringComparer.OrdinalIgnoreCase
);
var usersToRemove = auction.BidderStats.Keys
.Where(u => !usersInRecentBids.Contains(u))
.ToList();
foreach (var user in usersToRemove)
{
auction.BidderStats.Remove(user);
}
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -248,7 +248,39 @@ namespace AutoBidder.Services
return decision; return decision;
} }
// 1. Verifica soft retreat // ?? 1. ENTRY POINT - Verifica se il prezzo è conveniente
// Punta solo se prezzo < (MaxPrice * 0.7)
if (settings.EntryPointEnabled && auction.MaxPrice > 0)
{
var entryThreshold = auction.MaxPrice * 0.7;
if (state.Price >= entryThreshold)
{
decision.ShouldBid = false;
decision.Reason = $"Entry point: €{state.Price:F2} >= 70% di max €{auction.MaxPrice:F2}";
return decision;
}
}
// ?? 2. ANTI-BOT - Rileva pattern bot (timing identico)
if (settings.AntiBotDetectionEnabled && !string.IsNullOrEmpty(state.LastBidder))
{
var botCheck = DetectBotPattern(auction, state.LastBidder, currentUsername);
if (botCheck.IsBot)
{
decision.ShouldBid = false;
decision.Reason = $"Anti-bot: {state.LastBidder} pattern sospetto (var={botCheck.TimingVarianceMs:F0}ms)";
return decision;
}
}
// ?? 3. USER EXHAUSTION - Sfrutta utenti stanchi (info solo, non blocca)
if (settings.UserExhaustionEnabled && !string.IsNullOrEmpty(state.LastBidder))
{
var exhaustionCheck = CheckUserExhaustion(auction, state.LastBidder, currentUsername);
// Non blocchiamo, ma potremmo loggare per info
}
// 4. Verifica soft retreat
if (settings.SoftRetreatEnabled || (auction.SoftRetreatEnabledOverride ?? settings.SoftRetreatEnabled)) if (settings.SoftRetreatEnabled || (auction.SoftRetreatEnabledOverride ?? settings.SoftRetreatEnabled))
{ {
if (auction.IsInSoftRetreat) if (auction.IsInSoftRetreat)
@@ -338,6 +370,68 @@ namespace AutoBidder.Services
return decision; return decision;
} }
/// <summary>
/// Rileva pattern bot analizzando i delta timing degli ultimi bid
/// </summary>
private (bool IsBot, double TimingVarianceMs) DetectBotPattern(AuctionInfo auction, string? lastBidder, string currentUsername)
{
if (string.IsNullOrEmpty(lastBidder) || lastBidder.Equals(currentUsername, StringComparison.OrdinalIgnoreCase))
return (false, 999);
// Ottieni gli ultimi 3+ bid di questo utente
var userBids = auction.RecentBids
.Where(b => b.Username.Equals(lastBidder, StringComparison.OrdinalIgnoreCase))
.OrderByDescending(b => b.Timestamp)
.Take(4)
.ToList();
if (userBids.Count < 3)
return (false, 999);
// Calcola i delta tra bid consecutivi
var deltas = new List<long>();
for (int i = 0; i < userBids.Count - 1; i++)
{
deltas.Add(userBids[i].Timestamp - userBids[i + 1].Timestamp);
}
if (deltas.Count < 2)
return (false, 999);
// Calcola varianza dei delta
var avg = deltas.Average();
var variance = deltas.Sum(d => Math.Pow(d - avg, 2)) / deltas.Count;
var stdDev = Math.Sqrt(variance) * 1000; // Converti in ms
// Se la varianza è < 50ms, probabilmente è un bot
return (stdDev < 50, stdDev);
}
/// <summary>
/// Verifica se un utente è esausto (molte puntate, può mollare)
/// </summary>
private (bool ShouldExploit, string Reason) CheckUserExhaustion(AuctionInfo auction, string? lastBidder, string currentUsername)
{
if (string.IsNullOrEmpty(lastBidder) || lastBidder.Equals(currentUsername, StringComparison.OrdinalIgnoreCase))
return (false, "");
// Verifica se l'utente è un "heavy user" (>50 puntate totali)
if (auction.BidderStats.TryGetValue(lastBidder, out var stats))
{
if (stats.BidCount > 50)
{
// Se ci sono pochi altri bidder attivi, può essere un buon momento
var activeBidders = auction.BidderStats.Values.Count(b => b.BidCount > 5);
if (activeBidders <= 3)
{
return (true, $"{lastBidder} ha {stats.BidCount} puntate, potrebbe mollare");
}
}
}
return (false, "");
}
/// <summary> /// <summary>
/// Calcola probabilità di puntata basata su competizione e ROI /// Calcola probabilità di puntata basata su competizione e ROI
/// </summary> /// </summary>

View File

@@ -376,11 +376,11 @@ namespace AutoBidder.Services
if (nameMatch.Success) if (nameMatch.Success)
{ {
var name = System.Net.WebUtility.HtmlDecode(nameMatch.Groups[1].Value.Trim()); var name = System.Net.WebUtility.HtmlDecode(nameMatch.Groups[1].Value.Trim());
// ?? FIX: Sostituisci entità HTML non standard // ?? FIX: Sostituisci entità HTML non standard con +
name = name name = name
.Replace("&plus;", "+") .Replace("&plus;", "+")
.Replace("&amp;plus;", "+") .Replace("&amp;plus;", "+")
.Replace(" + ", " & "); .Replace("&amp;", "&"); // Decodifica & residui
auction.Name = name; auction.Name = name;
} }

View File

@@ -1,4 +1,4 @@
using System; using System;
using System.IO; using System.IO;
using System.Text.Json; using System.Text.Json;
@@ -49,7 +49,7 @@ namespace AutoBidder.Utilities
// ? NUOVO: LIMITE MINIMO PUNTATE // ? NUOVO: LIMITE MINIMO PUNTATE
/// <summary> /// <summary>
/// Numero minimo di puntate residue da mantenere sull'account. /// Numero minimo di puntate residue da mantenere sull'account.
/// Se impostato > 0, il sistema non punterà se le puntate residue scenderebbero sotto questa soglia. /// Se impostato > 0, il sistema non punterà se le puntate residue scenderebbero sotto questa soglia.
/// Default: 0 (nessun limite) /// Default: 0 (nessun limite)
/// </summary> /// </summary>
public int MinimumRemainingBids { get; set; } = 0; public int MinimumRemainingBids { get; set; } = 0;
@@ -89,13 +89,13 @@ namespace AutoBidder.Utilities
/// <summary> /// <summary>
/// Esegue pulizia automatica record incompleti all'avvio. /// Esegue pulizia automatica record incompleti all'avvio.
/// Default: false (può rimuovere dati utili in caso di errori temporanei) /// Default: false (può rimuovere dati utili in caso di errori temporanei)
/// </summary> /// </summary>
public bool DatabaseAutoCleanupIncomplete { get; set; } = false; public bool DatabaseAutoCleanupIncomplete { get; set; } = false;
/// <summary> /// <summary>
/// Numero massimo di giorni da mantenere nei risultati aste. /// Numero massimo di giorni da mantenere nei risultati aste.
/// Record più vecchi vengono eliminati automaticamente. /// Record più vecchi vengono eliminati automaticamente.
/// Default: 180 (6 mesi), 0 = disabilitato /// Default: 180 (6 mesi), 0 = disabilitato
/// </summary> /// </summary>
public int DatabaseMaxRetentionDays { get; set; } = 180; public int DatabaseMaxRetentionDays { get; set; } = 180;
@@ -113,19 +113,19 @@ namespace AutoBidder.Utilities
/// <summary> /// <summary>
/// Abilita jitter casuale sull'offset per evitare sincronizzazione con altri bot. /// Abilita jitter casuale sull'offset per evitare sincronizzazione con altri bot.
/// Aggiunge ±JitterRangeMs al timing di puntata. /// Aggiunge ±JitterRangeMs al timing di puntata.
/// Default: true /// Default: true
/// </summary> /// </summary>
public bool JitterEnabled { get; set; } = true; public bool JitterEnabled { get; set; } = true;
/// <summary> /// <summary>
/// Range massimo del jitter casuale in millisecondi (±X ms). /// Range massimo del jitter casuale in millisecondi (±X ms).
/// Default: 50 (range -50ms a +50ms) /// Default: 50 (range -50ms a +50ms)
/// </summary> /// </summary>
public int JitterRangeMs { get; set; } = 50; public int JitterRangeMs { get; set; } = 50;
/// <summary> /// <summary>
/// Abilita offset dinamico per asta basato su ping, storico e volatilità. /// Abilita offset dinamico per asta basato su ping, storico e volatilità.
/// Default: true /// Default: true
/// </summary> /// </summary>
public bool DynamicOffsetEnabled { get; set; } = true; public bool DynamicOffsetEnabled { get; set; } = true;
@@ -155,7 +155,7 @@ namespace AutoBidder.Utilities
public bool WaitForAutoBidEnabled { get; set; } = true; public bool WaitForAutoBidEnabled { get; set; } = true;
/// <summary> /// <summary>
/// Soglia in secondi sotto la quale si può puntare. /// Soglia in secondi sotto la quale si può puntare.
/// Bidoo attiva le auto-puntate a ~2 secondi, quindi aspettiamo che passino. /// Bidoo attiva le auto-puntate a ~2 secondi, quindi aspettiamo che passino.
/// Default: 1.8 (punta solo quando timer < 1.8s, dopo che le auto-puntate si sono attivate) /// Default: 1.8 (punta solo quando timer < 1.8s, dopo che le auto-puntate si sono attivate)
/// </summary> /// </summary>
@@ -167,9 +167,32 @@ namespace AutoBidder.Utilities
/// </summary> /// </summary>
public bool LogAutoBidWaitSkips { get; set; } = false; public bool LogAutoBidWaitSkips { get; set; } = false;
// ?????????????????????????????????????????????????????????????? // 🎯 STRATEGIE SEMPLIFICATE
/// <summary>
/// Entry Point: Punta solo se prezzo attuale è inferiore al 70% del MaxPrice.
/// Richiede che MaxPrice sia impostato sull'asta.
/// Default: true
/// </summary>
public bool EntryPointEnabled { get; set; } = true;
/// <summary>
/// Anti-Bot: Rileva pattern bot (timing identico con varianza minore di 50ms)
/// e evita di competere contro bot automatici.
/// Default: true
/// </summary>
public bool AntiBotDetectionEnabled { get; set; } = true;
/// <summary>
/// User Exhaustion: Sfrutta utenti stanchi (oltre 50 puntate)
/// quando ci sono pochi altri bidder attivi.
/// Default: true
/// </summary>
public bool UserExhaustionEnabled { get; set; } = true;
// 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
// RILEVAMENTO COMPETIZIONE E HEAT METRIC // RILEVAMENTO COMPETIZIONE E HEAT METRIC
// ?????????????????????????????????????????????????????????????? // 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
/// <summary> /// <summary>
/// Abilita rilevamento competizione e heat metric. /// Abilita rilevamento competizione e heat metric.
@@ -231,19 +254,19 @@ namespace AutoBidder.Utilities
/// <summary> /// <summary>
/// Abilita policy di puntata probabilistica. /// Abilita policy di puntata probabilistica.
/// Decide se puntare con probabilità p basata su competizione e ROI. /// Decide se puntare con probabilità p basata su competizione e ROI.
/// Default: false (richiede tuning) /// Default: false (richiede tuning)
/// </summary> /// </summary>
public bool ProbabilisticBiddingEnabled { get; set; } = false; public bool ProbabilisticBiddingEnabled { get; set; } = false;
/// <summary> /// <summary>
/// Probabilità base di puntata (0.0 - 1.0). /// Probabilità base di puntata (0.0 - 1.0).
/// Default: 0.8 (80%) /// Default: 0.8 (80%)
/// </summary> /// </summary>
public double BaseBidProbability { get; set; } = 0.8; public double BaseBidProbability { get; set; } = 0.8;
/// <summary> /// <summary>
/// Fattore di riduzione probabilità per ogni bidder attivo extra. /// Fattore di riduzione probabilità per ogni bidder attivo extra.
/// Default: 0.1 (riduce del 10% per ogni bidder oltre la soglia) /// Default: 0.1 (riduce del 10% per ogni bidder oltre la soglia)
/// </summary> /// </summary>
public double ProbabilityReductionPerBidder { get; set; } = 0.1; public double ProbabilityReductionPerBidder { get; set; } = 0.1;
@@ -274,7 +297,7 @@ namespace AutoBidder.Utilities
/// <summary> /// <summary>
/// Soglia percentuale per considerare un utente "aggressivo". /// Soglia percentuale per considerare un utente "aggressivo".
/// Se un utente ha più di X% delle puntate nella finestra, è aggressivo. /// Se un utente ha più di X% delle puntate nella finestra, è aggressivo.
/// Default: 40 (40% delle puntate) /// Default: 40 (40% delle puntate)
/// </summary> /// </summary>
public double AggressiveBidderPercentageThreshold { get; set; } = 40.0; public double AggressiveBidderPercentageThreshold { get; set; } = 40.0;
@@ -287,7 +310,7 @@ namespace AutoBidder.Utilities
/// <summary> /// <summary>
/// Azione da intraprendere con bidder aggressivi. /// Azione da intraprendere con bidder aggressivi.
/// "Avoid" = evita l'asta, "Compete" = continua normalmente, "Outbid" = punta più aggressivamente /// "Avoid" = evita l'asta, "Compete" = continua normalmente, "Outbid" = punta più aggressivamente
/// Default: "Compete" (cambiato da Avoid per essere meno restrittivo) /// Default: "Compete" (cambiato da Avoid per essere meno restrittivo)
/// </summary> /// </summary>
public string AggressiveBidderAction { get; set; } = "Compete"; public string AggressiveBidderAction { get; set; } = "Compete";
@@ -316,7 +339,7 @@ namespace AutoBidder.Utilities
/// <summary> /// <summary>
/// Budget massimo giornaliero in euro (0 = illimitato). /// Budget massimo giornaliero in euro (0 = illimitato).
/// Calcolato come: puntate usate × costo medio puntata. /// Calcolato come: puntate usate × costo medio puntata.
/// Default: 0 /// Default: 0
/// </summary> /// </summary>
public double DailyBudgetEuro { get; set; } = 0; public double DailyBudgetEuro { get; set; } = 0;

View File

@@ -299,17 +299,22 @@
transition: transform 0.3s ease, box-shadow 0.3s ease; transition: transform 0.3s ease, box-shadow 0.3s ease;
} }
/* ?? RIMOSSO: hover-lift causava movimento fastidioso */
.hover-lift:hover { .hover-lift:hover {
transform: translateY(-4px); /* transform: translateY(-4px); - RIMOSSO */
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
background-color: rgba(255, 255, 255, 0.05);
} }
/* ?? RIMOSSO: hover-scale causava zoom fastidioso */
.hover-scale { .hover-scale {
transition: transform 0.3s ease; transition: background-color 0.2s ease, border-color 0.2s ease;
} }
.hover-scale:hover { .hover-scale:hover {
transform: scale(1.05); /* transform: scale(1.05); - RIMOSSO */
background-color: rgba(255, 255, 255, 0.1);
border-color: rgba(13, 110, 253, 0.5);
} }
.hover-rotate { .hover-rotate {

View File

@@ -585,55 +585,67 @@ body {
.btn-success { .btn-success {
background: var(--success-color); background: var(--success-color);
color: white; color: white;
transition: filter 0.2s ease, box-shadow 0.2s ease;
} }
.btn-success:hover:not(:disabled) { .btn-success:hover:not(:disabled) {
background: #059669; filter: brightness(1.1);
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
} }
.btn-warning { .btn-warning {
background: var(--warning-color); background: var(--warning-color);
color: white; color: white;
transition: filter 0.2s ease, box-shadow 0.2s ease;
} }
.btn-warning:hover:not(:disabled) { .btn-warning:hover:not(:disabled) {
background: #d97706; filter: brightness(1.1);
box-shadow: 0 2px 8px rgba(245, 158, 11, 0.3);
} }
.btn-danger { .btn-danger {
background: var(--danger-color); background: var(--danger-color);
color: white; color: white;
transition: filter 0.2s ease, box-shadow 0.2s ease;
} }
.btn-danger:hover:not(:disabled) { .btn-danger:hover:not(:disabled) {
background: #dc2626; filter: brightness(1.1);
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3);
} }
.btn-primary { .btn-primary {
background: var(--primary-color); background: var(--primary-color);
color: white; color: white;
transition: filter 0.2s ease, box-shadow 0.2s ease;
} }
.btn-primary:hover:not(:disabled) { .btn-primary:hover:not(:disabled) {
background: #0284c7; filter: brightness(1.1);
box-shadow: 0 2px 8px rgba(14, 165, 233, 0.3);
} }
.btn-secondary { .btn-secondary {
background: var(--bg-hover); background: var(--bg-hover);
color: var(--text-secondary); color: var(--text-secondary);
transition: filter 0.2s ease, box-shadow 0.2s ease;
} }
.btn-secondary:hover:not(:disabled) { .btn-secondary:hover:not(:disabled) {
background: var(--text-muted); filter: brightness(1.15);
box-shadow: 0 2px 8px rgba(100, 116, 139, 0.2);
} }
.btn-info { .btn-info {
background: var(--info-color); background: var(--info-color);
color: white; color: white;
transition: filter 0.2s ease, box-shadow 0.2s ease;
} }
.btn-info:hover:not(:disabled) { .btn-info:hover:not(:disabled) {
background: #2563eb; filter: brightness(1.1);
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
} }
.btn:disabled { .btn:disabled {

View File

@@ -714,8 +714,9 @@ main {
height: 100%; height: 100%;
} }
/* 🔥 COMPATTATO: Ridotto padding per massimizzare spazio */
.tab-panel-content { .tab-panel-content {
padding: 1rem; padding: 0.5rem 0.75rem;
} }
/* === GRADIENTS FOR CARDS === */ /* === GRADIENTS FOR CARDS === */
@@ -883,24 +884,33 @@ main {
background: var(--bg-tertiary); background: var(--bg-tertiary);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 3px; border-radius: 3px;
padding: 0.75rem; padding: 0.5rem;
margin: 0.5rem; margin: 0.25rem;
} }
/* 🔥 COMPATTATO: Ridotto margin e padding per info-group */
.info-group { .info-group {
margin-bottom: 0.75rem; margin-bottom: 0.4rem;
} }
.info-group label { .info-group label {
display: block; display: block;
font-weight: 600; font-weight: 600;
margin-bottom: 0.25rem; margin-bottom: 0.15rem;
color: var(--text-secondary); color: var(--text-secondary);
font-size: 0.813rem; font-size: 0.75rem;
}
/* 🔥 COMPATTATO: Input più piccoli */
.info-group input.form-control,
.info-group select.form-control {
padding: 0.25rem 0.5rem;
font-size: 0.85rem;
height: auto;
} }
.auction-log, .bidders-stats { .auction-log, .bidders-stats {
margin: 0.5rem; margin: 0.25rem;
} }
.auction-log h4, .bidders-stats h4 { .auction-log h4, .bidders-stats h4 {

View File

@@ -28,6 +28,29 @@
white-space: nowrap; white-space: nowrap;
} }
/* 🔥 Header ordinabili */
.sortable-header {
cursor: pointer;
user-select: none;
transition: background-color 0.2s ease;
}
.sortable-header:hover {
background-color: rgba(13, 110, 253, 0.15);
}
/* 🎯 Evidenziazione riga utente corrente */
.my-bid-row {
background-color: rgba(40, 167, 69, 0.2) !important;
border-left: 3px solid #28a745;
font-weight: 500;
}
.my-bid-row:hover {
background-color: rgba(40, 167, 69, 0.3) !important;
}
.page-header { .page-header {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -249,6 +272,7 @@
border: 1px solid var(--border-color) !important; border: 1px solid var(--border-color) !important;
color: var(--text-secondary) !important; color: var(--text-secondary) !important;
border-radius: var(--radius-md) !important; border-radius: var(--radius-md) !important;
transition: filter 0.2s ease, background-color 0.2s ease;
} }
.settings-container .btn-outline-secondary:hover { .settings-container .btn-outline-secondary:hover {
@@ -256,6 +280,36 @@
color: var(--text-primary) !important; color: var(--text-primary) !important;
} }
/* 🎨 Stili hover moderni per pulsanti outline */
.btn-outline-primary,
.btn-outline-secondary,
.btn-outline-success,
.btn-outline-danger,
.btn-outline-warning,
.btn-outline-info {
transition: filter 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease;
}
.btn-outline-primary:hover:not(:disabled) {
filter: brightness(1.05);
box-shadow: 0 2px 6px rgba(13, 110, 253, 0.2);
}
.btn-outline-success:hover:not(:disabled) {
filter: brightness(1.05);
box-shadow: 0 2px 6px rgba(25, 135, 84, 0.2);
}
.btn-outline-danger:hover:not(:disabled) {
filter: brightness(1.05);
box-shadow: 0 2px 6px rgba(220, 53, 69, 0.2);
}
.btn-outline-warning:hover:not(:disabled) {
filter: brightness(1.05);
box-shadow: 0 2px 6px rgba(255, 193, 7, 0.2);
}
/* === AUCTION BROWSER STYLES === */ /* === AUCTION BROWSER STYLES === */
.browser-container { .browser-container {