diff --git a/Mimante/Models/AuctionInfo.cs b/Mimante/Models/AuctionInfo.cs index 8dc3abf..2b9d4dc 100644 --- a/Mimante/Models/AuctionInfo.cs +++ b/Mimante/Models/AuctionInfo.cs @@ -128,20 +128,56 @@ namespace AutoBidder.Models [JsonIgnore] public AuctionState? LastState { get; set; } + /// - /// 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. /// /// Messaggio da aggiungere al log /// Numero massimo di righe da mantenere (default: 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); // Mantieni solo gli ultimi maxLines log if (AuctionLog.Count > maxLines) { - // Rimuovi i log pi vecchi per mantenere la dimensione sotto controllo int excessCount = AuctionLog.Count - maxLines; AuctionLog.RemoveRange(0, excessCount); } diff --git a/Mimante/Models/BidderInfo.cs b/Mimante/Models/BidderInfo.cs index 71e77c4..6edd184 100644 --- a/Mimante/Models/BidderInfo.cs +++ b/Mimante/Models/BidderInfo.cs @@ -3,12 +3,25 @@ using System; namespace AutoBidder.Models { /// - /// 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). /// public class BidderInfo { public string Username { get; set; } = ""; + + /// + /// Conteggio CUMULATIVO delle puntate dall'inizio del monitoraggio. + /// Questo valore non viene mai decrementato anche se RecentBids viene troncato. + /// public int BidCount { get; set; } = 0; + + /// + /// Conteggio puntate visibili nell'attuale finestra RecentBids (per UI). + /// Pu essere inferiore a BidCount se RecentBids stato troncato. + /// + public int RecentBidCount { get; set; } = 0; + public DateTime LastBidTime { get; set; } = DateTime.MinValue; public string LastBidTimeDisplay => LastBidTime == DateTime.MinValue diff --git a/Mimante/Pages/Index.razor b/Mimante/Pages/Index.razor index 7cb1641..e8fe82b 100644 --- a/Mimante/Pages/Index.razor +++ b/Mimante/Pages/Index.razor @@ -63,6 +63,9 @@ + @@ -85,18 +88,18 @@ - - - - + + + + - - + + - @foreach (var auction in auctions) + @foreach (var auction in GetSortedAuctions()) {
@{ + // ?? FIX: Rimuovi duplicati consecutivi (stesso prezzo + stesso utente) var recentBidsList = GetRecentBidsSafe(selectedAuction); + var filteredBids = new List(); + 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()) {
Stato Nome Prezzo Timer Stato @GetSortIndicator("stato") Nome @GetSortIndicator("nome") Prezzo @GetSortIndicator("prezzo") Timer @GetSortIndicator("timer") Ultimo Puntate Ping Puntate @GetSortIndicator("puntate") Ping @GetSortIndicator("ping") Azioni
@@ -402,15 +420,19 @@ - @foreach (var bid in recentBidsList.Take(50)) + @foreach (var bid in filteredBids.Take(50)) { - + @@ -434,29 +456,29 @@
@{ - // Crea una copia thread-safe per evitare modifiche durante l'enumerazione - var recentBidsCopy = GetRecentBidsSafe(selectedAuction); + // ?? FIX: Usa BidderStats che contiene i conteggi CUMULATIVI (non limitati) + var bidderStatsCopy = selectedAuction.BidderStats + .Values + .OrderByDescending(b => b.BidCount) + .ToList(); - // ?? FIX: Per l'utente corrente, usa BidsUsedOnThisAuction (valore ufficiale dal server) + // Per l'utente corrente, usa BidsUsedOnThisAuction (valore ufficiale dal server) var myOfficialBidsCount = selectedAuction.BidsUsedOnThisAuction ?? 0; var currentUsername = GetCurrentUsername(); } - @if (recentBidsCopy.Any()) + @if (bidderStatsCopy.Any()) { - // Calcola statistiche puntatori - var bidderStats = recentBidsCopy - .GroupBy(b => b.Username) - .Select(g => new { - Username = g.Key, - // Per l'utente corrente usa il conteggio ufficiale, per gli altri conta dalla lista - Count = g.First().IsMyBid && myOfficialBidsCount > 0 ? myOfficialBidsCount : g.Count(), - IsMe = g.First().IsMyBid - }) - .OrderByDescending(s => s.Count) - .ToList(); + // Calcola il totale CUMULATIVO + var totalBidsCumulative = bidderStatsCopy.Sum(b => b.BidCount); - // Ricalcola il totale includendo il conteggio corretto per l'utente - var totalBids = bidderStats.Sum(b => b.Count); + // Correggi il conteggio per l'utente corrente se disponibile + var myBidder = bidderStatsCopy.FirstOrDefault(b => + b.Username.Equals(currentUsername, StringComparison.OrdinalIgnoreCase)); + if (myBidder != null && myOfficialBidsCount > myBidder.BidCount) + { + // Usa il valore ufficiale se maggiore + totalBidsCumulative = totalBidsCumulative - myBidder.BidCount + myOfficialBidsCount; + }
- @bid.Username @if (bid.IsMyBid) { + @bid.Username TU } + else + { + @bid.Username + } @bid.PriceFormatted @bid.TimeFormatted
@@ -469,25 +491,32 @@ - @for (int i = 0; i < bidderStats.Count; i++) + @for (int i = 0; i < bidderStatsCopy.Count; i++) { - var bidder = bidderStats[i]; - var percentage = (bidder.Count * 100.0 / totalBids); - + var bidder = bidderStatsCopy[i]; + var isMe = bidder.Username.Equals(currentUsername, StringComparison.OrdinalIgnoreCase); + // Per l'utente corrente usa il conteggio ufficiale + var displayCount = isMe && myOfficialBidsCount > bidder.BidCount + ? myOfficialBidsCount + : bidder.BidCount; + var percentage = totalBidsCumulative > 0 + ? (displayCount * 100.0 / totalBidsCumulative) + : 0; + - +
#@(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("&plus;", "+") - .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 {