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
This commit is contained in:
398
TradingBot/Components/Pages/Logs.razor
Normal file
398
TradingBot/Components/Pages/Logs.razor
Normal file
@@ -0,0 +1,398 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
@using TradingBot.Services
|
||||
@using TradingBot.Models
|
||||
@inject SettingsService SettingsService
|
||||
@inject TradingBotService TradingBotService
|
||||
@inject TradeHistoryService HistoryService
|
||||
@implements IDisposable
|
||||
@rendermode InteractiveServer
|
||||
|
||||
@@ -67,6 +69,39 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h2>Dati Persistenti</h2>
|
||||
<div class="settings-group">
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Trade Salvati</div>
|
||||
<div class="setting-description">@TradingBotService.Trades.Count trade nella cronologia</div>
|
||||
</div>
|
||||
<div class="setting-value">
|
||||
@FormatBytes(dataSize)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Posizioni Attive</div>
|
||||
<div class="setting-description">@TradingBotService.ActivePositions.Count posizioni aperte</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Cancella Tutti i Dati</div>
|
||||
<div class="setting-description text-danger">Elimina cronologia trade e resetta i saldi</div>
|
||||
</div>
|
||||
<button class="btn-danger" @onclick="ShowClearDataConfirmation" disabled="@TradingBotService.Status.IsRunning">
|
||||
<span class="bi bi-trash"></span>
|
||||
Cancella Dati
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h2>Avanzate</h2>
|
||||
<div class="settings-group">
|
||||
@@ -116,16 +151,57 @@
|
||||
Impostazioni salvate con successo!
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (showClearConfirmation)
|
||||
{
|
||||
<div class="modal-overlay" @onclick="HideClearDataConfirmation">
|
||||
<div class="modal-dialog" @onclick:stopPropagation="true">
|
||||
<div class="modal-header">
|
||||
<h3>Conferma Cancellazione</h3>
|
||||
<button class="btn-close" @onclick="HideClearDataConfirmation">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-danger">
|
||||
<strong>Attenzione!</strong> Questa azione eliminerà:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Tutta la cronologia dei trade (@TradingBotService.Trades.Count trade)</li>
|
||||
<li>Tutte le posizioni attive (@TradingBotService.ActivePositions.Count posizioni)</li>
|
||||
<li>I saldi verranno resettati ai valori iniziali</li>
|
||||
</ul>
|
||||
<p class="text-danger">
|
||||
<strong>Questa operazione è irreversibile!</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" @onclick="HideClearDataConfirmation">Annulla</button>
|
||||
<button class="btn-danger" @onclick="ConfirmClearData">
|
||||
<span class="bi bi-trash"></span>
|
||||
Conferma Cancellazione
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private AppSettings settings = new();
|
||||
private bool showNotification = false;
|
||||
private bool showClearConfirmation = false;
|
||||
private long dataSize = 0;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
settings = SettingsService.GetSettings();
|
||||
SettingsService.OnSettingsChanged += HandleSettingsChanged;
|
||||
TradingBotService.OnStatusChanged += HandleStatusChanged;
|
||||
UpdateDataSize();
|
||||
}
|
||||
|
||||
private void UpdateDataSize()
|
||||
{
|
||||
dataSize = HistoryService.GetDataSize();
|
||||
}
|
||||
|
||||
private void UpdateSetting<T>(string propertyName, T value)
|
||||
@@ -148,6 +224,28 @@
|
||||
ShowNotification();
|
||||
}
|
||||
|
||||
private void ShowClearDataConfirmation()
|
||||
{
|
||||
showClearConfirmation = true;
|
||||
}
|
||||
|
||||
private void HideClearDataConfirmation()
|
||||
{
|
||||
showClearConfirmation = false;
|
||||
}
|
||||
|
||||
private async Task ConfirmClearData()
|
||||
{
|
||||
await TradingBotService.ClearAllDataAsync();
|
||||
UpdateDataSize();
|
||||
showClearConfirmation = false;
|
||||
showNotification = true;
|
||||
StateHasChanged();
|
||||
await Task.Delay(3000);
|
||||
showNotification = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async void ShowNotification()
|
||||
{
|
||||
showNotification = true;
|
||||
@@ -157,14 +255,34 @@
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private string FormatBytes(long bytes)
|
||||
{
|
||||
string[] sizes = { "B", "KB", "MB", "GB" };
|
||||
double len = bytes;
|
||||
int order = 0;
|
||||
while (len >= 1024 && order < sizes.Length - 1)
|
||||
{
|
||||
order++;
|
||||
len = len / 1024;
|
||||
}
|
||||
return $"{len:0.##} {sizes[order]}";
|
||||
}
|
||||
|
||||
private void HandleSettingsChanged()
|
||||
{
|
||||
settings = SettingsService.GetSettings();
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private void HandleStatusChanged()
|
||||
{
|
||||
UpdateDataSize();
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
SettingsService.OnSettingsChanged -= HandleSettingsChanged;
|
||||
TradingBotService.OnStatusChanged -= HandleStatusChanged;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user