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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
filteredBids.Add(bid);
|
||||
lastBid = bid;
|
||||
}
|
||||
}
|
||||
@if (recentBidsList.Any())
|
||||
@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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,58 +520,46 @@ 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 ?? "";
|
||||
|
||||
// Verifica se le strategie permettono di puntare
|
||||
var decision = _bidStrategy.ShouldPlaceBid(auction, state, settings, currentUsername);
|
||||
|
||||
if (!decision.ShouldBid)
|
||||
{
|
||||
var session = _apiClient.GetSession();
|
||||
var currentUsername = session?.Username ?? "";
|
||||
|
||||
_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;
|
||||
}
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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("+", "+")
|
||||
.Replace("&plus;", "+")
|
||||
.Replace(" + ", " & ");
|
||||
.Replace("&", "&"); // Decodifica & residui
|
||||
auction.Name = name;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user