Files
Alberto Balbo 92c8e57a8c Persistenza dati e logging avanzato con UI e Unraid
- Aggiunto TradeHistoryService per persistenza trade/posizioni attive su disco (JSON, auto-save/restore)
- Logging centralizzato (LoggingService) con livelli, categorie, simbolo e buffer circolare (500 log)
- Nuova pagina Logs: monitoraggio real-time, filtri avanzati, cancellazione log, colorazione livelli
- Sezione "Dati Persistenti" in Settings: conteggio trade, dimensione dati, reset con conferma modale
- Background service per salvataggio sicuro su shutdown/stop container
- Aggiornata sidebar, stili modali/bottoni danger, .gitignore e documentazione (README, CHANGELOG, UNRAID_INSTALL, checklist)
- Versione 1.3.0
2025-12-22 11:24:17 +01:00

399 lines
10 KiB
Plaintext

@page "/logs"
@using TradingBot.Services
@using TradingBot.Models
@inject LoggingService LoggingService
@implements IDisposable
@rendermode InteractiveServer
<PageTitle>Logs - TradingBot</PageTitle>
<div class="logs-page">
<div class="page-header">
<div>
<h1>Logs Operazioni</h1>
<p class="subtitle">Cronologia eventi e operazioni del bot</p>
</div>
<div class="header-actions">
<button class="btn-secondary" @onclick="ClearLogs">
<span class="bi bi-trash"></span>
Cancella Logs
</button>
</div>
</div>
<div class="logs-filters">
<div class="filter-group">
<label>Livello:</label>
<select @bind="selectedLevel" @bind:after="FilterLogs">
<option value="">Tutti</option>
<option value="Debug">Debug</option>
<option value="Info">Info</option>
<option value="Warning">Warning</option>
<option value="Error">Error</option>
<option value="Trade">Trade</option>
</select>
</div>
<div class="filter-group">
<label>Categoria:</label>
<select @bind="selectedCategory" @bind:after="FilterLogs">
<option value="">Tutte</option>
@foreach (var category in categories)
{
<option value="@category">@category</option>
}
</select>
</div>
<div class="filter-group">
<label>Symbol:</label>
<select @bind="selectedSymbol" @bind:after="FilterLogs">
<option value="">Tutti</option>
@foreach (var symbol in symbols)
{
<option value="@symbol">@symbol</option>
}
</select>
</div>
<div class="filter-group">
<label>
<input type="checkbox" @bind="autoScroll" />
Auto-scroll
</label>
</div>
<div class="logs-count">
@filteredLogs.Count / @allLogs.Count logs
</div>
</div>
<div class="logs-container" @ref="logsContainer">
@if (filteredLogs.Count == 0)
{
<div class="empty-state">
<span class="bi bi-inbox"></span>
<p>Nessun log disponibile</p>
</div>
}
else
{
<div class="logs-list">
@foreach (var log in filteredLogs.OrderByDescending(l => l.Timestamp))
{
<div class="log-entry log-@log.Level.ToString().ToLower()">
<div class="log-header">
<span class="log-timestamp">@log.Timestamp.ToLocalTime().ToString("HH:mm:ss.fff")</span>
<span class="log-level">
<span class="bi bi-@GetLevelIcon(log.Level)"></span>
@log.Level
</span>
<span class="log-category">@log.Category</span>
@if (!string.IsNullOrEmpty(log.Symbol))
{
<span class="log-symbol">@log.Symbol</span>
}
</div>
<div class="log-message">@log.Message</div>
@if (!string.IsNullOrEmpty(log.Details))
{
<div class="log-details">@log.Details</div>
}
</div>
}
</div>
}
</div>
</div>
<style>
.logs-page {
height: 100%;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.logs-filters {
display: flex;
gap: 1rem;
align-items: center;
padding: 1rem;
background: #1a1f3a;
border-radius: 0.75rem;
border: 1px solid rgba(99, 102, 241, 0.2);
flex-wrap: wrap;
}
.filter-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.filter-group label {
font-size: 0.875rem;
color: #94a3b8;
font-weight: 600;
}
.filter-group select {
padding: 0.5rem 1rem;
border-radius: 0.375rem;
background: #0f1629;
border: 1px solid #334155;
color: #e2e8f0;
font-size: 0.875rem;
}
.logs-count {
margin-left: auto;
padding: 0.5rem 1rem;
background: rgba(99, 102, 241, 0.1);
border-radius: 0.375rem;
color: #6366f1;
font-weight: 600;
font-size: 0.875rem;
}
.logs-container {
flex: 1;
overflow-y: auto;
background: #1a1f3a;
border-radius: 0.75rem;
border: 1px solid rgba(99, 102, 241, 0.2);
padding: 1rem;
}
.logs-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.log-entry {
padding: 1rem;
border-radius: 0.5rem;
border-left: 3px solid;
background: #0f1629;
}
.log-entry.log-debug {
border-left-color: #64748b;
}
.log-entry.log-info {
border-left-color: #3b82f6;
}
.log-entry.log-warning {
border-left-color: #f59e0b;
}
.log-entry.log-error {
border-left-color: #ef4444;
}
.log-entry.log-trade {
border-left-color: #10b981;
background: rgba(16, 185, 129, 0.05);
}
.log-header {
display: flex;
gap: 1rem;
align-items: center;
margin-bottom: 0.5rem;
font-size: 0.875rem;
}
.log-timestamp {
color: #64748b;
font-family: 'Courier New', monospace;
}
.log-level {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.25rem 0.75rem;
border-radius: 0.25rem;
font-weight: 700;
text-transform: uppercase;
font-size: 0.75rem;
}
.log-entry.log-debug .log-level {
background: rgba(100, 116, 139, 0.2);
color: #94a3b8;
}
.log-entry.log-info .log-level {
background: rgba(59, 130, 246, 0.2);
color: #3b82f6;
}
.log-entry.log-warning .log-level {
background: rgba(245, 158, 11, 0.2);
color: #f59e0b;
}
.log-entry.log-error .log-level {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
.log-entry.log-trade .log-level {
background: rgba(16, 185, 129, 0.2);
color: #10b981;
}
.log-category {
padding: 0.25rem 0.75rem;
background: rgba(99, 102, 241, 0.1);
border-radius: 0.25rem;
color: #6366f1;
font-weight: 600;
}
.log-symbol {
padding: 0.25rem 0.75rem;
background: rgba(139, 92, 246, 0.1);
border-radius: 0.25rem;
color: #8b5cf6;
font-weight: 700;
}
.log-message {
color: #e2e8f0;
line-height: 1.6;
}
.log-details {
margin-top: 0.5rem;
padding: 0.75rem;
background: rgba(0, 0, 0, 0.3);
border-radius: 0.375rem;
color: #94a3b8;
font-size: 0.875rem;
font-family: 'Courier New', monospace;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4rem 2rem;
color: #64748b;
}
.empty-state .bi {
font-size: 4rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.empty-state p {
font-size: 1.125rem;
margin: 0;
}
</style>
@code {
private ElementReference logsContainer;
private List<LogEntry> allLogs = new();
private List<LogEntry> filteredLogs = new();
private string selectedLevel = "";
private string selectedCategory = "";
private string selectedSymbol = "";
private bool autoScroll = true;
private List<string> categories = new();
private List<string> symbols = new();
protected override void OnInitialized()
{
LoadLogs();
LoggingService.OnLogAdded += HandleLogAdded;
}
private void LoadLogs()
{
allLogs = LoggingService.GetLogs().ToList();
UpdateFilters();
FilterLogs();
}
private void UpdateFilters()
{
categories = allLogs.Select(l => l.Category).Distinct().OrderBy(c => c).ToList();
symbols = allLogs.Where(l => !string.IsNullOrEmpty(l.Symbol))
.Select(l => l.Symbol!)
.Distinct()
.OrderBy(s => s)
.ToList();
}
private void FilterLogs()
{
var query = allLogs.AsEnumerable();
if (!string.IsNullOrEmpty(selectedLevel))
{
if (Enum.TryParse<TradingBot.Models.LogLevel>(selectedLevel, out var level))
{
query = query.Where(l => l.Level == level);
}
}
if (!string.IsNullOrEmpty(selectedCategory))
{
query = query.Where(l => l.Category == selectedCategory);
}
if (!string.IsNullOrEmpty(selectedSymbol))
{
query = query.Where(l => l.Symbol == selectedSymbol);
}
filteredLogs = query.ToList();
StateHasChanged();
}
private async void HandleLogAdded()
{
LoadLogs();
await InvokeAsync(StateHasChanged);
if (autoScroll)
{
await Task.Delay(100);
// Auto-scroll logic would go here if needed
}
}
private void ClearLogs()
{
LoggingService.ClearLogs();
LoadLogs();
}
private string GetLevelIcon(TradingBot.Models.LogLevel level)
{
return level switch
{
TradingBot.Models.LogLevel.Debug => "bug",
TradingBot.Models.LogLevel.Info => "info-circle",
TradingBot.Models.LogLevel.Warning => "exclamation-triangle",
TradingBot.Models.LogLevel.Error => "x-circle",
TradingBot.Models.LogLevel.Trade => "graph-up-arrow",
_ => "circle"
};
}
public void Dispose()
{
LoggingService.OnLogAdded -= HandleLogAdded;
}
}