| #@(i + 1) |
@bidder.Username
- @if (bidder.IsMe)
+ @if (isMe)
{
TU
}
|
- @bidder.Count |
+ @displayCount |
-
+ style="width: @percentage.ToString("F1", System.Globalization.CultureInfo.InvariantCulture)%">
@percentage.ToString("F1")%
diff --git a/Mimante/Pages/Index.razor.cs b/Mimante/Pages/Index.razor.cs
index 2adf6a4..8927fb4 100644
--- a/Mimante/Pages/Index.razor.cs
+++ b/Mimante/Pages/Index.razor.cs
@@ -28,6 +28,7 @@ namespace AutoBidder.Pages
set => AppState.IsMonitoringActive = value;
}
+
private System.Threading.Timer? refreshTimer;
private System.Threading.Timer? sessionTimer;
@@ -50,6 +51,10 @@ namespace AutoBidder.Pages
// Auto-scroll log
private ElementReference globalLogRef;
private int lastLogCount = 0;
+
+ // ?? Sorting griglia aste
+ private string auctionSortColumn = "nome";
+ private bool auctionSortAscending = true;
protected override void OnInitialized()
{
@@ -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();
}
@@ -618,6 +629,61 @@ namespace AutoBidder.Pages
await JSRuntime.InvokeVoidAsync("alert", $"Errore durante la rimozione:\n{ex.Message}");
}
}
+
+ ///
+ /// Rimuove tutte le aste terminate (salvandole prima nel database)
+ ///
+ 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("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}");
+ }
+ }
+
+ ///
+ /// Verifica se ci sono aste terminate
+ ///
+ private bool HasCompletedAuctions()
+ {
+ return auctions.Any(a => !a.IsActive);
+ }
private async Task RemoveSelectedAuctionWithConfirm()
{
@@ -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 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)
diff --git a/Mimante/Services/ApplicationStateService.cs b/Mimante/Services/ApplicationStateService.cs
index d870f84..cc4414d 100644
--- a/Mimante/Services/ApplicationStateService.cs
+++ b/Mimante/Services/ApplicationStateService.cs
@@ -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
}
}
+ ///
+ /// Ottiene la lista originale delle aste per il salvataggio.
+ /// ATTENZIONE: Usare solo per persistenza, non per iterazione durante modifiche!
+ ///
+ public List GetAuctionsForPersistence()
+ {
+ lock (_lock)
+ {
+ return _auctions;
+ }
+ }
+
+ ///
+ /// Forza il salvataggio delle aste correnti su disco.
+ ///
+ public void PersistAuctions()
+ {
+ lock (_lock)
+ {
+ AutoBidder.Utilities.PersistenceManager.SaveAuctions(_auctions);
+ }
+ }
+
public AuctionInfo? SelectedAuction
{
get
diff --git a/Mimante/Services/AuctionMonitor.cs b/Mimante/Services/AuctionMonitor.cs
index 8938fc7..c10043e 100644
--- a/Mimante/Services/AuctionMonitor.cs
+++ b/Mimante/Services/AuctionMonitor.cs
@@ -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
}
///
- /// Determina se l'asta stata vinta dall'utente corrente
+ /// Determina se l'asta è stata vinta dall'utente corrente
///
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;
@@ -367,6 +367,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 ||
@@ -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
}
///
- /// 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.
///
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
}
///
@@ -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
///
/// 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.
///
private void MergeBidHistory(AuctionInfo auction, List 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
}
///
- /// 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.
+ ///
+ 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 */ }
+ }
+
+ ///
+ /// 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.
///
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(
- 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)
{
diff --git a/Mimante/Services/BidStrategyService.cs b/Mimante/Services/BidStrategyService.cs
index 677e248..b4c5d50 100644
--- a/Mimante/Services/BidStrategyService.cs
+++ b/Mimante/Services/BidStrategyService.cs
@@ -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;
}
+ ///
+ /// Rileva pattern bot analizzando i delta timing degli ultimi bid
+ ///
+ 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();
+ 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);
+ }
+
+ ///
+ /// Verifica se un utente esausto (molte puntate, pu mollare)
+ ///
+ 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, "");
+ }
+
///
/// Calcola probabilit di puntata basata su competizione e ROI
///
diff --git a/Mimante/Services/BidooBrowserService.cs b/Mimante/Services/BidooBrowserService.cs
index 35fd99d..81f7efe 100644
--- a/Mimante/Services/BidooBrowserService.cs
+++ b/Mimante/Services/BidooBrowserService.cs
@@ -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("+", "+")
- .Replace(" + ", " & ");
+ .Replace("&", "&"); // Decodifica & residui
auction.Name = name;
}
diff --git a/Mimante/Utilities/SettingsManager.cs b/Mimante/Utilities/SettingsManager.cs
index 16a00d4..0dd5558 100644
--- a/Mimante/Utilities/SettingsManager.cs
+++ b/Mimante/Utilities/SettingsManager.cs
@@ -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
///
/// 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)
///
public int MinimumRemainingBids { get; set; } = 0;
@@ -89,13 +89,13 @@ namespace AutoBidder.Utilities
///
/// 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)
///
public bool DatabaseAutoCleanupIncomplete { get; set; } = false;
///
/// 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
///
public int DatabaseMaxRetentionDays { get; set; } = 180;
@@ -113,19 +113,19 @@ namespace AutoBidder.Utilities
///
/// 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
///
public bool JitterEnabled { get; set; } = true;
///
- /// Range massimo del jitter casuale in millisecondi (X ms).
+ /// Range massimo del jitter casuale in millisecondi (±X ms).
/// Default: 50 (range -50ms a +50ms)
///
public int JitterRangeMs { get; set; } = 50;
///
- /// Abilita offset dinamico per asta basato su ping, storico e volatilit.
+ /// Abilita offset dinamico per asta basato su ping, storico e volatilità.
/// Default: true
///
public bool DynamicOffsetEnabled { get; set; } = true;
@@ -155,7 +155,7 @@ namespace AutoBidder.Utilities
public bool WaitForAutoBidEnabled { get; set; } = true;
///
- /// 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)
///
@@ -167,9 +167,32 @@ namespace AutoBidder.Utilities
///
public bool LogAutoBidWaitSkips { get; set; } = false;
- // ??????????????????????????????????????????????????????????????
+ // 🎯 STRATEGIE SEMPLIFICATE
+
+ ///
+ /// Entry Point: Punta solo se prezzo attuale è inferiore al 70% del MaxPrice.
+ /// Richiede che MaxPrice sia impostato sull'asta.
+ /// Default: true
+ ///
+ public bool EntryPointEnabled { get; set; } = true;
+
+ ///
+ /// Anti-Bot: Rileva pattern bot (timing identico con varianza minore di 50ms)
+ /// e evita di competere contro bot automatici.
+ /// Default: true
+ ///
+ public bool AntiBotDetectionEnabled { get; set; } = true;
+
+ ///
+ /// User Exhaustion: Sfrutta utenti stanchi (oltre 50 puntate)
+ /// quando ci sono pochi altri bidder attivi.
+ /// Default: true
+ ///
+ public bool UserExhaustionEnabled { get; set; } = true;
+
+ // 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
// RILEVAMENTO COMPETIZIONE E HEAT METRIC
- // ??????????????????????????????????????????????????????????????
+ // 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
///
/// Abilita rilevamento competizione e heat metric.
@@ -231,19 +254,19 @@ namespace AutoBidder.Utilities
///
/// 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)
///
public bool ProbabilisticBiddingEnabled { get; set; } = false;
///
- /// Probabilit base di puntata (0.0 - 1.0).
+ /// Probabilità base di puntata (0.0 - 1.0).
/// Default: 0.8 (80%)
///
public double BaseBidProbability { get; set; } = 0.8;
///
- /// 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)
///
public double ProbabilityReductionPerBidder { get; set; } = 0.1;
@@ -274,7 +297,7 @@ namespace AutoBidder.Utilities
///
/// 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)
///
public double AggressiveBidderPercentageThreshold { get; set; } = 40.0;
@@ -287,7 +310,7 @@ namespace AutoBidder.Utilities
///
/// 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)
///
public string AggressiveBidderAction { get; set; } = "Compete";
@@ -316,7 +339,7 @@ namespace AutoBidder.Utilities
///
/// Budget massimo giornaliero in euro (0 = illimitato).
- /// Calcolato come: puntate usate costo medio puntata.
+ /// Calcolato come: puntate usate × costo medio puntata.
/// Default: 0
///
public double DailyBudgetEuro { get; set; } = 0;
diff --git a/Mimante/wwwroot/css/animations.css b/Mimante/wwwroot/css/animations.css
index 963d70b..58d8d0b 100644
--- a/Mimante/wwwroot/css/animations.css
+++ b/Mimante/wwwroot/css/animations.css
@@ -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 {
diff --git a/Mimante/wwwroot/css/app-modern.css b/Mimante/wwwroot/css/app-modern.css
index c1ac410..ecc6662 100644
--- a/Mimante/wwwroot/css/app-modern.css
+++ b/Mimante/wwwroot/css/app-modern.css
@@ -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 {
diff --git a/Mimante/wwwroot/css/app-wpf.css b/Mimante/wwwroot/css/app-wpf.css
index 118b181..a7dd637 100644
--- a/Mimante/wwwroot/css/app-wpf.css
+++ b/Mimante/wwwroot/css/app-wpf.css
@@ -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 {
diff --git a/Mimante/wwwroot/css/modern-pages.css b/Mimante/wwwroot/css/modern-pages.css
index 7267ea7..02c6c03 100644
--- a/Mimante/wwwroot/css/modern-pages.css
+++ b/Mimante/wwwroot/css/modern-pages.css
@@ -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 {
|