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]
public AuctionState? LastState { get; set; }
/// <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>
/// <param name="message">Messaggio da aggiungere al log</param>
/// <param name="maxLines">Numero massimo di righe da mantenere (default: 500)</param>
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);
// Mantieni solo gli ultimi maxLines log
if (AuctionLog.Count > maxLines)
{
// Rimuovi i log più vecchi per mantenere la dimensione sotto controllo
int excessCount = AuctionLog.Count - maxLines;
AuctionLog.RemoveRange(0, excessCount);
}

View File

@@ -3,12 +3,25 @@ using System;
namespace AutoBidder.Models
{
/// <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>
public class BidderInfo
{
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;
/// <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 string LastBidTimeDisplay => LastBidTime == DateTime.MinValue

View File

@@ -63,6 +63,9 @@
<button class="btn btn-secondary hover-lift" @onclick="RemoveSelectedAuction" disabled="@(selectedAuction == null)">
<i class="bi bi-trash"></i> Rimuovi
</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)">
<i class="bi bi-trash-fill"></i> Rimuovi Tutte
</button>
@@ -85,18 +88,18 @@
<table class="table table-striped table-hover mb-0 table-fixed">
<thead>
<tr>
<th class="col-stato"><i class="bi bi-toggle-on"></i> Stato</th>
<th class="col-nome"><i class="bi bi-tag"></i> Nome</th>
<th class="col-prezzo"><i class="bi bi-currency-euro"></i> Prezzo</th>
<th class="col-timer"><i class="bi bi-clock"></i> Timer</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 sortable-header" @onclick='() => SortAuctionsBy("nome")'><i class="bi bi-tag"></i> Nome @GetSortIndicator("nome")</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 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-click"><i class="bi bi-hand-index" style="font-size: 0.85rem;"></i> Puntate</th>
<th class="col-ping"><i class="bi bi-speedometer"></i> Ping</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 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>
</tr>
</thead>
<tbody>
@foreach (var auction in auctions)
@foreach (var auction in GetSortedAuctions())
{
<tr class="@GetRowClass(auction) @(selectedAuction == auction ? "selected-row" : "") table-row-enter transition-all"
@onclick="() => SelectAuction(auction)"
@@ -387,9 +390,24 @@
<div class="tab-pane fade" id="content-history" role="tabpanel">
<div class="tab-panel-content">
@{
// ?? FIX: Rimuovi duplicati consecutivi (stesso prezzo + stesso utente)
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;
}
@if (recentBidsList.Any())
filteredBids.Add(bid);
lastBid = bid;
}
}
@if (filteredBids.Any())
{
<div class="table-responsive">
<table class="table table-sm table-striped">
@@ -402,15 +420,19 @@
</tr>
</thead>
<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>
@bid.Username
@if (bid.IsMyBid)
{
<strong class="text-success">@bid.Username</strong>
<span class="badge bg-success ms-1">TU</span>
}
else
{
@bid.Username
}
</td>
<td class="fw-bold">€@bid.PriceFormatted</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-panel-content">
@{
// Crea una copia thread-safe per evitare modifiche durante l'enumerazione
var recentBidsCopy = GetRecentBidsSafe(selectedAuction);
// ?? FIX: Usa BidderStats che contiene i conteggi CUMULATIVI (non limitati)
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 currentUsername = GetCurrentUsername();
}
@if (recentBidsCopy.Any())
@if (bidderStatsCopy.Any())
{
// Calcola statistiche puntatori
var bidderStats = recentBidsCopy
.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();
// Calcola il totale CUMULATIVO
var totalBidsCumulative = bidderStatsCopy.Sum(b => b.BidCount);
// Ricalcola il totale includendo il conteggio corretto per l'utente
var totalBids = bidderStats.Sum(b => b.Count);
// Correggi il conteggio per l'utente corrente se disponibile
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">
<table class="table table-sm table-striped">
@@ -469,25 +491,32 @@
</tr>
</thead>
<tbody>
@for (int i = 0; i < bidderStats.Count; i++)
@for (int i = 0; i < bidderStatsCopy.Count; i++)
{
var bidder = bidderStats[i];
var percentage = (bidder.Count * 100.0 / totalBids);
<tr class="@(bidder.IsMe ? "table-success" : "")">
var bidder = bidderStatsCopy[i];
var isMe = bidder.Username.Equals(currentUsername, StringComparison.OrdinalIgnoreCase);
// 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>
@bidder.Username
@if (bidder.IsMe)
@if (isMe)
{
<span class="badge bg-success ms-1">TU</span>
}
</td>
<td class="fw-bold">@bidder.Count</td>
<td class="fw-bold">@displayCount</td>
<td>
<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"
style="width: @percentage.ToString("F1")%">
style="width: @percentage.ToString("F1", System.Globalization.CultureInfo.InvariantCulture)%">
@percentage.ToString("F1")%
</div>
</div>

View File

@@ -28,6 +28,7 @@ namespace AutoBidder.Pages
set => AppState.IsMonitoringActive = value;
}
private System.Threading.Timer? refreshTimer;
private System.Threading.Timer? sessionTimer;
@@ -51,6 +52,10 @@ namespace AutoBidder.Pages
private ElementReference globalLogRef;
private int lastLogCount = 0;
// ?? Sorting griglia aste
private string auctionSortColumn = "nome";
private bool auctionSortAscending = true;
protected override void OnInitialized()
{
// Sottoscrivi agli eventi del servizio di stato (ASYNC)
@@ -137,7 +142,8 @@ namespace AutoBidder.Pages
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");
}
@@ -299,6 +305,11 @@ namespace AutoBidder.Pages
{
auction.BidsUsedOnThisAuction = result.BidsUsedOnThisAuction.Value;
}
else
{
// Incrementa contatore locale se server non risponde
auction.BidsUsedOnThisAuction = (auction.BidsUsedOnThisAuction ?? 0) + 1;
}
SaveAuctions();
}
@@ -619,6 +630,61 @@ namespace AutoBidder.Pages
}
}
/// <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()
{
if (selectedAuction == null) return;
@@ -909,6 +975,57 @@ namespace AutoBidder.Pages
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
private string GetTotalCostDisplay(AuctionInfo? auction)

View File

@@ -1,4 +1,5 @@
using AutoBidder.Models;
using AutoBidder.Utilities;
using System;
using System.Collections.Generic;
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
{
get

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@@ -98,7 +98,7 @@ namespace AutoBidder.Services
if (!_auctions.Any(a => a.AuctionId == auction.AuctionId))
{
_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})");
}
}
@@ -111,20 +111,20 @@ namespace AutoBidder.Services
var auction = _auctions.FirstOrDefault(a => a.AuctionId == auctionId);
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)
{
OnLog?.Invoke($"[STATS] Asta terminata rilevata: {auction.Name} - Salvataggio statistiche in corso...");
try
{
// Determina se è stata vinta dall'utente
// Determina se è stata vinta dall'utente
var won = IsAuctionWonByUser(auction);
OnLog?.Invoke($"[STATS] Asta {auction.Name} - Stato: {(won ? "VINTA" : "PERSA")}");
// Emetti evento per salvare le statistiche
// Questo trigger sarà gestito in Program.cs con scraping HTML
// Questo trigger sarà gestito in Program.cs con scraping HTML
OnAuctionCompleted?.Invoke(auction, auction.LastState, won);
}
catch (Exception ex)
@@ -143,7 +143,7 @@ namespace AutoBidder.Services
}
/// <summary>
/// Determina se l'asta è stata vinta dall'utente corrente
/// Determina se l'asta è stata vinta dall'utente corrente
/// </summary>
private bool IsAuctionWonByUser(AuctionInfo auction)
{
@@ -154,7 +154,7 @@ namespace AutoBidder.Services
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;
}
@@ -288,7 +288,7 @@ namespace AutoBidder.Services
continue;
}
// Delay adattivo OTTIMIZZATO basato su timer più basso
// Delay adattivo OTTIMIZZATO basato su timer più basso
var lowestTimer = activeAuctions
.Select(a => GetLastTimer(a))
.Where(t => t > 0)
@@ -355,7 +355,7 @@ namespace AutoBidder.Services
// ?? Aggiorna latenza con storico
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)
{
auction.IsTrackedFromStart = true;
@@ -368,6 +368,13 @@ namespace AutoBidder.Services
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 ||
state.Status == AuctionStatus.EndedLost ||
state.Status == AuctionStatus.Closed)
@@ -384,7 +391,7 @@ namespace AutoBidder.Services
var lastBidTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
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 =>
Math.Abs(b.Price - statePrice) < 0.001m &&
b.Username.Equals(state.LastBidder, StringComparison.OrdinalIgnoreCase));
@@ -399,7 +406,7 @@ namespace AutoBidder.Services
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)
{
// 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
}
else if (state.Status == AuctionStatus.Paused)
@@ -502,8 +509,9 @@ namespace AutoBidder.Services
}
/// <summary>
/// Strategia di puntata ottimizzata con BidStrategyService
/// Usa: adaptive latency, jitter, dynamic offset, heat metric, competition detection
/// Strategia di puntata SEMPLIFICATA
/// Le strategie restituiscono solo un booleano (posso puntare?).
/// Il timing è sempre fisso: offset globale o override per asta.
/// </summary>
private async Task ExecuteBidStrategy(AuctionInfo auction, AuctionState state, CancellationToken token)
{
@@ -512,21 +520,22 @@ namespace AutoBidder.Services
// Calcola il tempo rimanente in millisecondi
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)
{
return;
}
// ?? AGGIORNA METRICHE (solo se strategie avanzate abilitate)
if (auction.AdvancedStrategiesEnabled != false)
{
// 🎯 OFFSET FISSO: Usa override asta se impostato, altrimenti globale
int fixedOffsetMs = auction.BidBeforeDeadlineMs > 0
? auction.BidBeforeDeadlineMs
: settings.DefaultBidBeforeDeadlineMs;
// 🧠 VALUTAZIONE STRATEGIE (restituiscono solo bool)
var session = _apiClient.GetSession();
var currentUsername = session?.Username ?? "";
_bidStrategy.UpdateHeatMetric(auction, settings, currentUsername);
// Verifica strategie avanzate (soft retreat, competition, probabilistic, etc.)
// Verifica se le strategie permettono di puntare
var decision = _bidStrategy.ShouldPlaceBid(auction, state, settings, currentUsername);
if (!decision.ShouldBid)
@@ -534,36 +543,23 @@ namespace AutoBidder.Services
auction.AddLog($"[STRATEGY] {decision.Reason}");
return;
}
}
// ?? CALCOLA TIMING OTTIMALE
var timing = _bidStrategy.CalculateOptimalTiming(auction, settings);
int effectiveOffset = timing.FinalOffsetMs;
// ?? TIMER-BASED SCHEDULING
if (timerMs > effectiveOffset)
// 🕐 TIMER-BASED SCHEDULING (offset fisso, niente calcoli dinamici)
if (timerMs > fixedOffsetMs)
{
// Timer ancora alto ? Schedula puntata futura
double delayMs = timerMs - effectiveOffset;
// Timer ancora alto - Schedula puntata futura
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)
{
return; // Task già schedulato
return;
}
auction.IsAttackInProgress = true;
auction.LastUsedOffsetMs = effectiveOffset;
auction.LastUsedOffsetMs = fixedOffsetMs;
// Log con dettagli timing (solo se logging avanzato)
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)");
}
auction.AddLog($"[TIMING] Timer={timerMs:F0}ms - Puntata tra {delayMs:F0}ms (offset fisso={fixedOffsetMs}ms)");
// Avvia task asincrono che attende e poi punta
_ = Task.Run(async () =>
@@ -576,42 +572,22 @@ namespace AutoBidder.Services
// Verifica che l'asta sia ancora attiva e non in pausa
if (!auction.IsActive || auction.IsPaused || token.IsCancellationRequested)
{
auction.AddLog($"[STRATEGIA] Task annullato (asta inattiva/pausa)");
auction.AddLog($"[TIMING] Task annullato (asta inattiva/pausa)");
return;
}
// Verifica soft retreat
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!");
auction.AddLog($"[TIMING] Eseguo puntata!");
// Esegui la puntata
await ExecuteBid(auction, state, token);
}
catch (OperationCanceledException)
{
auction.AddLog($"[STRATEGIA] Task cancellato");
auction.AddLog($"[TIMING] Task cancellato");
}
catch (Exception ex)
{
auction.AddLog($"[STRATEGIA ERROR] {ex.Message}");
auction.AddLog($"[TIMING ERROR] {ex.Message}");
}
finally
{
@@ -619,33 +595,20 @@ namespace AutoBidder.Services
}
}, 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)
{
return; // Già in corso
return;
}
auction.IsAttackInProgress = true;
auction.LastUsedOffsetMs = effectiveOffset;
auction.LastUsedOffsetMs = fixedOffsetMs;
try
{
auction.AddLog($"[STRATEGIA] Timer già in finestra ({timerMs:F0}ms <= {effectiveOffset}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;
}
}
auction.AddLog($"[TIMING] Timer in finestra ({timerMs:F0}ms <= {fixedOffsetMs}ms) - PUNTA SUBITO!");
// Esegui la puntata
await ExecuteBid(auction, state, token);
@@ -655,7 +618,7 @@ namespace AutoBidder.Services
auction.IsAttackInProgress = false;
}
}
// Se timer <= 0, asta già scaduta ? Non fare nulla
// Se timer <= 0, asta già scaduta - Non fare nulla
}
/// <summary>
@@ -678,17 +641,24 @@ namespace AutoBidder.Services
_bidStrategy.RecordTimerExpired(auction);
}
// Aggiorna dati puntate da risposta server
// 🔥 FIX: Aggiorna contatore puntate
if (result.Success)
{
if (result.RemainingBids.HasValue)
{
auction.RemainingBids = result.RemainingBids.Value;
}
// Usa valore server se disponibile, altrimenti incrementa localmente
if (result.BidsUsedOnThisAuction.HasValue)
{
auction.BidsUsedOnThisAuction = result.BidsUsedOnThisAuction.Value;
}
else
{
// Incrementa contatore locale
auction.BidsUsedOnThisAuction = (auction.BidsUsedOnThisAuction ?? 0) + 1;
}
}
OnBidExecuted?.Invoke(auction, result);
@@ -727,11 +697,11 @@ namespace AutoBidder.Services
{
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.
// Aspettiamo che il timer scenda sotto la soglia per lasciare che
// 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)
{
// 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)
// IMPORTANTE: Applica solo se BuyNowPrice è valido (> 0)
// IMPORTANTE: Applica solo se BuyNowPrice è valido (> 0)
// Se BuyNowPrice == 0, significa errore scraping - non bloccare le puntate
if (auction.BuyNowPrice.HasValue &&
auction.BuyNowPrice.Value > 0 &&
@@ -751,7 +721,7 @@ namespace AutoBidder.Services
auction.CalculatedValue.Savings.HasValue &&
!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%)
if (auction.CalculatedValue.SavingsPercentage.HasValue &&
auction.CalculatedValue.SavingsPercentage.Value < -5)
@@ -780,7 +750,7 @@ namespace AutoBidder.Services
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 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)
{
return false;
@@ -814,13 +784,13 @@ namespace AutoBidder.Services
// ?? CONTROLLO 3: MinPrice/MaxPrice
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;
}
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;
}
@@ -926,8 +896,8 @@ namespace AutoBidder.Services
/// <summary>
/// Unisce la storia puntate ricevuta dall'API con quella esistente,
/// mantenendo le puntate più vecchie e aggiungendo solo le nuove.
/// Le puntate sono ordinate con le più RECENTI in CIMA.
/// mantenendo le puntate più vecchie e aggiungendo solo le nuove.
/// Le puntate sono ordinate con le più RECENTI in CIMA.
/// </summary>
private void MergeBidHistory(AuctionInfo auction, List<BidHistoryEntry> newBids)
{
@@ -940,7 +910,7 @@ namespace AutoBidder.Services
// ?? FIX: Usa lock per thread-safety
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)
{
auction.RecentBids = newBids.ToList();
@@ -985,7 +955,7 @@ namespace AutoBidder.Services
.ThenByDescending(b => b.Price)
.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)
{
auction.RecentBids = auction.RecentBids
@@ -1005,21 +975,74 @@ namespace AutoBidder.Services
}
/// <summary>
/// Aggiorna le statistiche dei bidder basandosi sulla lista RecentBids (fonte ufficiale).
/// Raggruppa le puntate per utente e conta il numero di puntate per ciascuno.
/// Assicura che la puntata corrente (quella vincente) sia sempre presente nello storico.
/// 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>
private void UpdateBidderStatsFromRecentBids(AuctionInfo auction)
{
try
{
// Raggruppa puntate per username
// Raggruppa puntate per username nella finestra corrente
var bidsByUser = auction.RecentBids
.GroupBy(b => b.Username, StringComparer.OrdinalIgnoreCase)
.ToDictionary(
g => g.Key,
g => new
{
Count = g.Count(),
RecentCount = g.Count(),
LastBidTime = DateTimeOffset.FromUnixTimeSeconds(g.Max(b => b.Timestamp)).DateTime
},
StringComparer.OrdinalIgnoreCase
@@ -1033,36 +1056,37 @@ namespace AutoBidder.Services
if (!auction.BidderStats.ContainsKey(username))
{
// Nuovo bidder - inizializza con i conteggi attuali
auction.BidderStats[username] = new BidderInfo
{
Username = username,
BidCount = stats.Count,
BidCount = stats.RecentCount, // Primo conteggio
RecentBidCount = stats.RecentCount,
LastBidTime = stats.LastBidTime
};
}
else
{
// Aggiorna statistiche esistenti
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;
}
}
// Rimuovi bidder che non sono più in RecentBids
var usersInRecentBids = new HashSet<string>(
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);
}
// NON rimuovere i bidder che non sono più in RecentBids!
// Il conteggio totale è cumulativo e persistente.
}
catch (Exception ex)
{

View File

@@ -248,7 +248,39 @@ namespace AutoBidder.Services
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 (auction.IsInSoftRetreat)
@@ -338,6 +370,68 @@ namespace AutoBidder.Services
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>
/// Calcola probabilità di puntata basata su competizione e ROI
/// </summary>

View File

@@ -376,11 +376,11 @@ namespace AutoBidder.Services
if (nameMatch.Success)
{
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
.Replace("&plus;", "+")
.Replace("&amp;plus;", "+")
.Replace(" + ", " & ");
.Replace("&amp;", "&"); // Decodifica & residui
auction.Name = name;
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.IO;
using System.Text.Json;
@@ -49,7 +49,7 @@ namespace AutoBidder.Utilities
// ? NUOVO: LIMITE MINIMO PUNTATE
/// <summary>
/// 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)
/// </summary>
public int MinimumRemainingBids { get; set; } = 0;
@@ -89,13 +89,13 @@ namespace AutoBidder.Utilities
/// <summary>
/// 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>
public bool DatabaseAutoCleanupIncomplete { get; set; } = false;
/// <summary>
/// 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
/// </summary>
public int DatabaseMaxRetentionDays { get; set; } = 180;
@@ -113,19 +113,19 @@ namespace AutoBidder.Utilities
/// <summary>
/// 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
/// </summary>
public bool JitterEnabled { get; set; } = true;
/// <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)
/// </summary>
public int JitterRangeMs { get; set; } = 50;
/// <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
/// </summary>
public bool DynamicOffsetEnabled { get; set; } = true;
@@ -155,7 +155,7 @@ namespace AutoBidder.Utilities
public bool WaitForAutoBidEnabled { get; set; } = true;
/// <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.
/// Default: 1.8 (punta solo quando timer < 1.8s, dopo che le auto-puntate si sono attivate)
/// </summary>
@@ -167,9 +167,32 @@ namespace AutoBidder.Utilities
/// </summary>
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
// ??????????????????????????????????????????????????????????????
// 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
/// <summary>
/// Abilita rilevamento competizione e heat metric.
@@ -231,19 +254,19 @@ namespace AutoBidder.Utilities
/// <summary>
/// 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)
/// </summary>
public bool ProbabilisticBiddingEnabled { get; set; } = false;
/// <summary>
/// Probabilità base di puntata (0.0 - 1.0).
/// Probabilità base di puntata (0.0 - 1.0).
/// Default: 0.8 (80%)
/// </summary>
public double BaseBidProbability { get; set; } = 0.8;
/// <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)
/// </summary>
public double ProbabilityReductionPerBidder { get; set; } = 0.1;
@@ -274,7 +297,7 @@ namespace AutoBidder.Utilities
/// <summary>
/// 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)
/// </summary>
public double AggressiveBidderPercentageThreshold { get; set; } = 40.0;
@@ -287,7 +310,7 @@ namespace AutoBidder.Utilities
/// <summary>
/// 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)
/// </summary>
public string AggressiveBidderAction { get; set; } = "Compete";
@@ -316,7 +339,7 @@ namespace AutoBidder.Utilities
/// <summary>
/// Budget massimo giornaliero in euro (0 = illimitato).
/// Calcolato come: puntate usate × costo medio puntata.
/// Calcolato come: puntate usate × costo medio puntata.
/// Default: 0
/// </summary>
public double DailyBudgetEuro { get; set; } = 0;

View File

@@ -299,17 +299,22 @@
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
/* ?? RIMOSSO: hover-lift causava movimento fastidioso */
.hover-lift:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
/* transform: translateY(-4px); - RIMOSSO */
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 {
transition: transform 0.3s ease;
transition: background-color 0.2s ease, border-color 0.2s ease;
}
.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 {

View File

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

View File

@@ -714,8 +714,9 @@ main {
height: 100%;
}
/* 🔥 COMPATTATO: Ridotto padding per massimizzare spazio */
.tab-panel-content {
padding: 1rem;
padding: 0.5rem 0.75rem;
}
/* === GRADIENTS FOR CARDS === */
@@ -883,24 +884,33 @@ main {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 3px;
padding: 0.75rem;
margin: 0.5rem;
padding: 0.5rem;
margin: 0.25rem;
}
/* 🔥 COMPATTATO: Ridotto margin e padding per info-group */
.info-group {
margin-bottom: 0.75rem;
margin-bottom: 0.4rem;
}
.info-group label {
display: block;
font-weight: 600;
margin-bottom: 0.25rem;
margin-bottom: 0.15rem;
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 {
margin: 0.5rem;
margin: 0.25rem;
}
.auction-log h4, .bidders-stats h4 {

View File

@@ -28,6 +28,29 @@
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 {
display: flex;
align-items: center;
@@ -249,6 +272,7 @@
border: 1px solid var(--border-color) !important;
color: var(--text-secondary) !important;
border-radius: var(--radius-md) !important;
transition: filter 0.2s ease, background-color 0.2s ease;
}
.settings-container .btn-outline-secondary:hover {
@@ -256,6 +280,36 @@
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 === */
.browser-container {