Files
Mimante/Mimante/Pages/Index.razor
T
Alby96 ae861e78d2 Implementate strategie avanzate e tracking aste v1.3.0
- Aggiunto BidStrategyService: adaptive latency, jitter, offset dinamico, heat metric, soft retreat, probabilistic bidding, profiling avversari, bankroll manager.
- Esteso AuctionInfo con metriche avanzate: latenze, collisioni, heat, duello, tracking sessione, override strategie.
- Nuova sezione "Strategie Avanzate" in Settings (UI) con opzioni dettagliate e bulk update.
- Miglioramenti UX: auto-scroll log, filtri e dettagli avanzati in Statistics, gestione nomi prodotti, pulsanti sempre attivi.
- Fix bug Blazor (layout, redirect, log, conteggio puntate, entità HTML).
- Aggiornata documentazione, changelog, guide Docker/Gitea.
- Versione incrementata a 1.3.0. Migrazione database per nuove metriche e tracking completo.
2026-01-28 11:37:40 +01:00

581 lines
33 KiB
Plaintext

@page "/"
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
@inject AuctionMonitor AuctionMonitor
@inject AuctionStateService AuctionStateService
@inject IJSRuntime JSRuntime
@implements IDisposable
<PageTitle>Monitor Aste - AutoBidder</PageTitle>
<div class="auction-monitor animate-fade-in">
<!-- Toolbar Superiore -->
<div class="toolbar animate-fade-in-down">
<!-- Box Sessione Utente - Compatto in linea -->
<div class="toolbar-user-info">
@if (!string.IsNullOrEmpty(sessionUsername))
{
<div class="user-card connected">
<i class="bi bi-person-circle user-icon"></i>
<span class="user-name">@sessionUsername</span>
<div class="divider"></div>
<div class="stat-compact">
<i class="bi bi-hand-index-thumb-fill"></i>
<span class="stat-value @GetBidsClass()">@sessionRemainingBids</span>
</div>
<div class="divider"></div>
<div class="stat-compact">
<i class="bi bi-wallet2"></i>
<span class="stat-value text-success">€@sessionShopCredit.ToString("F2")</span>
</div>
@if (sessionAuctionsWon > 0)
{
<div class="divider"></div>
<div class="stat-compact">
<i class="bi bi-trophy-fill"></i>
<span class="stat-value text-warning">@sessionAuctionsWon</span>
</div>
}
</div>
}
else
{
<div class="user-card disconnected">
<i class="bi bi-person-x user-icon"></i>
<span class="user-name text-muted">Non connesso</span>
</div>
}
</div>
<!-- Pulsanti Azioni (Centro-Destra) -->
<div class="toolbar-actions">
<button class="btn btn-success hover-lift" @onclick="StartAll">
<i class="bi bi-play-fill"></i> Avvia Tutto
</button>
<button class="btn btn-warning hover-lift" @onclick="PauseAll">
<i class="bi bi-pause-fill"></i> Pausa Tutto
</button>
<button class="btn btn-danger hover-lift" @onclick="StopAll">
<i class="bi bi-stop-fill"></i> Ferma Tutto
</button>
<button class="btn btn-primary ms-3 hover-lift" @onclick="ShowAddAuctionDialog">
<i class="bi bi-plus-lg"></i> Aggiungi Asta
</button>
<button class="btn btn-secondary hover-lift" @onclick="RemoveSelectedAuction" disabled="@(selectedAuction == null)">
<i class="bi bi-trash"></i> Rimuovi
</button>
<button class="btn btn-outline-danger hover-lift" @onclick="RemoveAllAuctions" disabled="@(auctions.Count == 0)" title="Rimuovi tutte le aste (quelle terminate verranno salvate)">
<i class="bi bi-trash-fill"></i> Rimuovi Tutte
</button>
</div>
</div>
<div class="content-layout">
<!-- GRIGLIA ASTE - PARTE SUPERIORE SINISTRA -->
<div class="auctions-grid-section animate-fade-in-left delay-100 shadow-hover">
<h3><i class="bi bi-list-check"></i> Aste Monitorate (@auctions.Count)</h3>
@if (auctions.Count == 0)
{
<div class="alert alert-info animate-fade-in-up">
<i class="bi bi-info-circle"></i> Nessuna asta monitorata. Clicca su "Aggiungi Asta" per iniziare.
</div>
}
else
{
<div class="table-responsive">
<table class="table table-striped table-hover mb-0 table-fixed">
<thead>
<tr>
<th class="col-stato"><i class="bi bi-toggle-on"></i> Stato</th>
<th class="col-nome"><i class="bi bi-tag"></i> Nome</th>
<th class="col-prezzo"><i class="bi bi-currency-euro"></i> Prezzo</th>
<th class="col-timer"><i class="bi bi-clock"></i> Timer</th>
<th class="col-ultimo"><i class="bi bi-person"></i> Ultimo</th>
<th class="col-click"><i class="bi bi-hand-index" style="font-size: 0.85rem;"></i> Puntate</th>
<th class="col-ping"><i class="bi bi-speedometer"></i> Ping</th>
<th class="col-azioni"><i class="bi bi-gear"></i> Azioni</th>
</tr>
</thead>
<tbody>
@foreach (var auction in auctions)
{
<tr class="@GetRowClass(auction) @(selectedAuction == auction ? "selected-row" : "") table-row-enter transition-all"
@onclick="() => SelectAuction(auction)"
style="cursor: pointer;">
<td class="col-stato">
<span class="badge @GetStatusBadgeClass(auction) @GetStatusAnimationClass(auction)">
@((MarkupString)GetStatusIcon(auction)) @GetStatusText(auction)
</span>
</td>
<td class="col-nome fw-semibold">@auction.Name</td>
<td class="col-prezzo @GetPriceClass(auction)">@GetPriceDisplay(auction)</td>
<td class="col-timer">@GetTimerDisplay(auction)</td>
<td class="col-ultimo">@GetLastBidder(auction)</td>
<td class="col-click bids-column fw-bold">@GetMyBidsCount(auction)</td>
<td class="col-ping">@GetPingDisplay(auction)</td>
<td class="col-azioni">
<div class="btn-group btn-group-sm" @onclick:stopPropagation="true">
<button class="btn btn-primary hover-scale"
@onclick="() => ManualBidAuction(auction)"
title="Punta Manualmente"
disabled="@IsManualBidding(auction)">
@if (IsManualBidding(auction))
{
<span class="spinner-border spinner-border-sm" role="status"></span>
}
else
{
<i class="bi bi-hand-index-thumb"></i>
}
</button>
@if (auction.IsActive && !auction.IsPaused)
{
<button class="btn btn-warning hover-scale" @onclick="() => PauseAuction(auction)" title="Pausa">
<i class="bi bi-pause-fill"></i>
</button>
}
else if (auction.IsPaused)
{
<button class="btn btn-success hover-scale" @onclick="() => ResumeAuction(auction)" title="Riprendi">
<i class="bi bi-play-fill"></i>
</button>
}
else
{
<button class="btn btn-success hover-scale" @onclick="() => StartAuction(auction)" title="Avvia">
<i class="bi bi-play-fill"></i>
</button>
}
<button class="btn btn-danger hover-scale" @onclick="() => StopAuction(auction)" title="Ferma">
<i class="bi bi-stop-fill"></i>
</button>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>
<!-- SPLITTER VERTICALE -->
<div class="splitter-vertical"></div>
<!-- LOG GLOBALE - PARTE SUPERIORE DESTRA -->
<div class="global-log animate-fade-in-right delay-200">
<div class="d-flex justify-content-between align-items-center">
<h4 class="mb-0"><i class="bi bi-terminal"></i> Log Globale</h4>
<button class="btn btn-sm btn-secondary" @onclick="ClearGlobalLog">
<i class="bi bi-trash"></i>
</button>
</div>
<div class="log-box" id="globalLogContainer" @ref="globalLogRef">
@if (globalLog.Count == 0)
{
<div class="text-muted"><i class="bi bi-inbox"></i> Nessun log ancora...</div>
}
else
{
@foreach (var logEntry in globalLog.TakeLast(100))
{
<div class="@GetLogEntryClass(logEntry)">@logEntry.Message</div>
}
}
<div id="logScrollAnchor"></div>
</div>
</div>
</div>
<!-- SPLITTER ORIZZONTALE -->
<div class="splitter-horizontal"></div>
<!-- DETTAGLI ASTA CON TABS - PARTE INFERIORE (full width) -->
@if (selectedAuction != null)
{
<div class="auction-details-tabs animate-fade-in-up delay-300 shadow-hover">
<h3><i class="bi bi-info-circle-fill"></i> @selectedAuction.Name <small class="text-muted">(ID: @selectedAuction.AuctionId)</small></h3>
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="tab-settings" data-bs-toggle="tab" data-bs-target="#content-settings" type="button" role="tab">
<i class="bi bi-gear"></i> Impostazioni
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="tab-product" data-bs-toggle="tab" data-bs-target="#content-product" type="button" role="tab">
<i class="bi bi-box"></i> Info Prodotto
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="tab-history" data-bs-toggle="tab" data-bs-target="#content-history" type="button" role="tab">
<i class="bi bi-clock-history"></i> Storia Puntate
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="tab-bidders" data-bs-toggle="tab" data-bs-target="#content-bidders" type="button" role="tab">
<i class="bi bi-people"></i> Puntatori
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="tab-log" data-bs-toggle="tab" data-bs-target="#content-log" type="button" role="tab">
<i class="bi bi-terminal"></i> Log
</button>
</li>
</ul>
<div class="tab-content">
<!-- TAB IMPOSTAZIONI -->
<div class="tab-pane fade show active" id="content-settings" role="tabpanel">
<div class="tab-panel-content">
<div class="info-group">
<label><i class="bi bi-link-45deg"></i> URL:</label>
<div class="input-group">
<input type="text" class="form-control" value="@selectedAuction.OriginalUrl" readonly />
<button class="btn btn-outline-secondary" @onclick="() => CopyToClipboard(selectedAuction.OriginalUrl)" title="Copia">
<i class="bi bi-clipboard"></i>
</button>
<button class="btn btn-outline-primary" @onclick="() => OpenAuctionInNewTab(selectedAuction.OriginalUrl)" title="Apri in nuova scheda">
<i class="bi bi-box-arrow-up-right"></i>
</button>
</div>
</div>
<div class="row">
<div class="col-md-12 info-group">
<label><i class="bi bi-speedometer2"></i> Anticipo (ms):</label>
<input type="number" class="form-control" @bind="selectedAuction.BidBeforeDeadlineMs" @bind:after="SaveAuctions" />
</div>
</div>
<div class="row">
<div class="col-md-6 info-group">
<label><i class="bi bi-currency-euro"></i> Min €:</label>
<input type="number" step="0.01" class="form-control" @bind="selectedAuction.MinPrice" @bind:after="SaveAuctions" />
</div>
<div class="col-md-6 info-group">
<label><i class="bi bi-currency-euro"></i> Max €:</label>
<input type="number" step="0.01" class="form-control" @bind="selectedAuction.MaxPrice" @bind:after="SaveAuctions" />
</div>
</div>
<div class="row">
<div class="col-md-12 info-group">
<label><i class="bi bi-hand-index-thumb"></i> Max Puntate (0 = illimitate):</label>
<input type="number" class="form-control" @bind="selectedAuction.MaxBidsOverride" @bind:after="SaveAuctions" />
<small class="text-muted">Limite puntate per questa asta. 0 o vuoto = usa limite globale.</small>
</div>
</div>
<!-- Pulsante Applica Limiti Consigliati -->
<div class="mt-3 pt-3 border-top">
<button class="btn btn-outline-primary w-100"
@onclick="ApplyRecommendedLimitsToSelected"
disabled="@isLoadingRecommendations">
@if (isLoadingRecommendations)
{
<span class="spinner-border spinner-border-sm me-2"></span>
<span>Caricamento...</span>
}
else
{
<i class="bi bi-magic me-2"></i>
<span>Applica Limiti Consigliati</span>
}
</button>
@if (!string.IsNullOrEmpty(recommendationMessage))
{
<div class="alert @(recommendationSuccess ? "alert-success" : "alert-warning") mt-2 mb-0 py-2 small">
<i class="bi @(recommendationSuccess ? "bi-check-circle" : "bi-exclamation-triangle") me-1"></i>
@recommendationMessage
</div>
}
</div>
</div>
</div>
<!-- TAB INFO PRODOTTO -->
<div class="tab-pane fade" id="content-product" role="tabpanel">
<div class="tab-panel-content">
@if (selectedAuction.CalculatedValue != null)
{
<!-- Sezione Principale - Compatta -->
<div class="product-info-compact">
<div class="info-cards">
<div class="info-card primary">
<i class="bi bi-tag-fill"></i>
<div>
<small>Prezzo Compra Subito</small>
<strong>@GetBuyNowPriceDisplay(selectedAuction)</strong>
</div>
</div>
<div class="info-card info">
<i class="bi bi-truck"></i>
<div>
<small>Spedizione</small>
<strong>€@(selectedAuction.ShippingCost?.ToString("F2") ?? "0.00")</strong>
</div>
</div>
</div>
<!-- Calcoli in linea -->
<div class="calc-inline">
<div class="calc-item">
<i class="bi bi-currency-euro"></i>
<span class="label">Prezzo attuale</span>
<span class="value">€@selectedAuction.CalculatedValue.CurrentPrice.ToString("F2")</span>
</div>
<div class="calc-item">
<i class="bi bi-hand-index"></i>
<span class="label">Totale puntate</span>
<span class="value">@selectedAuction.CalculatedValue.TotalBids</span>
</div>
<div class="calc-item highlight">
<i class="bi bi-person-check-fill"></i>
<span class="label">Tue puntate</span>
<span class="value">@selectedAuction.CalculatedValue.MyBids</span>
</div>
<div class="calc-item">
<i class="bi bi-cash-coin"></i>
<span class="label">Costo puntate</span>
<span class="value">€@selectedAuction.CalculatedValue.MyBidsCost.ToString("F2")</span>
</div>
</div>
<!-- Totali compatti -->
<div class="totals-compact">
<div class="total-item warning">
<span>Costo Totale se vinci</span>
<strong>€@selectedAuction.CalculatedValue.TotalCostIfWin.ToString("F2")</strong>
</div>
<div class="total-item @(selectedAuction.CalculatedValue.Savings > 0 ? "success" : "danger")">
<span>
<i class="bi bi-@(selectedAuction.CalculatedValue.Savings > 0 ? "arrow-down-circle-fill" : "arrow-up-circle-fill")"></i>
@(selectedAuction.CalculatedValue.Savings > 0 ? "Risparmio" : "Perdita")
</span>
<strong>@GetSavingsDisplay(selectedAuction)</strong>
</div>
<div class="verdict-badge @(selectedAuction.CalculatedValue.Savings > 0 ? "success" : "danger")">
<i class="bi bi-@(selectedAuction.CalculatedValue.Savings > 0 ? "check-circle-fill" : "x-circle-fill")"></i>
@(selectedAuction.CalculatedValue.Savings > 0 ? "Conveniente!" : "Non conveniente")
</div>
</div>
</div>
@if (!string.IsNullOrEmpty(selectedAuction.CalculatedValue.Summary))
{
<div class="alert alert-info mt-3 mb-0">
<i class="bi bi-info-circle"></i> @selectedAuction.CalculatedValue.Summary
</div>
}
}
else
{
<div class="alert alert-secondary">
<i class="bi bi-hourglass-split"></i> Informazioni prodotto non ancora disponibili. Verranno caricate automaticamente.
</div>
}
</div>
</div>
<!-- TAB STORIA PUNTATE -->
<div class="tab-pane fade" id="content-history" role="tabpanel">
<div class="tab-panel-content">
@{
var recentBidsList = GetRecentBidsSafe(selectedAuction);
}
@if (recentBidsList.Any())
{
<div class="table-responsive">
<table class="table table-sm table-striped">
<thead>
<tr>
<th>Utente</th>
<th>Prezzo</th>
<th>Data/Ora</th>
<th>Tipo</th>
</tr>
</thead>
<tbody>
@foreach (var bid in recentBidsList.Take(50))
{
<tr class="@(bid.IsMyBid ? "table-success" : "")">
<td>
@bid.Username
@if (bid.IsMyBid)
{
<span class="badge bg-success ms-1">TU</span>
}
</td>
<td class="fw-bold">€@bid.PriceFormatted</td>
<td class="text-muted small">@bid.TimeFormatted</td>
<td><span class="badge bg-secondary">@bid.BidType</span></td>
</tr>
}
</tbody>
</table>
</div>
}
else
{
<div class="alert alert-secondary">
<i class="bi bi-inbox"></i> Nessuna puntata registrata per questa asta.
</div>
}
</div>
</div>
<!-- TAB PUNTATORI -->
<div class="tab-pane fade" id="content-bidders" role="tabpanel">
<div class="tab-panel-content">
@{
// Crea una copia thread-safe per evitare modifiche durante l'enumerazione
var recentBidsCopy = GetRecentBidsSafe(selectedAuction);
// ?? FIX: Per l'utente corrente, usa BidsUsedOnThisAuction (valore ufficiale dal server)
var myOfficialBidsCount = selectedAuction.BidsUsedOnThisAuction ?? 0;
var currentUsername = GetCurrentUsername();
}
@if (recentBidsCopy.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();
// Ricalcola il totale includendo il conteggio corretto per l'utente
var totalBids = bidderStats.Sum(b => b.Count);
<div class="table-responsive">
<table class="table table-sm table-striped">
<thead>
<tr>
<th>Posizione</th>
<th>Utente</th>
<th>Puntate</th>
<th>Percentuale</th>
</tr>
</thead>
<tbody>
@for (int i = 0; i < bidderStats.Count; i++)
{
var bidder = bidderStats[i];
var percentage = (bidder.Count * 100.0 / totalBids);
<tr class="@(bidder.IsMe ? "table-success" : "")">
<td><span class="badge bg-primary">#@(i + 1)</span></td>
<td>
@bidder.Username
@if (bidder.IsMe)
{
<span class="badge bg-success ms-1">TU</span>
}
</td>
<td class="fw-bold">@bidder.Count</td>
<td>
<div class="progress" style="height: 20px;">
<div class="progress-bar @(bidder.IsMe ? "bg-success" : "bg-primary")"
role="progressbar"
style="width: @percentage.ToString("F1")%">
@percentage.ToString("F1")%
</div>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
}
else
{
<div class="alert alert-secondary">
<i class="bi bi-inbox"></i> Nessun dato sui puntatori disponibile.
</div>
}
</div>
</div>
<!-- TAB LOG -->
<div class="tab-pane fade" id="content-log" role="tabpanel">
<div class="tab-panel-content">
<div class="log-box-compact">
@if (selectedAuction.AuctionLog.Any())
{
@foreach (var logEntry in GetAuctionLog(selectedAuction))
{
<div class="log-entry">@logEntry</div>
}
}
else
{
<div class="text-muted"><i class="bi bi-inbox"></i> Nessun log disponibile per questa asta.</div>
}
</div>
</div>
</div>
</div>
</div>
}
else
{
<div class="auction-details-tabs animate-fade-in shadow-hover">
<div class="alert alert-secondary text-center my-5">
<i class="bi bi-arrow-up" style="font-size: 2rem; display: block; margin-bottom: 0.5rem;"></i>
<p class="mb-0">Seleziona un'asta dalla griglia per visualizzare i dettagli</p>
</div>
</div>
}
</div>
<!-- Modal Aggiungi Asta -->
@if (showAddDialog)
{
<div class="modal show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5);">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content animate-scale-in">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-plus-circle"></i> Aggiungi Nuova Asta</h5>
<button type="button" class="btn-close" @onclick="CloseAddDialog"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label fw-bold"><i class="bi bi-link-45deg"></i> URL Asta:</label>
<input type="text" class="form-control transition-colors @(addDialogError != null ? "is-invalid" : "")"
@bind="addDialogUrl"
placeholder="https://it.bidoo.com/asta/..." />
@if (addDialogError != null)
{
<div class="invalid-feedback d-block animate-shake">
<i class="bi bi-exclamation-triangle"></i> @addDialogError
</div>
}
<small class="form-text text-muted">
<i class="bi bi-info-circle"></i> Inserisci l'URL completo dell'asta da Bidoo.com. Il nome sarà rilevato automaticamente.
</small>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary hover-lift" @onclick="CloseAddDialog">
<i class="bi bi-x-circle"></i> Annulla
</button>
<button type="button" class="btn btn-primary hover-lift" @onclick="AddAuction" disabled="@string.IsNullOrWhiteSpace(addDialogUrl)">
<i class="bi bi-plus-lg"></i> Aggiungi
</button>
</div>
</div>
</div>
</div>
}