Expand debug info with library stats, service status, config, and configurable log output
This commit is contained in:
parent
fc05412a13
commit
f9d80606e3
3 changed files with 268 additions and 32 deletions
119
web_server.py
119
web_server.py
|
|
@ -3839,12 +3839,26 @@ def get_system_stats():
|
|||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
def _safe_check(fn, default=False):
|
||||
"""Safely evaluate a check function, returning default on any error."""
|
||||
try:
|
||||
return fn()
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
@app.route('/api/debug-info')
|
||||
def get_debug_info():
|
||||
"""Collect system diagnostics for troubleshooting support requests."""
|
||||
import platform
|
||||
import sys
|
||||
import psutil
|
||||
import time
|
||||
from datetime import timedelta
|
||||
|
||||
log_lines = request.args.get('lines', 20, type=int)
|
||||
log_lines = max(10, min(log_lines, 500))
|
||||
log_source = request.args.get('log', 'app')
|
||||
|
||||
info = {}
|
||||
|
||||
|
|
@ -3854,6 +3868,11 @@ def get_debug_info():
|
|||
info['python'] = sys.version.split()[0]
|
||||
info['docker'] = os.path.exists('/.dockerenv')
|
||||
|
||||
# Uptime
|
||||
start_time = getattr(app, 'start_time', time.time())
|
||||
uptime_seconds = time.time() - start_time
|
||||
info['uptime'] = str(timedelta(seconds=int(uptime_seconds)))
|
||||
|
||||
# Paths
|
||||
download_path = config_manager.get('soulseek.download_path', './downloads')
|
||||
transfer_folder = config_manager.get('soulseek.transfer_path', './Transfer')
|
||||
|
|
@ -3881,6 +3900,8 @@ def get_debug_info():
|
|||
'media_server_connected': media_server_cache.get('connected', False),
|
||||
'soulseek_connected': soulseek_cache.get('connected', False),
|
||||
'download_source': config_manager.get('download_source.mode', 'soulseek'),
|
||||
'tidal_connected': _safe_check(lambda: bool(tidal_client and tidal_client.is_authenticated())),
|
||||
'qobuz_connected': _safe_check(lambda: bool(qobuz_enrichment_worker and qobuz_enrichment_worker.client and qobuz_enrichment_worker.client.is_authenticated())),
|
||||
}
|
||||
|
||||
# Enrichment workers
|
||||
|
|
@ -3891,6 +3912,68 @@ def get_debug_info():
|
|||
workers[name] = 'paused' if config_manager.get(paused_key, False) else 'active'
|
||||
info['enrichment_workers'] = workers
|
||||
|
||||
# Library stats
|
||||
try:
|
||||
db = get_db()
|
||||
lib_stats = db.get_statistics()
|
||||
info['library'] = {
|
||||
'artists': lib_stats.get('artists', 0),
|
||||
'albums': lib_stats.get('albums', 0),
|
||||
'tracks': lib_stats.get('tracks', 0),
|
||||
}
|
||||
except Exception:
|
||||
info['library'] = {'artists': 0, 'albums': 0, 'tracks': 0}
|
||||
|
||||
# Watchlist count
|
||||
try:
|
||||
db = get_db()
|
||||
info['watchlist_count'] = db.get_watchlist_count()
|
||||
except Exception:
|
||||
info['watchlist_count'] = 0
|
||||
|
||||
# Automation count
|
||||
try:
|
||||
db = get_db()
|
||||
automations = db.get_automations()
|
||||
info['automations'] = {
|
||||
'total': len(automations),
|
||||
'enabled': len([a for a in automations if a.get('enabled', False)]),
|
||||
}
|
||||
except Exception:
|
||||
info['automations'] = {'total': 0, 'enabled': 0}
|
||||
|
||||
# Active downloads & syncs (use list() snapshots to avoid RuntimeError from concurrent mutation)
|
||||
try:
|
||||
active_downloads = len([bid for bid, bd in list(download_batches.items()) if bd.get('phase') == 'downloading'])
|
||||
except Exception:
|
||||
active_downloads = 0
|
||||
active_syncs = 0
|
||||
try:
|
||||
for pid, ss in list(sync_states.items()):
|
||||
if ss.get('status') == 'syncing':
|
||||
active_syncs += 1
|
||||
for uh, st in list(youtube_playlist_states.items()):
|
||||
if st.get('phase') == 'syncing':
|
||||
active_syncs += 1
|
||||
for pid, st in list(tidal_discovery_states.items()):
|
||||
if st.get('phase') == 'syncing':
|
||||
active_syncs += 1
|
||||
except Exception:
|
||||
pass
|
||||
info['active_downloads'] = active_downloads
|
||||
info['active_syncs'] = active_syncs
|
||||
|
||||
# Config settings relevant to troubleshooting
|
||||
info['config'] = {
|
||||
'source_mode': config_manager.get('download_source.mode', 'soulseek'),
|
||||
'quality_profile': config_manager.get('download_source.quality_profile', 'default'),
|
||||
'organization_template': config_manager.get('organization.folder_template', ''),
|
||||
'post_processing_enabled': config_manager.get('post_processing.enabled', True),
|
||||
'acoustid_enabled': bool(config_manager.get('acoustid.api_key', '')),
|
||||
'auto_scan_enabled': config_manager.get('watchlist.auto_scan', False),
|
||||
'm3u_export_enabled': config_manager.get('m3u.enabled', False),
|
||||
}
|
||||
|
||||
# Database size
|
||||
db_path = os.path.join('database', 'music_library.db')
|
||||
if os.path.exists(db_path):
|
||||
|
|
@ -3899,22 +3982,50 @@ def get_debug_info():
|
|||
else:
|
||||
info['database_size'] = 'not found'
|
||||
|
||||
# Memory
|
||||
# Memory & CPU
|
||||
process = psutil.Process(os.getpid())
|
||||
mem = process.memory_info()
|
||||
info['memory_usage'] = f"{mem.rss / (1024 * 1024):.0f} MB"
|
||||
info['system_memory'] = f"{psutil.virtual_memory().percent}%"
|
||||
try:
|
||||
info['cpu_percent'] = f"{process.cpu_percent(interval=0.1):.1f}%"
|
||||
except Exception:
|
||||
info['cpu_percent'] = 'unknown'
|
||||
info['thread_count'] = process.num_threads()
|
||||
|
||||
# Recent log lines
|
||||
log_path = os.path.join('logs', 'app.log')
|
||||
# Log lines
|
||||
log_map = {
|
||||
'app': os.path.join('logs', 'app.log'),
|
||||
'acoustid': os.path.join('logs', 'acoustid.log'),
|
||||
'post_processing': os.path.join('logs', 'post_processing.log'),
|
||||
'source_reuse': os.path.join('logs', 'source_reuse.log'),
|
||||
}
|
||||
log_path = log_map.get(log_source, log_map['app'])
|
||||
info['log_source'] = log_source
|
||||
info['log_lines_requested'] = log_lines
|
||||
info['recent_logs'] = []
|
||||
if os.path.exists(log_path):
|
||||
try:
|
||||
with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
|
||||
lines = f.readlines()
|
||||
info['recent_logs'] = [line.rstrip() for line in lines[-20:]]
|
||||
info['recent_logs'] = [line.rstrip() for line in lines[-log_lines:]]
|
||||
except Exception:
|
||||
info['recent_logs'] = ['(could not read log file)']
|
||||
|
||||
# Available log files
|
||||
info['available_logs'] = []
|
||||
logs_dir = 'logs'
|
||||
if os.path.isdir(logs_dir):
|
||||
for fname in sorted(os.listdir(logs_dir)):
|
||||
if fname.endswith('.log'):
|
||||
fpath = os.path.join(logs_dir, fname)
|
||||
size_kb = os.path.getsize(fpath) / 1024
|
||||
info['available_logs'].append({
|
||||
'name': fname.replace('.log', ''),
|
||||
'file': fname,
|
||||
'size': f"{size_kb:.0f} KB" if size_kb < 1024 else f"{size_kb/1024:.1f} MB",
|
||||
})
|
||||
|
||||
return jsonify(info)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1726,50 +1726,115 @@ function initializeDocsPage() {
|
|||
});
|
||||
nav.innerHTML = navHTML;
|
||||
|
||||
// Add debug info button to sidebar header
|
||||
// Add debug info panel to sidebar header
|
||||
const sidebarHeader = document.querySelector('.docs-sidebar-header');
|
||||
if (sidebarHeader) {
|
||||
const debugBtn = document.createElement('button');
|
||||
debugBtn.className = 'docs-debug-button';
|
||||
debugBtn.innerHTML = '📋 Copy Debug Info';
|
||||
const debugWrap = document.createElement('div');
|
||||
debugWrap.className = 'docs-debug-wrap';
|
||||
debugWrap.innerHTML = `
|
||||
<button class="docs-debug-button">📋 Copy Debug Info</button>
|
||||
<div class="docs-debug-options">
|
||||
<div class="docs-debug-row">
|
||||
<label>Log lines</label>
|
||||
<select class="docs-debug-select" id="debug-log-lines">
|
||||
<option value="20">20</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100" selected>100</option>
|
||||
<option value="200">200</option>
|
||||
<option value="500">500</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="docs-debug-row">
|
||||
<label>Log source</label>
|
||||
<select class="docs-debug-select" id="debug-log-source">
|
||||
<option value="app">app.log</option>
|
||||
<option value="post_processing">post_processing.log</option>
|
||||
<option value="acoustid">acoustid.log</option>
|
||||
<option value="source_reuse">source_reuse.log</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
sidebarHeader.appendChild(debugWrap);
|
||||
|
||||
const debugBtn = debugWrap.querySelector('.docs-debug-button');
|
||||
debugBtn.onclick = async () => {
|
||||
const logLines = document.getElementById('debug-log-lines').value;
|
||||
const logSource = document.getElementById('debug-log-source').value;
|
||||
try {
|
||||
debugBtn.textContent = 'Collecting...';
|
||||
const resp = await fetch('/api/debug-info');
|
||||
const resp = await fetch(`/api/debug-info?lines=${logLines}&log=${logSource}`);
|
||||
const data = await resp.json();
|
||||
|
||||
const ck = '\u2713';
|
||||
const ex = '\u2717';
|
||||
let text = 'SoulSync Debug Info\n';
|
||||
text += '===================================\n\n';
|
||||
text += `Version: ${data.version}\n`;
|
||||
text += `OS: ${data.os}${data.docker ? ' (Docker)' : ''}\n`;
|
||||
text += `Python: ${data.python}\n\n`;
|
||||
text += '═══════════════════════════════════\n\n';
|
||||
|
||||
text += '-- Services --\n';
|
||||
text += `Music Source: ${data.services?.music_source || 'unknown'}\n`;
|
||||
text += `Spotify: ${data.services?.spotify_connected ? 'Connected' : 'Disconnected'}${data.services?.spotify_rate_limited ? ' (Rate Limited)' : ''}\n`;
|
||||
text += `Media Server: ${data.services?.media_server_type || 'none'} (${data.services?.media_server_connected ? 'Connected' : 'Disconnected'})\n`;
|
||||
text += `Soulseek: ${data.services?.soulseek_connected ? 'Connected' : 'Disconnected'}\n`;
|
||||
text += `Download Source: ${data.services?.download_source || 'unknown'}\n\n`;
|
||||
text += '── System ──\n';
|
||||
text += `Version: ${data.version}\n`;
|
||||
text += `OS: ${data.os}${data.docker ? ' (Docker)' : ''}\n`;
|
||||
text += `Python: ${data.python}\n`;
|
||||
text += `Uptime: ${data.uptime || 'unknown'}\n`;
|
||||
text += `Memory: ${data.memory_usage || '?'} (system: ${data.system_memory || '?'})\n`;
|
||||
text += `CPU: ${data.cpu_percent || '?'}\n`;
|
||||
text += `Threads: ${data.thread_count || '?'}\n\n`;
|
||||
|
||||
text += '-- Paths --\n';
|
||||
text += `Download: ${data.paths?.download_path || '(not set)'} ${data.paths?.download_path_exists ? '\u2713 exists' : '\u2717 missing'}${data.paths?.download_path_writable ? ' \u2713 writable' : ' \u2717 not writable'}\n`;
|
||||
text += `Transfer: ${data.paths?.transfer_folder || '(not set)'} ${data.paths?.transfer_folder_exists ? '\u2713 exists' : '\u2717 missing'}${data.paths?.transfer_folder_writable ? ' \u2713 writable' : ' \u2717 not writable'}\n`;
|
||||
text += `Staging: ${data.paths?.staging_folder || '(not set)'} ${data.paths?.staging_folder_exists ? '\u2713 exists' : '\u2717 missing'}\n\n`;
|
||||
text += '── Services ──\n';
|
||||
text += `Music Source: ${data.services?.music_source || 'unknown'}\n`;
|
||||
text += `Spotify: ${data.services?.spotify_connected ? ck + ' Connected' : ex + ' Disconnected'}${data.services?.spotify_rate_limited ? ' (RATE LIMITED)' : ''}\n`;
|
||||
text += `Media Server: ${data.services?.media_server_type || 'none'} ${data.services?.media_server_connected ? ck + ' Connected' : ex + ' Disconnected'}\n`;
|
||||
text += `Soulseek: ${data.services?.soulseek_connected ? ck + ' Connected' : ex + ' Disconnected'}\n`;
|
||||
text += `Tidal: ${data.services?.tidal_connected ? ck + ' Connected' : ex + ' Disconnected'}\n`;
|
||||
text += `Qobuz: ${data.services?.qobuz_connected ? ck + ' Connected' : ex + ' Disconnected'}\n`;
|
||||
text += `Download Mode: ${data.services?.download_source || 'unknown'}\n\n`;
|
||||
|
||||
text += '-- Enrichment Workers --\n';
|
||||
if (data.enrichment_workers) {
|
||||
Object.entries(data.enrichment_workers).forEach(([name, status]) => {
|
||||
text += `${name}: ${status}\n`;
|
||||
});
|
||||
text += '── Library ──\n';
|
||||
text += `Artists: ${data.library?.artists?.toLocaleString() || '0'}\n`;
|
||||
text += `Albums: ${data.library?.albums?.toLocaleString() || '0'}\n`;
|
||||
text += `Tracks: ${data.library?.tracks?.toLocaleString() || '0'}\n`;
|
||||
text += `Database: ${data.database_size || 'unknown'}\n`;
|
||||
text += `Watchlist: ${data.watchlist_count || 0} artists\n`;
|
||||
text += `Automations: ${data.automations?.enabled || 0} enabled / ${data.automations?.total || 0} total\n\n`;
|
||||
|
||||
text += '── Active ──\n';
|
||||
text += `Downloads: ${data.active_downloads || 0}\n`;
|
||||
text += `Syncs: ${data.active_syncs || 0}\n\n`;
|
||||
|
||||
text += '── Paths ──\n';
|
||||
const pathStatus = (exists, writable) => exists ? (writable ? ck + ' ok' : ck + ' exists ' + ex + ' not writable') : ex + ' missing';
|
||||
text += `Download: ${data.paths?.download_path || '(not set)'} [${pathStatus(data.paths?.download_path_exists, data.paths?.download_path_writable)}]\n`;
|
||||
text += `Transfer: ${data.paths?.transfer_folder || '(not set)'} [${pathStatus(data.paths?.transfer_folder_exists, data.paths?.transfer_folder_writable)}]\n`;
|
||||
text += `Staging: ${data.paths?.staging_folder || '(not set)'} [${data.paths?.staging_folder_exists ? ck + ' ok' : ex + ' missing'}]\n\n`;
|
||||
|
||||
text += '── Config ──\n';
|
||||
if (data.config) {
|
||||
text += `Source Mode: ${data.config.source_mode || 'unknown'}\n`;
|
||||
text += `Quality Profile: ${data.config.quality_profile || 'default'}\n`;
|
||||
text += `Folder Template: ${data.config.organization_template || '(default)'}\n`;
|
||||
text += `Post-Processing: ${data.config.post_processing_enabled ? 'enabled' : 'disabled'}\n`;
|
||||
text += `AcoustID: ${data.config.acoustid_enabled ? 'enabled' : 'disabled'}\n`;
|
||||
text += `Auto Scan: ${data.config.auto_scan_enabled ? 'enabled' : 'disabled'}\n`;
|
||||
text += `M3U Export: ${data.config.m3u_export_enabled ? 'enabled' : 'disabled'}\n`;
|
||||
}
|
||||
text += '\n';
|
||||
|
||||
text += `Database: ${data.database_size || 'unknown'}\n`;
|
||||
text += `Memory: ${data.memory_usage || 'unknown'}\n\n`;
|
||||
text += '── Enrichment Workers ──\n';
|
||||
if (data.enrichment_workers) {
|
||||
const active = [], paused = [];
|
||||
Object.entries(data.enrichment_workers).forEach(([name, status]) => {
|
||||
(status === 'active' ? active : paused).push(name);
|
||||
});
|
||||
text += `Active: ${active.length > 0 ? active.join(', ') : 'none'}\n`;
|
||||
text += `Paused: ${paused.length > 0 ? paused.join(', ') : 'none'}\n`;
|
||||
}
|
||||
text += '\n';
|
||||
|
||||
text += '-- Recent Logs (last 20 lines) --\n';
|
||||
text += `── Logs: ${data.log_source || 'app'}.log (last ${data.recent_logs?.length || 0} lines) ──\n`;
|
||||
if (data.recent_logs?.length) {
|
||||
data.recent_logs.forEach(line => { text += line + '\n'; });
|
||||
} else {
|
||||
text += '(no log lines)\n';
|
||||
}
|
||||
|
||||
await navigator.clipboard.writeText(text);
|
||||
|
|
@ -1785,7 +1850,6 @@ function initializeDocsPage() {
|
|||
setTimeout(() => { debugBtn.innerHTML = '📋 Copy Debug Info'; }, 2000);
|
||||
}
|
||||
};
|
||||
sidebarHeader.appendChild(debugBtn);
|
||||
}
|
||||
|
||||
// Build content
|
||||
|
|
|
|||
|
|
@ -34942,6 +34942,67 @@ tr.tag-diff-same {
|
|||
border-color: rgb(var(--accent-rgb));
|
||||
}
|
||||
|
||||
.docs-debug-wrap {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.docs-debug-wrap .docs-debug-button {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.docs-debug-options {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.docs-debug-row {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.docs-debug-row label {
|
||||
font-size: 10px;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.docs-debug-select {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 6px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 11.5px;
|
||||
padding: 5px 8px;
|
||||
font-family: 'SF Pro Text', -apple-system, sans-serif;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='rgba(255,255,255,0.4)'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 8px center;
|
||||
padding-right: 22px;
|
||||
}
|
||||
|
||||
.docs-debug-select:hover {
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.docs-debug-select:focus {
|
||||
outline: none;
|
||||
border-color: rgba(var(--accent-rgb), 0.4);
|
||||
}
|
||||
|
||||
.docs-debug-select option {
|
||||
background: #1a1a2e;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
/* Toast help link */
|
||||
.toast-help-link {
|
||||
display: inline-block;
|
||||
|
|
|
|||
Loading…
Reference in a new issue