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]
|
[JsonIgnore]
|
||||||
public AuctionState? LastState { get; set; }
|
public AuctionState? LastState { get; set; }
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Aggiunge una voce al log dell'asta con limite automatico di righe
|
/// Aggiunge una voce al log dell'asta con deduplicazione e limite automatico di righe.
|
||||||
|
/// Se il messaggio è identico all'ultimo, incrementa un contatore invece di duplicare.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">Messaggio da aggiungere al log</param>
|
/// <param name="message">Messaggio da aggiungere al log</param>
|
||||||
/// <param name="maxLines">Numero massimo di righe da mantenere (default: 500)</param>
|
/// <param name="maxLines">Numero massimo di righe da mantenere (default: 500)</param>
|
||||||
public void AddLog(string message, int maxLines = 500)
|
public void AddLog(string message, int maxLines = 500)
|
||||||
{
|
{
|
||||||
var entry = $"{DateTime.Now:HH:mm:ss.fff} - {message}";
|
var timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
|
||||||
|
|
||||||
|
// ?? DEDUPLICAZIONE: Se l'ultimo messaggio è uguale, incrementa contatore
|
||||||
|
if (AuctionLog.Count > 0)
|
||||||
|
{
|
||||||
|
var lastEntry = AuctionLog[^1]; // Ultimo elemento
|
||||||
|
|
||||||
|
// Estrai il messaggio senza timestamp e contatore
|
||||||
|
var lastMessageStart = lastEntry.IndexOf(" - ");
|
||||||
|
if (lastMessageStart > 0)
|
||||||
|
{
|
||||||
|
var lastMessage = lastEntry.Substring(lastMessageStart + 3);
|
||||||
|
|
||||||
|
// Rimuovi eventuale contatore esistente (es: " (x5)")
|
||||||
|
var counterMatch = System.Text.RegularExpressions.Regex.Match(lastMessage, @" \(x(\d+)\)$");
|
||||||
|
if (counterMatch.Success)
|
||||||
|
{
|
||||||
|
lastMessage = lastMessage.Substring(0, lastMessage.Length - counterMatch.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se il messaggio è identico, aggiorna contatore
|
||||||
|
if (lastMessage == message)
|
||||||
|
{
|
||||||
|
int newCount = counterMatch.Success
|
||||||
|
? int.Parse(counterMatch.Groups[1].Value) + 1
|
||||||
|
: 2;
|
||||||
|
|
||||||
|
// Aggiorna l'ultimo entry con il nuovo contatore
|
||||||
|
AuctionLog[^1] = $"{timestamp} - {message} (x{newCount})";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nuovo messaggio diverso dall'ultimo
|
||||||
|
var entry = $"{timestamp} - {message}";
|
||||||
AuctionLog.Add(entry);
|
AuctionLog.Add(entry);
|
||||||
|
|
||||||
// Mantieni solo gli ultimi maxLines log
|
// Mantieni solo gli ultimi maxLines log
|
||||||
if (AuctionLog.Count > maxLines)
|
if (AuctionLog.Count > maxLines)
|
||||||
{
|
{
|
||||||
// Rimuovi i log più vecchi per mantenere la dimensione sotto controllo
|
|
||||||
int excessCount = AuctionLog.Count - maxLines;
|
int excessCount = AuctionLog.Count - maxLines;
|
||||||
AuctionLog.RemoveRange(0, excessCount);
|
AuctionLog.RemoveRange(0, excessCount);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,25 @@ using System;
|
|||||||
namespace AutoBidder.Models
|
namespace AutoBidder.Models
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Informazioni su un utente che ha piazzato puntate
|
/// Informazioni su un utente che ha piazzato puntate.
|
||||||
|
/// Il conteggio è CUMULATIVO dall'inizio del monitoraggio (non limitato come RecentBids).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class BidderInfo
|
public class BidderInfo
|
||||||
{
|
{
|
||||||
public string Username { get; set; } = "";
|
public string Username { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Conteggio CUMULATIVO delle puntate dall'inizio del monitoraggio.
|
||||||
|
/// Questo valore non viene mai decrementato anche se RecentBids viene troncato.
|
||||||
|
/// </summary>
|
||||||
public int BidCount { get; set; } = 0;
|
public int BidCount { get; set; } = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Conteggio puntate visibili nell'attuale finestra RecentBids (per UI).
|
||||||
|
/// Può essere inferiore a BidCount se RecentBids è stato troncato.
|
||||||
|
/// </summary>
|
||||||
|
public int RecentBidCount { get; set; } = 0;
|
||||||
|
|
||||||
public DateTime LastBidTime { get; set; } = DateTime.MinValue;
|
public DateTime LastBidTime { get; set; } = DateTime.MinValue;
|
||||||
|
|
||||||
public string LastBidTimeDisplay => LastBidTime == DateTime.MinValue
|
public string LastBidTimeDisplay => LastBidTime == DateTime.MinValue
|
||||||
|
|||||||
@@ -63,6 +63,9 @@
|
|||||||
<button class="btn btn-secondary hover-lift" @onclick="RemoveSelectedAuction" disabled="@(selectedAuction == null)">
|
<button class="btn btn-secondary hover-lift" @onclick="RemoveSelectedAuction" disabled="@(selectedAuction == null)">
|
||||||
<i class="bi bi-trash"></i> Rimuovi
|
<i class="bi bi-trash"></i> Rimuovi
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-outline-warning hover-lift" @onclick="RemoveCompletedAuctions" disabled="@(!HasCompletedAuctions())" title="Rimuovi aste terminate (vengono salvate nel database)">
|
||||||
|
<i class="bi bi-check2-all"></i> Rimuovi Terminate
|
||||||
|
</button>
|
||||||
<button class="btn btn-outline-danger hover-lift" @onclick="RemoveAllAuctions" disabled="@(auctions.Count == 0)" title="Rimuovi tutte le aste (quelle terminate verranno salvate)">
|
<button class="btn btn-outline-danger hover-lift" @onclick="RemoveAllAuctions" disabled="@(auctions.Count == 0)" title="Rimuovi tutte le aste (quelle terminate verranno salvate)">
|
||||||
<i class="bi bi-trash-fill"></i> Rimuovi Tutte
|
<i class="bi bi-trash-fill"></i> Rimuovi Tutte
|
||||||
</button>
|
</button>
|
||||||
@@ -85,18 +88,18 @@
|
|||||||
<table class="table table-striped table-hover mb-0 table-fixed">
|
<table class="table table-striped table-hover mb-0 table-fixed">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="col-stato"><i class="bi bi-toggle-on"></i> Stato</th>
|
<th class="col-stato sortable-header" @onclick='() => SortAuctionsBy("stato")'><i class="bi bi-toggle-on"></i> Stato @GetSortIndicator("stato")</th>
|
||||||
<th class="col-nome"><i class="bi bi-tag"></i> Nome</th>
|
<th class="col-nome sortable-header" @onclick='() => SortAuctionsBy("nome")'><i class="bi bi-tag"></i> Nome @GetSortIndicator("nome")</th>
|
||||||
<th class="col-prezzo"><i class="bi bi-currency-euro"></i> Prezzo</th>
|
<th class="col-prezzo sortable-header" @onclick='() => SortAuctionsBy("prezzo")'><i class="bi bi-currency-euro"></i> Prezzo @GetSortIndicator("prezzo")</th>
|
||||||
<th class="col-timer"><i class="bi bi-clock"></i> Timer</th>
|
<th class="col-timer sortable-header" @onclick='() => SortAuctionsBy("timer")'><i class="bi bi-clock"></i> Timer @GetSortIndicator("timer")</th>
|
||||||
<th class="col-ultimo"><i class="bi bi-person"></i> Ultimo</th>
|
<th class="col-ultimo"><i class="bi bi-person"></i> Ultimo</th>
|
||||||
<th class="col-click"><i class="bi bi-hand-index" style="font-size: 0.85rem;"></i> Puntate</th>
|
<th class="col-click sortable-header" @onclick='() => SortAuctionsBy("puntate")'><i class="bi bi-hand-index" style="font-size: 0.85rem;"></i> Puntate @GetSortIndicator("puntate")</th>
|
||||||
<th class="col-ping"><i class="bi bi-speedometer"></i> Ping</th>
|
<th class="col-ping sortable-header" @onclick='() => SortAuctionsBy("ping")'><i class="bi bi-speedometer"></i> Ping @GetSortIndicator("ping")</th>
|
||||||
<th class="col-azioni"><i class="bi bi-gear"></i> Azioni</th>
|
<th class="col-azioni"><i class="bi bi-gear"></i> Azioni</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach (var auction in auctions)
|
@foreach (var auction in GetSortedAuctions())
|
||||||
{
|
{
|
||||||
<tr class="@GetRowClass(auction) @(selectedAuction == auction ? "selected-row" : "") table-row-enter transition-all"
|
<tr class="@GetRowClass(auction) @(selectedAuction == auction ? "selected-row" : "") table-row-enter transition-all"
|
||||||
@onclick="() => SelectAuction(auction)"
|
@onclick="() => SelectAuction(auction)"
|
||||||
@@ -387,9 +390,24 @@
|
|||||||
<div class="tab-pane fade" id="content-history" role="tabpanel">
|
<div class="tab-pane fade" id="content-history" role="tabpanel">
|
||||||
<div class="tab-panel-content">
|
<div class="tab-panel-content">
|
||||||
@{
|
@{
|
||||||
|
// ?? FIX: Rimuovi duplicati consecutivi (stesso prezzo + stesso utente)
|
||||||
var recentBidsList = GetRecentBidsSafe(selectedAuction);
|
var recentBidsList = GetRecentBidsSafe(selectedAuction);
|
||||||
|
var filteredBids = new List<BidHistoryEntry>();
|
||||||
|
BidHistoryEntry? lastBid = null;
|
||||||
|
foreach (var bid in recentBidsList)
|
||||||
|
{
|
||||||
|
// Salta se è un duplicato del precedente (stesso prezzo E stesso utente)
|
||||||
|
if (lastBid != null &&
|
||||||
|
Math.Abs(bid.Price - lastBid.Price) < 0.001m &&
|
||||||
|
bid.Username.Equals(lastBid.Username, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
filteredBids.Add(bid);
|
||||||
|
lastBid = bid;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@if (recentBidsList.Any())
|
@if (filteredBids.Any())
|
||||||
{
|
{
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-sm table-striped">
|
<table class="table table-sm table-striped">
|
||||||
@@ -402,15 +420,19 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach (var bid in recentBidsList.Take(50))
|
@foreach (var bid in filteredBids.Take(50))
|
||||||
{
|
{
|
||||||
<tr class="@(bid.IsMyBid ? "table-success" : "")">
|
<tr class="@(bid.IsMyBid ? "my-bid-row" : "")">
|
||||||
<td>
|
<td>
|
||||||
@bid.Username
|
|
||||||
@if (bid.IsMyBid)
|
@if (bid.IsMyBid)
|
||||||
{
|
{
|
||||||
|
<strong class="text-success">@bid.Username</strong>
|
||||||
<span class="badge bg-success ms-1">TU</span>
|
<span class="badge bg-success ms-1">TU</span>
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@bid.Username
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
<td class="fw-bold">€@bid.PriceFormatted</td>
|
<td class="fw-bold">€@bid.PriceFormatted</td>
|
||||||
<td class="text-muted small">@bid.TimeFormatted</td>
|
<td class="text-muted small">@bid.TimeFormatted</td>
|
||||||
@@ -434,29 +456,29 @@
|
|||||||
<div class="tab-pane fade" id="content-bidders" role="tabpanel">
|
<div class="tab-pane fade" id="content-bidders" role="tabpanel">
|
||||||
<div class="tab-panel-content">
|
<div class="tab-panel-content">
|
||||||
@{
|
@{
|
||||||
// Crea una copia thread-safe per evitare modifiche durante l'enumerazione
|
// ?? FIX: Usa BidderStats che contiene i conteggi CUMULATIVI (non limitati)
|
||||||
var recentBidsCopy = GetRecentBidsSafe(selectedAuction);
|
var bidderStatsCopy = selectedAuction.BidderStats
|
||||||
|
.Values
|
||||||
|
.OrderByDescending(b => b.BidCount)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
// ?? FIX: Per l'utente corrente, usa BidsUsedOnThisAuction (valore ufficiale dal server)
|
// Per l'utente corrente, usa BidsUsedOnThisAuction (valore ufficiale dal server)
|
||||||
var myOfficialBidsCount = selectedAuction.BidsUsedOnThisAuction ?? 0;
|
var myOfficialBidsCount = selectedAuction.BidsUsedOnThisAuction ?? 0;
|
||||||
var currentUsername = GetCurrentUsername();
|
var currentUsername = GetCurrentUsername();
|
||||||
}
|
}
|
||||||
@if (recentBidsCopy.Any())
|
@if (bidderStatsCopy.Any())
|
||||||
{
|
{
|
||||||
// Calcola statistiche puntatori
|
// Calcola il totale CUMULATIVO
|
||||||
var bidderStats = recentBidsCopy
|
var totalBidsCumulative = bidderStatsCopy.Sum(b => b.BidCount);
|
||||||
.GroupBy(b => b.Username)
|
|
||||||
.Select(g => new {
|
|
||||||
Username = g.Key,
|
|
||||||
// Per l'utente corrente usa il conteggio ufficiale, per gli altri conta dalla lista
|
|
||||||
Count = g.First().IsMyBid && myOfficialBidsCount > 0 ? myOfficialBidsCount : g.Count(),
|
|
||||||
IsMe = g.First().IsMyBid
|
|
||||||
})
|
|
||||||
.OrderByDescending(s => s.Count)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
// Ricalcola il totale includendo il conteggio corretto per l'utente
|
// Correggi il conteggio per l'utente corrente se disponibile
|
||||||
var totalBids = bidderStats.Sum(b => b.Count);
|
var myBidder = bidderStatsCopy.FirstOrDefault(b =>
|
||||||
|
b.Username.Equals(currentUsername, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (myBidder != null && myOfficialBidsCount > myBidder.BidCount)
|
||||||
|
{
|
||||||
|
// Usa il valore ufficiale se maggiore
|
||||||
|
totalBidsCumulative = totalBidsCumulative - myBidder.BidCount + myOfficialBidsCount;
|
||||||
|
}
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-sm table-striped">
|
<table class="table table-sm table-striped">
|
||||||
@@ -469,25 +491,32 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@for (int i = 0; i < bidderStats.Count; i++)
|
@for (int i = 0; i < bidderStatsCopy.Count; i++)
|
||||||
{
|
{
|
||||||
var bidder = bidderStats[i];
|
var bidder = bidderStatsCopy[i];
|
||||||
var percentage = (bidder.Count * 100.0 / totalBids);
|
var isMe = bidder.Username.Equals(currentUsername, StringComparison.OrdinalIgnoreCase);
|
||||||
<tr class="@(bidder.IsMe ? "table-success" : "")">
|
// Per l'utente corrente usa il conteggio ufficiale
|
||||||
|
var displayCount = isMe && myOfficialBidsCount > bidder.BidCount
|
||||||
|
? myOfficialBidsCount
|
||||||
|
: bidder.BidCount;
|
||||||
|
var percentage = totalBidsCumulative > 0
|
||||||
|
? (displayCount * 100.0 / totalBidsCumulative)
|
||||||
|
: 0;
|
||||||
|
<tr class="@(isMe ? "table-success" : "")">
|
||||||
<td><span class="badge bg-primary">#@(i + 1)</span></td>
|
<td><span class="badge bg-primary">#@(i + 1)</span></td>
|
||||||
<td>
|
<td>
|
||||||
@bidder.Username
|
@bidder.Username
|
||||||
@if (bidder.IsMe)
|
@if (isMe)
|
||||||
{
|
{
|
||||||
<span class="badge bg-success ms-1">TU</span>
|
<span class="badge bg-success ms-1">TU</span>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td class="fw-bold">@bidder.Count</td>
|
<td class="fw-bold">@displayCount</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="progress" style="height: 20px;">
|
<div class="progress" style="height: 20px;">
|
||||||
<div class="progress-bar @(bidder.IsMe ? "bg-success" : "bg-primary")"
|
<div class="progress-bar @(isMe ? "bg-success" : "bg-primary")"
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
style="width: @percentage.ToString("F1")%">
|
style="width: @percentage.ToString("F1", System.Globalization.CultureInfo.InvariantCulture)%">
|
||||||
@percentage.ToString("F1")%
|
@percentage.ToString("F1")%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ namespace AutoBidder.Pages
|
|||||||
set => AppState.IsMonitoringActive = value;
|
set => AppState.IsMonitoringActive = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private System.Threading.Timer? refreshTimer;
|
private System.Threading.Timer? refreshTimer;
|
||||||
private System.Threading.Timer? sessionTimer;
|
private System.Threading.Timer? sessionTimer;
|
||||||
|
|
||||||
@@ -51,6 +52,10 @@ namespace AutoBidder.Pages
|
|||||||
private ElementReference globalLogRef;
|
private ElementReference globalLogRef;
|
||||||
private int lastLogCount = 0;
|
private int lastLogCount = 0;
|
||||||
|
|
||||||
|
// ?? Sorting griglia aste
|
||||||
|
private string auctionSortColumn = "nome";
|
||||||
|
private bool auctionSortAscending = true;
|
||||||
|
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
// Sottoscrivi agli eventi del servizio di stato (ASYNC)
|
// Sottoscrivi agli eventi del servizio di stato (ASYNC)
|
||||||
@@ -137,7 +142,8 @@ namespace AutoBidder.Pages
|
|||||||
|
|
||||||
private void SaveAuctions()
|
private void SaveAuctions()
|
||||||
{
|
{
|
||||||
AutoBidder.Utilities.PersistenceManager.SaveAuctions(auctions);
|
// ?? FIX: Usa il metodo dedicato che salva la lista originale, non una copia
|
||||||
|
AppState.PersistAuctions();
|
||||||
AddLog("Aste salvate");
|
AddLog("Aste salvate");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,6 +305,11 @@ namespace AutoBidder.Pages
|
|||||||
{
|
{
|
||||||
auction.BidsUsedOnThisAuction = result.BidsUsedOnThisAuction.Value;
|
auction.BidsUsedOnThisAuction = result.BidsUsedOnThisAuction.Value;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Incrementa contatore locale se server non risponde
|
||||||
|
auction.BidsUsedOnThisAuction = (auction.BidsUsedOnThisAuction ?? 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
SaveAuctions();
|
SaveAuctions();
|
||||||
}
|
}
|
||||||
@@ -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()
|
private async Task RemoveSelectedAuctionWithConfirm()
|
||||||
{
|
{
|
||||||
if (selectedAuction == null) return;
|
if (selectedAuction == null) return;
|
||||||
@@ -909,6 +975,57 @@ namespace AutoBidder.Pages
|
|||||||
return sessionUsername ?? "";
|
return sessionUsername ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ?? SORTING GRIGLIA ASTE
|
||||||
|
|
||||||
|
private void SortAuctionsBy(string column)
|
||||||
|
{
|
||||||
|
if (auctionSortColumn == column)
|
||||||
|
{
|
||||||
|
auctionSortAscending = !auctionSortAscending;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
auctionSortColumn = column;
|
||||||
|
auctionSortAscending = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MarkupString GetSortIndicator(string column)
|
||||||
|
{
|
||||||
|
if (auctionSortColumn != column) return new MarkupString("");
|
||||||
|
return new MarkupString(auctionSortAscending ? " ?" : " ?");
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<AuctionInfo> GetSortedAuctions()
|
||||||
|
{
|
||||||
|
var list = auctions.AsEnumerable();
|
||||||
|
|
||||||
|
list = auctionSortColumn switch
|
||||||
|
{
|
||||||
|
"stato" => auctionSortAscending
|
||||||
|
? list.OrderBy(a => a.IsActive).ThenBy(a => a.IsPaused)
|
||||||
|
: list.OrderByDescending(a => a.IsActive).ThenByDescending(a => a.IsPaused),
|
||||||
|
"nome" => auctionSortAscending
|
||||||
|
? list.OrderBy(a => a.Name)
|
||||||
|
: list.OrderByDescending(a => a.Name),
|
||||||
|
"prezzo" => auctionSortAscending
|
||||||
|
? list.OrderBy(a => a.LastState?.Price ?? 0)
|
||||||
|
: list.OrderByDescending(a => a.LastState?.Price ?? 0),
|
||||||
|
"timer" => auctionSortAscending
|
||||||
|
? list.OrderBy(a => a.LastState?.Timer ?? 999)
|
||||||
|
: list.OrderByDescending(a => a.LastState?.Timer ?? 999),
|
||||||
|
"puntate" => auctionSortAscending
|
||||||
|
? list.OrderBy(a => a.BidsUsedOnThisAuction ?? 0)
|
||||||
|
: list.OrderByDescending(a => a.BidsUsedOnThisAuction ?? 0),
|
||||||
|
"ping" => auctionSortAscending
|
||||||
|
? list.OrderBy(a => a.PollingLatencyMs)
|
||||||
|
: list.OrderByDescending(a => a.PollingLatencyMs),
|
||||||
|
_ => list
|
||||||
|
};
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
// ?? NUOVI METODI: Visualizzazione valori prodotto
|
// ?? NUOVI METODI: Visualizzazione valori prodotto
|
||||||
|
|
||||||
private string GetTotalCostDisplay(AuctionInfo? auction)
|
private string GetTotalCostDisplay(AuctionInfo? auction)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using AutoBidder.Models;
|
using AutoBidder.Models;
|
||||||
|
using AutoBidder.Utilities;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -52,6 +53,29 @@ namespace AutoBidder.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ottiene la lista originale delle aste per il salvataggio.
|
||||||
|
/// ATTENZIONE: Usare solo per persistenza, non per iterazione durante modifiche!
|
||||||
|
/// </summary>
|
||||||
|
public List<AuctionInfo> GetAuctionsForPersistence()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return _auctions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Forza il salvataggio delle aste correnti su disco.
|
||||||
|
/// </summary>
|
||||||
|
public void PersistAuctions()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
AutoBidder.Utilities.PersistenceManager.SaveAuctions(_auctions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public AuctionInfo? SelectedAuction
|
public AuctionInfo? SelectedAuction
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -98,7 +98,7 @@ namespace AutoBidder.Services
|
|||||||
if (!_auctions.Any(a => a.AuctionId == auction.AuctionId))
|
if (!_auctions.Any(a => a.AuctionId == auction.AuctionId))
|
||||||
{
|
{
|
||||||
_auctions.Add(auction);
|
_auctions.Add(auction);
|
||||||
// ? RIMOSSO: Log ridondante - viene già loggato da MainWindow con defaults e stato
|
// ? RIMOSSO: Log ridondante - viene già loggato da MainWindow con defaults e stato
|
||||||
// OnLog?.Invoke($"[+] Asta aggiunta: {auction.Name} (ID: {auction.AuctionId})");
|
// OnLog?.Invoke($"[+] Asta aggiunta: {auction.Name} (ID: {auction.AuctionId})");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,20 +111,20 @@ namespace AutoBidder.Services
|
|||||||
var auction = _auctions.FirstOrDefault(a => a.AuctionId == auctionId);
|
var auction = _auctions.FirstOrDefault(a => a.AuctionId == auctionId);
|
||||||
if (auction != null)
|
if (auction != null)
|
||||||
{
|
{
|
||||||
// ?? Se l'asta è terminata, salva le statistiche prima di rimuoverla
|
// ?? Se l'asta è terminata, salva le statistiche prima di rimuoverla
|
||||||
if (!auction.IsActive && auction.LastState != null)
|
if (!auction.IsActive && auction.LastState != null)
|
||||||
{
|
{
|
||||||
OnLog?.Invoke($"[STATS] Asta terminata rilevata: {auction.Name} - Salvataggio statistiche in corso...");
|
OnLog?.Invoke($"[STATS] Asta terminata rilevata: {auction.Name} - Salvataggio statistiche in corso...");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Determina se è stata vinta dall'utente
|
// Determina se è stata vinta dall'utente
|
||||||
var won = IsAuctionWonByUser(auction);
|
var won = IsAuctionWonByUser(auction);
|
||||||
|
|
||||||
OnLog?.Invoke($"[STATS] Asta {auction.Name} - Stato: {(won ? "VINTA" : "PERSA")}");
|
OnLog?.Invoke($"[STATS] Asta {auction.Name} - Stato: {(won ? "VINTA" : "PERSA")}");
|
||||||
|
|
||||||
// Emetti evento per salvare le statistiche
|
// Emetti evento per salvare le statistiche
|
||||||
// Questo trigger sarà gestito in Program.cs con scraping HTML
|
// Questo trigger sarà gestito in Program.cs con scraping HTML
|
||||||
OnAuctionCompleted?.Invoke(auction, auction.LastState, won);
|
OnAuctionCompleted?.Invoke(auction, auction.LastState, won);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -143,7 +143,7 @@ namespace AutoBidder.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determina se l'asta è stata vinta dall'utente corrente
|
/// Determina se l'asta è stata vinta dall'utente corrente
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private bool IsAuctionWonByUser(AuctionInfo auction)
|
private bool IsAuctionWonByUser(AuctionInfo auction)
|
||||||
{
|
{
|
||||||
@@ -154,7 +154,7 @@ namespace AutoBidder.Services
|
|||||||
|
|
||||||
if (string.IsNullOrEmpty(username)) return false;
|
if (string.IsNullOrEmpty(username)) return false;
|
||||||
|
|
||||||
// Controlla se l'ultimo puntatore è l'utente
|
// Controlla se l'ultimo puntatore è l'utente
|
||||||
return auction.LastState.LastBidder?.Equals(username, StringComparison.OrdinalIgnoreCase) == true;
|
return auction.LastState.LastBidder?.Equals(username, StringComparison.OrdinalIgnoreCase) == true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,7 +288,7 @@ namespace AutoBidder.Services
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delay adattivo OTTIMIZZATO basato su timer più basso
|
// Delay adattivo OTTIMIZZATO basato su timer più basso
|
||||||
var lowestTimer = activeAuctions
|
var lowestTimer = activeAuctions
|
||||||
.Select(a => GetLastTimer(a))
|
.Select(a => GetLastTimer(a))
|
||||||
.Where(t => t > 0)
|
.Where(t => t > 0)
|
||||||
@@ -355,7 +355,7 @@ namespace AutoBidder.Services
|
|||||||
// ?? Aggiorna latenza con storico
|
// ?? Aggiorna latenza con storico
|
||||||
auction.AddLatencyMeasurement(state.PollingLatencyMs);
|
auction.AddLatencyMeasurement(state.PollingLatencyMs);
|
||||||
|
|
||||||
// ?? Segna tracking dall'inizio se è la prima volta
|
// ?? Segna tracking dall'inizio se è la prima volta
|
||||||
if (!auction.IsTrackedFromStart && auction.BidHistory.Count == 0)
|
if (!auction.IsTrackedFromStart && auction.BidHistory.Count == 0)
|
||||||
{
|
{
|
||||||
auction.IsTrackedFromStart = true;
|
auction.IsTrackedFromStart = true;
|
||||||
@@ -368,6 +368,13 @@ namespace AutoBidder.Services
|
|||||||
MergeBidHistory(auction, state.RecentBidsHistory);
|
MergeBidHistory(auction, state.RecentBidsHistory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔥 FIX: Aggiungi SEMPRE l'ultima puntata corrente allo storico (durante il monitoraggio)
|
||||||
|
// Questo assicura che la puntata vincente sia sempre inclusa
|
||||||
|
if (!string.IsNullOrEmpty(state.LastBidder) && state.Price > 0)
|
||||||
|
{
|
||||||
|
EnsureCurrentBidInHistory(auction, state);
|
||||||
|
}
|
||||||
|
|
||||||
if (state.Status == AuctionStatus.EndedWon ||
|
if (state.Status == AuctionStatus.EndedWon ||
|
||||||
state.Status == AuctionStatus.EndedLost ||
|
state.Status == AuctionStatus.EndedLost ||
|
||||||
state.Status == AuctionStatus.Closed)
|
state.Status == AuctionStatus.Closed)
|
||||||
@@ -384,7 +391,7 @@ namespace AutoBidder.Services
|
|||||||
var lastBidTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
var lastBidTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
var statePrice = (decimal)state.Price;
|
var statePrice = (decimal)state.Price;
|
||||||
|
|
||||||
// Verifica se questa puntata non è già presente
|
// Verifica se questa puntata non è già presente
|
||||||
var alreadyExists = auction.RecentBids.Any(b =>
|
var alreadyExists = auction.RecentBids.Any(b =>
|
||||||
Math.Abs(b.Price - statePrice) < 0.001m &&
|
Math.Abs(b.Price - statePrice) < 0.001m &&
|
||||||
b.Username.Equals(state.LastBidder, StringComparison.OrdinalIgnoreCase));
|
b.Username.Equals(state.LastBidder, StringComparison.OrdinalIgnoreCase));
|
||||||
@@ -399,7 +406,7 @@ namespace AutoBidder.Services
|
|||||||
BidType = "Auto"
|
BidType = "Auto"
|
||||||
});
|
});
|
||||||
|
|
||||||
auction.AddLog($"[FIX] Aggiunta ultima puntata mancante: {state.LastBidder} €{state.Price:F2}");
|
auction.AddLog($"[FIX] Aggiunta ultima puntata mancante: {state.LastBidder} €{state.Price:F2}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,7 +443,7 @@ namespace AutoBidder.Services
|
|||||||
|
|
||||||
if (state.Status == AuctionStatus.Running)
|
if (state.Status == AuctionStatus.Running)
|
||||||
{
|
{
|
||||||
// Log RIMOSSO per ridurre verbosità - polling continuo non necessita log
|
// Log RIMOSSO per ridurre verbosità - polling continuo non necessita log
|
||||||
// Solo eventi importanti (bid, reset, errori) vengono loggati
|
// Solo eventi importanti (bid, reset, errori) vengono loggati
|
||||||
}
|
}
|
||||||
else if (state.Status == AuctionStatus.Paused)
|
else if (state.Status == AuctionStatus.Paused)
|
||||||
@@ -502,8 +509,9 @@ namespace AutoBidder.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Strategia di puntata ottimizzata con BidStrategyService
|
/// Strategia di puntata SEMPLIFICATA
|
||||||
/// Usa: adaptive latency, jitter, dynamic offset, heat metric, competition detection
|
/// Le strategie restituiscono solo un booleano (posso puntare?).
|
||||||
|
/// Il timing è sempre fisso: offset globale o override per asta.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task ExecuteBidStrategy(AuctionInfo auction, AuctionState state, CancellationToken token)
|
private async Task ExecuteBidStrategy(AuctionInfo auction, AuctionState state, CancellationToken token)
|
||||||
{
|
{
|
||||||
@@ -512,58 +520,46 @@ namespace AutoBidder.Services
|
|||||||
// Calcola il tempo rimanente in millisecondi
|
// Calcola il tempo rimanente in millisecondi
|
||||||
double timerMs = state.Timer * 1000;
|
double timerMs = state.Timer * 1000;
|
||||||
|
|
||||||
// ??? CONTROLLO: Se sono già il vincitore, non fare nulla
|
// 🛑 CONTROLLO: Se sono già il vincitore, non fare nulla
|
||||||
if (state.IsMyBid)
|
if (state.IsMyBid)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ?? AGGIORNA METRICHE (solo se strategie avanzate abilitate)
|
// 🎯 OFFSET FISSO: Usa override asta se impostato, altrimenti globale
|
||||||
if (auction.AdvancedStrategiesEnabled != false)
|
int fixedOffsetMs = auction.BidBeforeDeadlineMs > 0
|
||||||
|
? auction.BidBeforeDeadlineMs
|
||||||
|
: settings.DefaultBidBeforeDeadlineMs;
|
||||||
|
|
||||||
|
// 🧠 VALUTAZIONE STRATEGIE (restituiscono solo bool)
|
||||||
|
var session = _apiClient.GetSession();
|
||||||
|
var currentUsername = session?.Username ?? "";
|
||||||
|
|
||||||
|
// Verifica se le strategie permettono di puntare
|
||||||
|
var decision = _bidStrategy.ShouldPlaceBid(auction, state, settings, currentUsername);
|
||||||
|
|
||||||
|
if (!decision.ShouldBid)
|
||||||
{
|
{
|
||||||
var session = _apiClient.GetSession();
|
auction.AddLog($"[STRATEGY] {decision.Reason}");
|
||||||
var currentUsername = session?.Username ?? "";
|
return;
|
||||||
|
|
||||||
_bidStrategy.UpdateHeatMetric(auction, settings, currentUsername);
|
|
||||||
|
|
||||||
// Verifica strategie avanzate (soft retreat, competition, probabilistic, etc.)
|
|
||||||
var decision = _bidStrategy.ShouldPlaceBid(auction, state, settings, currentUsername);
|
|
||||||
|
|
||||||
if (!decision.ShouldBid)
|
|
||||||
{
|
|
||||||
auction.AddLog($"[STRATEGY] {decision.Reason}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ?? CALCOLA TIMING OTTIMALE
|
// 🕐 TIMER-BASED SCHEDULING (offset fisso, niente calcoli dinamici)
|
||||||
var timing = _bidStrategy.CalculateOptimalTiming(auction, settings);
|
if (timerMs > fixedOffsetMs)
|
||||||
int effectiveOffset = timing.FinalOffsetMs;
|
|
||||||
|
|
||||||
// ?? TIMER-BASED SCHEDULING
|
|
||||||
if (timerMs > effectiveOffset)
|
|
||||||
{
|
{
|
||||||
// Timer ancora alto ? Schedula puntata futura
|
// Timer ancora alto - Schedula puntata futura
|
||||||
double delayMs = timerMs - effectiveOffset;
|
double delayMs = timerMs - fixedOffsetMs;
|
||||||
|
|
||||||
// Non schedulare se già c'è un task attivo per questa asta
|
// Non schedulare se già c'è un task attivo per questa asta
|
||||||
if (auction.IsAttackInProgress)
|
if (auction.IsAttackInProgress)
|
||||||
{
|
{
|
||||||
return; // Task già schedulato
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auction.IsAttackInProgress = true;
|
auction.IsAttackInProgress = true;
|
||||||
auction.LastUsedOffsetMs = effectiveOffset;
|
auction.LastUsedOffsetMs = fixedOffsetMs;
|
||||||
|
|
||||||
// Log con dettagli timing (solo se logging avanzato)
|
auction.AddLog($"[TIMING] Timer={timerMs:F0}ms - Puntata tra {delayMs:F0}ms (offset fisso={fixedOffsetMs}ms)");
|
||||||
if (settings.AdvancedLoggingEnabled)
|
|
||||||
{
|
|
||||||
auction.AddLog($"[TIMING] Timer={timerMs:F0}ms, Offset={effectiveOffset}ms (base={timing.BaseOffsetMs}+lat={timing.LatencyCompensationMs}+dyn={timing.DynamicAdjustmentMs}+jit={timing.JitterMs}) ? Delay={delayMs:F0}ms");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
auction.AddLog($"[STRATEGIA] Timer={timerMs:F0}ms ? Puntata tra {delayMs:F0}ms (offset={effectiveOffset}ms)");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Avvia task asincrono che attende e poi punta
|
// Avvia task asincrono che attende e poi punta
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
@@ -576,42 +572,22 @@ namespace AutoBidder.Services
|
|||||||
// Verifica che l'asta sia ancora attiva e non in pausa
|
// Verifica che l'asta sia ancora attiva e non in pausa
|
||||||
if (!auction.IsActive || auction.IsPaused || token.IsCancellationRequested)
|
if (!auction.IsActive || auction.IsPaused || token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
auction.AddLog($"[STRATEGIA] Task annullato (asta inattiva/pausa)");
|
auction.AddLog($"[TIMING] Task annullato (asta inattiva/pausa)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verifica soft retreat
|
auction.AddLog($"[TIMING] Eseguo puntata!");
|
||||||
if (auction.IsInSoftRetreat)
|
|
||||||
{
|
|
||||||
auction.AddLog($"[STRATEGIA] Task annullato (soft retreat attivo)");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Controlla se qualcun altro ha puntato di recente
|
|
||||||
var lastBidTime = GetLastBidTime(auction, state.LastBidder);
|
|
||||||
if (lastBidTime.HasValue)
|
|
||||||
{
|
|
||||||
var timeSinceLastBid = DateTime.UtcNow - lastBidTime.Value;
|
|
||||||
if (timeSinceLastBid.TotalMilliseconds < 500)
|
|
||||||
{
|
|
||||||
auction.AddLog($"[COLLISION] Puntata recente di {state.LastBidder} ({timeSinceLastBid.TotalMilliseconds:F0}ms fa)");
|
|
||||||
_bidStrategy.RecordBidAttempt(auction, false, collision: true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auction.AddLog($"[STRATEGIA] Task eseguito ? PUNTA ORA!");
|
|
||||||
|
|
||||||
// Esegui la puntata
|
// Esegui la puntata
|
||||||
await ExecuteBid(auction, state, token);
|
await ExecuteBid(auction, state, token);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
auction.AddLog($"[STRATEGIA] Task cancellato");
|
auction.AddLog($"[TIMING] Task cancellato");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
auction.AddLog($"[STRATEGIA ERROR] {ex.Message}");
|
auction.AddLog($"[TIMING ERROR] {ex.Message}");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -619,33 +595,20 @@ namespace AutoBidder.Services
|
|||||||
}
|
}
|
||||||
}, token);
|
}, token);
|
||||||
}
|
}
|
||||||
else if (timerMs > 0 && timerMs <= effectiveOffset)
|
else if (timerMs > 0 && timerMs <= fixedOffsetMs)
|
||||||
{
|
{
|
||||||
// Timer già nella finestra ? Punta SUBITO senza delay
|
// Timer già nella finestra - Punta SUBITO senza delay
|
||||||
if (auction.IsAttackInProgress)
|
if (auction.IsAttackInProgress)
|
||||||
{
|
{
|
||||||
return; // Già in corso
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auction.IsAttackInProgress = true;
|
auction.IsAttackInProgress = true;
|
||||||
auction.LastUsedOffsetMs = effectiveOffset;
|
auction.LastUsedOffsetMs = fixedOffsetMs;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
auction.AddLog($"[STRATEGIA] Timer già in finestra ({timerMs:F0}ms <= {effectiveOffset}ms) ? PUNTA SUBITO!");
|
auction.AddLog($"[TIMING] Timer in finestra ({timerMs:F0}ms <= {fixedOffsetMs}ms) - PUNTA SUBITO!");
|
||||||
|
|
||||||
// Controlla se qualcun altro ha puntato di recente
|
|
||||||
var lastBidTime = GetLastBidTime(auction, state.LastBidder);
|
|
||||||
if (lastBidTime.HasValue)
|
|
||||||
{
|
|
||||||
var timeSinceLastBid = DateTime.UtcNow - lastBidTime.Value;
|
|
||||||
if (timeSinceLastBid.TotalMilliseconds < 500)
|
|
||||||
{
|
|
||||||
auction.AddLog($"[COLLISION] Puntata recente di {state.LastBidder} ({timeSinceLastBid.TotalMilliseconds:F0}ms fa)");
|
|
||||||
_bidStrategy.RecordBidAttempt(auction, false, collision: true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Esegui la puntata
|
// Esegui la puntata
|
||||||
await ExecuteBid(auction, state, token);
|
await ExecuteBid(auction, state, token);
|
||||||
@@ -655,7 +618,7 @@ namespace AutoBidder.Services
|
|||||||
auction.IsAttackInProgress = false;
|
auction.IsAttackInProgress = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Se timer <= 0, asta già scaduta ? Non fare nulla
|
// Se timer <= 0, asta già scaduta - Non fare nulla
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -678,17 +641,24 @@ namespace AutoBidder.Services
|
|||||||
_bidStrategy.RecordTimerExpired(auction);
|
_bidStrategy.RecordTimerExpired(auction);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aggiorna dati puntate da risposta server
|
// 🔥 FIX: Aggiorna contatore puntate
|
||||||
if (result.Success)
|
if (result.Success)
|
||||||
{
|
{
|
||||||
if (result.RemainingBids.HasValue)
|
if (result.RemainingBids.HasValue)
|
||||||
{
|
{
|
||||||
auction.RemainingBids = result.RemainingBids.Value;
|
auction.RemainingBids = result.RemainingBids.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Usa valore server se disponibile, altrimenti incrementa localmente
|
||||||
if (result.BidsUsedOnThisAuction.HasValue)
|
if (result.BidsUsedOnThisAuction.HasValue)
|
||||||
{
|
{
|
||||||
auction.BidsUsedOnThisAuction = result.BidsUsedOnThisAuction.Value;
|
auction.BidsUsedOnThisAuction = result.BidsUsedOnThisAuction.Value;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Incrementa contatore locale
|
||||||
|
auction.BidsUsedOnThisAuction = (auction.BidsUsedOnThisAuction ?? 0) + 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
OnBidExecuted?.Invoke(auction, result);
|
OnBidExecuted?.Invoke(auction, result);
|
||||||
@@ -727,11 +697,11 @@ namespace AutoBidder.Services
|
|||||||
{
|
{
|
||||||
var settings = Utilities.SettingsManager.Load();
|
var settings = Utilities.SettingsManager.Load();
|
||||||
|
|
||||||
// ?? CONTROLLO ANTI-AUTOBID BIDOO (PRIORITÀ MASSIMA)
|
// ?? CONTROLLO ANTI-AUTOBID BIDOO (PRIORITÀ MASSIMA)
|
||||||
// Bidoo ha un sistema di auto-puntata che si attiva a ~2 secondi.
|
// Bidoo ha un sistema di auto-puntata che si attiva a ~2 secondi.
|
||||||
// Aspettiamo che il timer scenda sotto la soglia per lasciare che
|
// Aspettiamo che il timer scenda sotto la soglia per lasciare che
|
||||||
// gli altri utenti con auto-puntata attiva puntino prima di noi.
|
// gli altri utenti con auto-puntata attiva puntino prima di noi.
|
||||||
// Questo ci fa risparmiare puntate perché non puntiamo "troppo presto".
|
// Questo ci fa risparmiare puntate perché non puntiamo "troppo presto".
|
||||||
if (settings.WaitForAutoBidEnabled && state.Timer > settings.WaitForAutoBidThresholdSeconds)
|
if (settings.WaitForAutoBidEnabled && state.Timer > settings.WaitForAutoBidThresholdSeconds)
|
||||||
{
|
{
|
||||||
// Timer ancora sopra la soglia - aspetta che le auto-puntate si attivino
|
// Timer ancora sopra la soglia - aspetta che le auto-puntate si attivino
|
||||||
@@ -743,7 +713,7 @@ namespace AutoBidder.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ?? CONTROLLO 0: Verifica convenienza (se dati disponibili)
|
// ?? CONTROLLO 0: Verifica convenienza (se dati disponibili)
|
||||||
// IMPORTANTE: Applica solo se BuyNowPrice è valido (> 0)
|
// IMPORTANTE: Applica solo se BuyNowPrice è valido (> 0)
|
||||||
// Se BuyNowPrice == 0, significa errore scraping - non bloccare le puntate
|
// Se BuyNowPrice == 0, significa errore scraping - non bloccare le puntate
|
||||||
if (auction.BuyNowPrice.HasValue &&
|
if (auction.BuyNowPrice.HasValue &&
|
||||||
auction.BuyNowPrice.Value > 0 &&
|
auction.BuyNowPrice.Value > 0 &&
|
||||||
@@ -751,7 +721,7 @@ namespace AutoBidder.Services
|
|||||||
auction.CalculatedValue.Savings.HasValue &&
|
auction.CalculatedValue.Savings.HasValue &&
|
||||||
!auction.CalculatedValue.IsWorthIt)
|
!auction.CalculatedValue.IsWorthIt)
|
||||||
{
|
{
|
||||||
// Permetti comunque di puntare se il risparmio è ancora positivo (anche se piccolo)
|
// Permetti comunque di puntare se il risparmio è ancora positivo (anche se piccolo)
|
||||||
// Blocca solo se sta andando in perdita significativa (< -5%)
|
// Blocca solo se sta andando in perdita significativa (< -5%)
|
||||||
if (auction.CalculatedValue.SavingsPercentage.HasValue &&
|
if (auction.CalculatedValue.SavingsPercentage.HasValue &&
|
||||||
auction.CalculatedValue.SavingsPercentage.Value < -5)
|
auction.CalculatedValue.SavingsPercentage.Value < -5)
|
||||||
@@ -780,7 +750,7 @@ namespace AutoBidder.Services
|
|||||||
|
|
||||||
if (activeBidders >= maxActiveBidders)
|
if (activeBidders >= maxActiveBidders)
|
||||||
{
|
{
|
||||||
// Controlla se l'ultimo bidder sono io - se sì, posso continuare
|
// Controlla se l'ultimo bidder sono io - se sì, posso continuare
|
||||||
var session = _apiClient.GetSession();
|
var session = _apiClient.GetSession();
|
||||||
var lastBid = recentBids.OrderByDescending(b => b.Timestamp).FirstOrDefault();
|
var lastBid = recentBids.OrderByDescending(b => b.Timestamp).FirstOrDefault();
|
||||||
|
|
||||||
@@ -805,7 +775,7 @@ namespace AutoBidder.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ? CONTROLLO 2: Non puntare se sono già il vincitore corrente
|
// ? CONTROLLO 2: Non puntare se sono già il vincitore corrente
|
||||||
if (state.IsMyBid)
|
if (state.IsMyBid)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -814,13 +784,13 @@ namespace AutoBidder.Services
|
|||||||
// ?? CONTROLLO 3: MinPrice/MaxPrice
|
// ?? CONTROLLO 3: MinPrice/MaxPrice
|
||||||
if (auction.MinPrice > 0 && state.Price < auction.MinPrice)
|
if (auction.MinPrice > 0 && state.Price < auction.MinPrice)
|
||||||
{
|
{
|
||||||
auction.AddLog($"[PRICE] Prezzo troppo basso: €{state.Price:F2} < Min €{auction.MinPrice:F2}");
|
auction.AddLog($"[PRICE] Prezzo troppo basso: €{state.Price:F2} < Min €{auction.MinPrice:F2}");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auction.MaxPrice > 0 && state.Price > auction.MaxPrice)
|
if (auction.MaxPrice > 0 && state.Price > auction.MaxPrice)
|
||||||
{
|
{
|
||||||
auction.AddLog($"[PRICE] Prezzo troppo alto: €{state.Price:F2} > Max €{auction.MaxPrice:F2}");
|
auction.AddLog($"[PRICE] Prezzo troppo alto: €{state.Price:F2} > Max €{auction.MaxPrice:F2}");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -926,8 +896,8 @@ namespace AutoBidder.Services
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Unisce la storia puntate ricevuta dall'API con quella esistente,
|
/// Unisce la storia puntate ricevuta dall'API con quella esistente,
|
||||||
/// mantenendo le puntate più vecchie e aggiungendo solo le nuove.
|
/// mantenendo le puntate più vecchie e aggiungendo solo le nuove.
|
||||||
/// Le puntate sono ordinate con le più RECENTI in CIMA.
|
/// Le puntate sono ordinate con le più RECENTI in CIMA.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void MergeBidHistory(AuctionInfo auction, List<BidHistoryEntry> newBids)
|
private void MergeBidHistory(AuctionInfo auction, List<BidHistoryEntry> newBids)
|
||||||
{
|
{
|
||||||
@@ -940,7 +910,7 @@ namespace AutoBidder.Services
|
|||||||
// ?? FIX: Usa lock per thread-safety
|
// ?? FIX: Usa lock per thread-safety
|
||||||
lock (auction.RecentBids)
|
lock (auction.RecentBids)
|
||||||
{
|
{
|
||||||
// Se la lista esistente è vuota, semplicemente copia le nuove
|
// Se la lista esistente è vuota, semplicemente copia le nuove
|
||||||
if (auction.RecentBids.Count == 0)
|
if (auction.RecentBids.Count == 0)
|
||||||
{
|
{
|
||||||
auction.RecentBids = newBids.ToList();
|
auction.RecentBids = newBids.ToList();
|
||||||
@@ -985,7 +955,7 @@ namespace AutoBidder.Services
|
|||||||
.ThenByDescending(b => b.Price)
|
.ThenByDescending(b => b.Price)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
// Limita al numero massimo di puntate (mantieni le più recenti = prime della lista)
|
// Limita al numero massimo di puntate (mantieni le più recenti = prime della lista)
|
||||||
if (maxEntries > 0 && auction.RecentBids.Count > maxEntries)
|
if (maxEntries > 0 && auction.RecentBids.Count > maxEntries)
|
||||||
{
|
{
|
||||||
auction.RecentBids = auction.RecentBids
|
auction.RecentBids = auction.RecentBids
|
||||||
@@ -1005,21 +975,74 @@ namespace AutoBidder.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Aggiorna le statistiche dei bidder basandosi sulla lista RecentBids (fonte ufficiale).
|
/// Assicura che la puntata corrente (quella vincente) sia sempre presente nello storico.
|
||||||
/// Raggruppa le puntate per utente e conta il numero di puntate per ciascuno.
|
/// Questo risolve il problema dell'API che a volte non include l'ultima puntata.
|
||||||
|
/// </summary>
|
||||||
|
private void EnsureCurrentBidInHistory(AuctionInfo auction, AuctionState state)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var statePrice = (decimal)state.Price;
|
||||||
|
var currentBidder = state.LastBidder;
|
||||||
|
|
||||||
|
// Verifica se questa puntata non è già presente
|
||||||
|
var alreadyExists = auction.RecentBids.Any(b =>
|
||||||
|
Math.Abs(b.Price - statePrice) < 0.001m &&
|
||||||
|
b.Username.Equals(currentBidder, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (!alreadyExists)
|
||||||
|
{
|
||||||
|
var lastBidTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
|
|
||||||
|
auction.RecentBids.Insert(0, new BidHistoryEntry
|
||||||
|
{
|
||||||
|
Username = currentBidder,
|
||||||
|
Price = statePrice,
|
||||||
|
Timestamp = lastBidTimestamp,
|
||||||
|
BidType = "Auto"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Aggiorna anche le statistiche bidder
|
||||||
|
if (!auction.BidderStats.ContainsKey(currentBidder))
|
||||||
|
{
|
||||||
|
auction.BidderStats[currentBidder] = new BidderInfo
|
||||||
|
{
|
||||||
|
Username = currentBidder,
|
||||||
|
BidCount = 1,
|
||||||
|
RecentBidCount = 1,
|
||||||
|
LastBidTime = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var bidder = auction.BidderStats[currentBidder];
|
||||||
|
bidder.BidCount++;
|
||||||
|
bidder.RecentBidCount++;
|
||||||
|
bidder.LastBidTime = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { /* Silenzioso */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Aggiorna le statistiche dei bidder in modo CUMULATIVO.
|
||||||
|
/// BidCount è il totale dall'inizio del monitoraggio.
|
||||||
|
/// RecentBidCount è il conteggio nella finestra corrente di RecentBids.
|
||||||
|
/// I bidder NON vengono mai rimossi, il contatore è infinito.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void UpdateBidderStatsFromRecentBids(AuctionInfo auction)
|
private void UpdateBidderStatsFromRecentBids(AuctionInfo auction)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Raggruppa puntate per username
|
// Raggruppa puntate per username nella finestra corrente
|
||||||
var bidsByUser = auction.RecentBids
|
var bidsByUser = auction.RecentBids
|
||||||
.GroupBy(b => b.Username, StringComparer.OrdinalIgnoreCase)
|
.GroupBy(b => b.Username, StringComparer.OrdinalIgnoreCase)
|
||||||
.ToDictionary(
|
.ToDictionary(
|
||||||
g => g.Key,
|
g => g.Key,
|
||||||
g => new
|
g => new
|
||||||
{
|
{
|
||||||
Count = g.Count(),
|
RecentCount = g.Count(),
|
||||||
LastBidTime = DateTimeOffset.FromUnixTimeSeconds(g.Max(b => b.Timestamp)).DateTime
|
LastBidTime = DateTimeOffset.FromUnixTimeSeconds(g.Max(b => b.Timestamp)).DateTime
|
||||||
},
|
},
|
||||||
StringComparer.OrdinalIgnoreCase
|
StringComparer.OrdinalIgnoreCase
|
||||||
@@ -1033,36 +1056,37 @@ namespace AutoBidder.Services
|
|||||||
|
|
||||||
if (!auction.BidderStats.ContainsKey(username))
|
if (!auction.BidderStats.ContainsKey(username))
|
||||||
{
|
{
|
||||||
|
// Nuovo bidder - inizializza con i conteggi attuali
|
||||||
auction.BidderStats[username] = new BidderInfo
|
auction.BidderStats[username] = new BidderInfo
|
||||||
{
|
{
|
||||||
Username = username,
|
Username = username,
|
||||||
BidCount = stats.Count,
|
BidCount = stats.RecentCount, // Primo conteggio
|
||||||
|
RecentBidCount = stats.RecentCount,
|
||||||
LastBidTime = stats.LastBidTime
|
LastBidTime = stats.LastBidTime
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Aggiorna statistiche esistenti
|
|
||||||
var existing = auction.BidderStats[username];
|
var existing = auction.BidderStats[username];
|
||||||
existing.BidCount = stats.Count;
|
|
||||||
|
// Calcola delta: quante nuove puntate rispetto all'ultimo check
|
||||||
|
int previousRecent = existing.RecentBidCount;
|
||||||
|
int currentRecent = stats.RecentCount;
|
||||||
|
|
||||||
|
// Se il conteggio è aumentato, aggiungi la differenza al totale cumulativo
|
||||||
|
if (currentRecent > previousRecent)
|
||||||
|
{
|
||||||
|
existing.BidCount += (currentRecent - previousRecent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggiorna conteggio finestra corrente e ultimo timestamp
|
||||||
|
existing.RecentBidCount = currentRecent;
|
||||||
existing.LastBidTime = stats.LastBidTime;
|
existing.LastBidTime = stats.LastBidTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rimuovi bidder che non sono più in RecentBids
|
// NON rimuovere i bidder che non sono più in RecentBids!
|
||||||
var usersInRecentBids = new HashSet<string>(
|
// Il conteggio totale è cumulativo e persistente.
|
||||||
auction.RecentBids.Select(b => b.Username),
|
|
||||||
StringComparer.OrdinalIgnoreCase
|
|
||||||
);
|
|
||||||
|
|
||||||
var usersToRemove = auction.BidderStats.Keys
|
|
||||||
.Where(u => !usersInRecentBids.Contains(u))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
foreach (var user in usersToRemove)
|
|
||||||
{
|
|
||||||
auction.BidderStats.Remove(user);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -248,7 +248,39 @@ namespace AutoBidder.Services
|
|||||||
return decision;
|
return decision;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Verifica soft retreat
|
// ?? 1. ENTRY POINT - Verifica se il prezzo è conveniente
|
||||||
|
// Punta solo se prezzo < (MaxPrice * 0.7)
|
||||||
|
if (settings.EntryPointEnabled && auction.MaxPrice > 0)
|
||||||
|
{
|
||||||
|
var entryThreshold = auction.MaxPrice * 0.7;
|
||||||
|
if (state.Price >= entryThreshold)
|
||||||
|
{
|
||||||
|
decision.ShouldBid = false;
|
||||||
|
decision.Reason = $"Entry point: €{state.Price:F2} >= 70% di max €{auction.MaxPrice:F2}";
|
||||||
|
return decision;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ?? 2. ANTI-BOT - Rileva pattern bot (timing identico)
|
||||||
|
if (settings.AntiBotDetectionEnabled && !string.IsNullOrEmpty(state.LastBidder))
|
||||||
|
{
|
||||||
|
var botCheck = DetectBotPattern(auction, state.LastBidder, currentUsername);
|
||||||
|
if (botCheck.IsBot)
|
||||||
|
{
|
||||||
|
decision.ShouldBid = false;
|
||||||
|
decision.Reason = $"Anti-bot: {state.LastBidder} pattern sospetto (var={botCheck.TimingVarianceMs:F0}ms)";
|
||||||
|
return decision;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ?? 3. USER EXHAUSTION - Sfrutta utenti stanchi (info solo, non blocca)
|
||||||
|
if (settings.UserExhaustionEnabled && !string.IsNullOrEmpty(state.LastBidder))
|
||||||
|
{
|
||||||
|
var exhaustionCheck = CheckUserExhaustion(auction, state.LastBidder, currentUsername);
|
||||||
|
// Non blocchiamo, ma potremmo loggare per info
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Verifica soft retreat
|
||||||
if (settings.SoftRetreatEnabled || (auction.SoftRetreatEnabledOverride ?? settings.SoftRetreatEnabled))
|
if (settings.SoftRetreatEnabled || (auction.SoftRetreatEnabledOverride ?? settings.SoftRetreatEnabled))
|
||||||
{
|
{
|
||||||
if (auction.IsInSoftRetreat)
|
if (auction.IsInSoftRetreat)
|
||||||
@@ -338,6 +370,68 @@ namespace AutoBidder.Services
|
|||||||
return decision;
|
return decision;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rileva pattern bot analizzando i delta timing degli ultimi bid
|
||||||
|
/// </summary>
|
||||||
|
private (bool IsBot, double TimingVarianceMs) DetectBotPattern(AuctionInfo auction, string? lastBidder, string currentUsername)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(lastBidder) || lastBidder.Equals(currentUsername, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return (false, 999);
|
||||||
|
|
||||||
|
// Ottieni gli ultimi 3+ bid di questo utente
|
||||||
|
var userBids = auction.RecentBids
|
||||||
|
.Where(b => b.Username.Equals(lastBidder, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.OrderByDescending(b => b.Timestamp)
|
||||||
|
.Take(4)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (userBids.Count < 3)
|
||||||
|
return (false, 999);
|
||||||
|
|
||||||
|
// Calcola i delta tra bid consecutivi
|
||||||
|
var deltas = new List<long>();
|
||||||
|
for (int i = 0; i < userBids.Count - 1; i++)
|
||||||
|
{
|
||||||
|
deltas.Add(userBids[i].Timestamp - userBids[i + 1].Timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deltas.Count < 2)
|
||||||
|
return (false, 999);
|
||||||
|
|
||||||
|
// Calcola varianza dei delta
|
||||||
|
var avg = deltas.Average();
|
||||||
|
var variance = deltas.Sum(d => Math.Pow(d - avg, 2)) / deltas.Count;
|
||||||
|
var stdDev = Math.Sqrt(variance) * 1000; // Converti in ms
|
||||||
|
|
||||||
|
// Se la varianza è < 50ms, probabilmente è un bot
|
||||||
|
return (stdDev < 50, stdDev);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifica se un utente è esausto (molte puntate, può mollare)
|
||||||
|
/// </summary>
|
||||||
|
private (bool ShouldExploit, string Reason) CheckUserExhaustion(AuctionInfo auction, string? lastBidder, string currentUsername)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(lastBidder) || lastBidder.Equals(currentUsername, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return (false, "");
|
||||||
|
|
||||||
|
// Verifica se l'utente è un "heavy user" (>50 puntate totali)
|
||||||
|
if (auction.BidderStats.TryGetValue(lastBidder, out var stats))
|
||||||
|
{
|
||||||
|
if (stats.BidCount > 50)
|
||||||
|
{
|
||||||
|
// Se ci sono pochi altri bidder attivi, può essere un buon momento
|
||||||
|
var activeBidders = auction.BidderStats.Values.Count(b => b.BidCount > 5);
|
||||||
|
if (activeBidders <= 3)
|
||||||
|
{
|
||||||
|
return (true, $"{lastBidder} ha {stats.BidCount} puntate, potrebbe mollare");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (false, "");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calcola probabilità di puntata basata su competizione e ROI
|
/// Calcola probabilità di puntata basata su competizione e ROI
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -376,11 +376,11 @@ namespace AutoBidder.Services
|
|||||||
if (nameMatch.Success)
|
if (nameMatch.Success)
|
||||||
{
|
{
|
||||||
var name = System.Net.WebUtility.HtmlDecode(nameMatch.Groups[1].Value.Trim());
|
var name = System.Net.WebUtility.HtmlDecode(nameMatch.Groups[1].Value.Trim());
|
||||||
// ?? FIX: Sostituisci entità HTML non standard
|
// ?? FIX: Sostituisci entità HTML non standard con +
|
||||||
name = name
|
name = name
|
||||||
.Replace("+", "+")
|
.Replace("+", "+")
|
||||||
.Replace("&plus;", "+")
|
.Replace("&plus;", "+")
|
||||||
.Replace(" + ", " & ");
|
.Replace("&", "&"); // Decodifica & residui
|
||||||
auction.Name = name;
|
auction.Name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ namespace AutoBidder.Utilities
|
|||||||
// ? NUOVO: LIMITE MINIMO PUNTATE
|
// ? NUOVO: LIMITE MINIMO PUNTATE
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Numero minimo di puntate residue da mantenere sull'account.
|
/// Numero minimo di puntate residue da mantenere sull'account.
|
||||||
/// Se impostato > 0, il sistema non punterà se le puntate residue scenderebbero sotto questa soglia.
|
/// Se impostato > 0, il sistema non punterà se le puntate residue scenderebbero sotto questa soglia.
|
||||||
/// Default: 0 (nessun limite)
|
/// Default: 0 (nessun limite)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int MinimumRemainingBids { get; set; } = 0;
|
public int MinimumRemainingBids { get; set; } = 0;
|
||||||
@@ -89,13 +89,13 @@ namespace AutoBidder.Utilities
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Esegue pulizia automatica record incompleti all'avvio.
|
/// Esegue pulizia automatica record incompleti all'avvio.
|
||||||
/// Default: false (può rimuovere dati utili in caso di errori temporanei)
|
/// Default: false (può rimuovere dati utili in caso di errori temporanei)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool DatabaseAutoCleanupIncomplete { get; set; } = false;
|
public bool DatabaseAutoCleanupIncomplete { get; set; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Numero massimo di giorni da mantenere nei risultati aste.
|
/// Numero massimo di giorni da mantenere nei risultati aste.
|
||||||
/// Record più vecchi vengono eliminati automaticamente.
|
/// Record più vecchi vengono eliminati automaticamente.
|
||||||
/// Default: 180 (6 mesi), 0 = disabilitato
|
/// Default: 180 (6 mesi), 0 = disabilitato
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int DatabaseMaxRetentionDays { get; set; } = 180;
|
public int DatabaseMaxRetentionDays { get; set; } = 180;
|
||||||
@@ -113,19 +113,19 @@ namespace AutoBidder.Utilities
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Abilita jitter casuale sull'offset per evitare sincronizzazione con altri bot.
|
/// Abilita jitter casuale sull'offset per evitare sincronizzazione con altri bot.
|
||||||
/// Aggiunge ±JitterRangeMs al timing di puntata.
|
/// Aggiunge ±JitterRangeMs al timing di puntata.
|
||||||
/// Default: true
|
/// Default: true
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool JitterEnabled { get; set; } = true;
|
public bool JitterEnabled { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Range massimo del jitter casuale in millisecondi (±X ms).
|
/// Range massimo del jitter casuale in millisecondi (±X ms).
|
||||||
/// Default: 50 (range -50ms a +50ms)
|
/// Default: 50 (range -50ms a +50ms)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int JitterRangeMs { get; set; } = 50;
|
public int JitterRangeMs { get; set; } = 50;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Abilita offset dinamico per asta basato su ping, storico e volatilità.
|
/// Abilita offset dinamico per asta basato su ping, storico e volatilità.
|
||||||
/// Default: true
|
/// Default: true
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool DynamicOffsetEnabled { get; set; } = true;
|
public bool DynamicOffsetEnabled { get; set; } = true;
|
||||||
@@ -155,7 +155,7 @@ namespace AutoBidder.Utilities
|
|||||||
public bool WaitForAutoBidEnabled { get; set; } = true;
|
public bool WaitForAutoBidEnabled { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Soglia in secondi sotto la quale si può puntare.
|
/// Soglia in secondi sotto la quale si può puntare.
|
||||||
/// Bidoo attiva le auto-puntate a ~2 secondi, quindi aspettiamo che passino.
|
/// Bidoo attiva le auto-puntate a ~2 secondi, quindi aspettiamo che passino.
|
||||||
/// Default: 1.8 (punta solo quando timer < 1.8s, dopo che le auto-puntate si sono attivate)
|
/// Default: 1.8 (punta solo quando timer < 1.8s, dopo che le auto-puntate si sono attivate)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -167,9 +167,32 @@ namespace AutoBidder.Utilities
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool LogAutoBidWaitSkips { get; set; } = false;
|
public bool LogAutoBidWaitSkips { get; set; } = false;
|
||||||
|
|
||||||
// ??????????????????????????????????????????????????????????????
|
// 🎯 STRATEGIE SEMPLIFICATE
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Entry Point: Punta solo se prezzo attuale è inferiore al 70% del MaxPrice.
|
||||||
|
/// Richiede che MaxPrice sia impostato sull'asta.
|
||||||
|
/// Default: true
|
||||||
|
/// </summary>
|
||||||
|
public bool EntryPointEnabled { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Anti-Bot: Rileva pattern bot (timing identico con varianza minore di 50ms)
|
||||||
|
/// e evita di competere contro bot automatici.
|
||||||
|
/// Default: true
|
||||||
|
/// </summary>
|
||||||
|
public bool AntiBotDetectionEnabled { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// User Exhaustion: Sfrutta utenti stanchi (oltre 50 puntate)
|
||||||
|
/// quando ci sono pochi altri bidder attivi.
|
||||||
|
/// Default: true
|
||||||
|
/// </summary>
|
||||||
|
public bool UserExhaustionEnabled { get; set; } = true;
|
||||||
|
|
||||||
|
// 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
|
||||||
// RILEVAMENTO COMPETIZIONE E HEAT METRIC
|
// RILEVAMENTO COMPETIZIONE E HEAT METRIC
|
||||||
// ??????????????????????????????????????????????????????????????
|
// 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Abilita rilevamento competizione e heat metric.
|
/// Abilita rilevamento competizione e heat metric.
|
||||||
@@ -231,19 +254,19 @@ namespace AutoBidder.Utilities
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Abilita policy di puntata probabilistica.
|
/// Abilita policy di puntata probabilistica.
|
||||||
/// Decide se puntare con probabilità p basata su competizione e ROI.
|
/// Decide se puntare con probabilità p basata su competizione e ROI.
|
||||||
/// Default: false (richiede tuning)
|
/// Default: false (richiede tuning)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool ProbabilisticBiddingEnabled { get; set; } = false;
|
public bool ProbabilisticBiddingEnabled { get; set; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Probabilità base di puntata (0.0 - 1.0).
|
/// Probabilità base di puntata (0.0 - 1.0).
|
||||||
/// Default: 0.8 (80%)
|
/// Default: 0.8 (80%)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double BaseBidProbability { get; set; } = 0.8;
|
public double BaseBidProbability { get; set; } = 0.8;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fattore di riduzione probabilità per ogni bidder attivo extra.
|
/// Fattore di riduzione probabilità per ogni bidder attivo extra.
|
||||||
/// Default: 0.1 (riduce del 10% per ogni bidder oltre la soglia)
|
/// Default: 0.1 (riduce del 10% per ogni bidder oltre la soglia)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double ProbabilityReductionPerBidder { get; set; } = 0.1;
|
public double ProbabilityReductionPerBidder { get; set; } = 0.1;
|
||||||
@@ -274,7 +297,7 @@ namespace AutoBidder.Utilities
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Soglia percentuale per considerare un utente "aggressivo".
|
/// Soglia percentuale per considerare un utente "aggressivo".
|
||||||
/// Se un utente ha più di X% delle puntate nella finestra, è aggressivo.
|
/// Se un utente ha più di X% delle puntate nella finestra, è aggressivo.
|
||||||
/// Default: 40 (40% delle puntate)
|
/// Default: 40 (40% delle puntate)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double AggressiveBidderPercentageThreshold { get; set; } = 40.0;
|
public double AggressiveBidderPercentageThreshold { get; set; } = 40.0;
|
||||||
@@ -287,7 +310,7 @@ namespace AutoBidder.Utilities
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Azione da intraprendere con bidder aggressivi.
|
/// Azione da intraprendere con bidder aggressivi.
|
||||||
/// "Avoid" = evita l'asta, "Compete" = continua normalmente, "Outbid" = punta più aggressivamente
|
/// "Avoid" = evita l'asta, "Compete" = continua normalmente, "Outbid" = punta più aggressivamente
|
||||||
/// Default: "Compete" (cambiato da Avoid per essere meno restrittivo)
|
/// Default: "Compete" (cambiato da Avoid per essere meno restrittivo)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string AggressiveBidderAction { get; set; } = "Compete";
|
public string AggressiveBidderAction { get; set; } = "Compete";
|
||||||
@@ -316,7 +339,7 @@ namespace AutoBidder.Utilities
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Budget massimo giornaliero in euro (0 = illimitato).
|
/// Budget massimo giornaliero in euro (0 = illimitato).
|
||||||
/// Calcolato come: puntate usate × costo medio puntata.
|
/// Calcolato come: puntate usate × costo medio puntata.
|
||||||
/// Default: 0
|
/// Default: 0
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double DailyBudgetEuro { get; set; } = 0;
|
public double DailyBudgetEuro { get; set; } = 0;
|
||||||
|
|||||||
@@ -299,17 +299,22 @@
|
|||||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ?? RIMOSSO: hover-lift causava movimento fastidioso */
|
||||||
.hover-lift:hover {
|
.hover-lift:hover {
|
||||||
transform: translateY(-4px);
|
/* transform: translateY(-4px); - RIMOSSO */
|
||||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ?? RIMOSSO: hover-scale causava zoom fastidioso */
|
||||||
.hover-scale {
|
.hover-scale {
|
||||||
transition: transform 0.3s ease;
|
transition: background-color 0.2s ease, border-color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hover-scale:hover {
|
.hover-scale:hover {
|
||||||
transform: scale(1.05);
|
/* transform: scale(1.05); - RIMOSSO */
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
border-color: rgba(13, 110, 253, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hover-rotate {
|
.hover-rotate {
|
||||||
|
|||||||
@@ -585,55 +585,67 @@ body {
|
|||||||
.btn-success {
|
.btn-success {
|
||||||
background: var(--success-color);
|
background: var(--success-color);
|
||||||
color: white;
|
color: white;
|
||||||
|
transition: filter 0.2s ease, box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-success:hover:not(:disabled) {
|
.btn-success:hover:not(:disabled) {
|
||||||
background: #059669;
|
filter: brightness(1.1);
|
||||||
|
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-warning {
|
.btn-warning {
|
||||||
background: var(--warning-color);
|
background: var(--warning-color);
|
||||||
color: white;
|
color: white;
|
||||||
|
transition: filter 0.2s ease, box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-warning:hover:not(:disabled) {
|
.btn-warning:hover:not(:disabled) {
|
||||||
background: #d97706;
|
filter: brightness(1.1);
|
||||||
|
box-shadow: 0 2px 8px rgba(245, 158, 11, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger {
|
.btn-danger {
|
||||||
background: var(--danger-color);
|
background: var(--danger-color);
|
||||||
color: white;
|
color: white;
|
||||||
|
transition: filter 0.2s ease, box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger:hover:not(:disabled) {
|
.btn-danger:hover:not(:disabled) {
|
||||||
background: #dc2626;
|
filter: brightness(1.1);
|
||||||
|
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background: var(--primary-color);
|
background: var(--primary-color);
|
||||||
color: white;
|
color: white;
|
||||||
|
transition: filter 0.2s ease, box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover:not(:disabled) {
|
.btn-primary:hover:not(:disabled) {
|
||||||
background: #0284c7;
|
filter: brightness(1.1);
|
||||||
|
box-shadow: 0 2px 8px rgba(14, 165, 233, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
background: var(--bg-hover);
|
background: var(--bg-hover);
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
|
transition: filter 0.2s ease, box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary:hover:not(:disabled) {
|
.btn-secondary:hover:not(:disabled) {
|
||||||
background: var(--text-muted);
|
filter: brightness(1.15);
|
||||||
|
box-shadow: 0 2px 8px rgba(100, 116, 139, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-info {
|
.btn-info {
|
||||||
background: var(--info-color);
|
background: var(--info-color);
|
||||||
color: white;
|
color: white;
|
||||||
|
transition: filter 0.2s ease, box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-info:hover:not(:disabled) {
|
.btn-info:hover:not(:disabled) {
|
||||||
background: #2563eb;
|
filter: brightness(1.1);
|
||||||
|
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:disabled {
|
.btn:disabled {
|
||||||
|
|||||||
@@ -714,8 +714,9 @@ main {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 🔥 COMPATTATO: Ridotto padding per massimizzare spazio */
|
||||||
.tab-panel-content {
|
.tab-panel-content {
|
||||||
padding: 1rem;
|
padding: 0.5rem 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === GRADIENTS FOR CARDS === */
|
/* === GRADIENTS FOR CARDS === */
|
||||||
@@ -883,24 +884,33 @@ main {
|
|||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
padding: 0.75rem;
|
padding: 0.5rem;
|
||||||
margin: 0.5rem;
|
margin: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 🔥 COMPATTATO: Ridotto margin e padding per info-group */
|
||||||
.info-group {
|
.info-group {
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-group label {
|
.info-group label {
|
||||||
display: block;
|
display: block;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.15rem;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-size: 0.813rem;
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 🔥 COMPATTATO: Input più piccoli */
|
||||||
|
.info-group input.form-control,
|
||||||
|
.info-group select.form-control {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auction-log, .bidders-stats {
|
.auction-log, .bidders-stats {
|
||||||
margin: 0.5rem;
|
margin: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auction-log h4, .bidders-stats h4 {
|
.auction-log h4, .bidders-stats h4 {
|
||||||
|
|||||||
@@ -28,6 +28,29 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 🔥 Header ordinabili */
|
||||||
|
.sortable-header {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable-header:hover {
|
||||||
|
background-color: rgba(13, 110, 253, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 🎯 Evidenziazione riga utente corrente */
|
||||||
|
.my-bid-row {
|
||||||
|
background-color: rgba(40, 167, 69, 0.2) !important;
|
||||||
|
border-left: 3px solid #28a745;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-bid-row:hover {
|
||||||
|
background-color: rgba(40, 167, 69, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.page-header {
|
.page-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -249,6 +272,7 @@
|
|||||||
border: 1px solid var(--border-color) !important;
|
border: 1px solid var(--border-color) !important;
|
||||||
color: var(--text-secondary) !important;
|
color: var(--text-secondary) !important;
|
||||||
border-radius: var(--radius-md) !important;
|
border-radius: var(--radius-md) !important;
|
||||||
|
transition: filter 0.2s ease, background-color 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-container .btn-outline-secondary:hover {
|
.settings-container .btn-outline-secondary:hover {
|
||||||
@@ -256,6 +280,36 @@
|
|||||||
color: var(--text-primary) !important;
|
color: var(--text-primary) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 🎨 Stili hover moderni per pulsanti outline */
|
||||||
|
.btn-outline-primary,
|
||||||
|
.btn-outline-secondary,
|
||||||
|
.btn-outline-success,
|
||||||
|
.btn-outline-danger,
|
||||||
|
.btn-outline-warning,
|
||||||
|
.btn-outline-info {
|
||||||
|
transition: filter 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-primary:hover:not(:disabled) {
|
||||||
|
filter: brightness(1.05);
|
||||||
|
box-shadow: 0 2px 6px rgba(13, 110, 253, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-success:hover:not(:disabled) {
|
||||||
|
filter: brightness(1.05);
|
||||||
|
box-shadow: 0 2px 6px rgba(25, 135, 84, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-danger:hover:not(:disabled) {
|
||||||
|
filter: brightness(1.05);
|
||||||
|
box-shadow: 0 2px 6px rgba(220, 53, 69, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-warning:hover:not(:disabled) {
|
||||||
|
filter: brightness(1.05);
|
||||||
|
box-shadow: 0 2px 6px rgba(255, 193, 7, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
/* === AUCTION BROWSER STYLES === */
|
/* === AUCTION BROWSER STYLES === */
|
||||||
|
|
||||||
.browser-container {
|
.browser-container {
|
||||||
|
|||||||
Reference in New Issue
Block a user