Add rich stats to automation run history
This commit is contained in:
parent
f9e8c8dadd
commit
5daa8c0596
3 changed files with 290 additions and 8 deletions
|
|
@ -274,10 +274,22 @@ def _register_automation_handlers():
|
|||
return {'status': 'completed'}
|
||||
except Exception as e:
|
||||
return {'status': 'error', 'error': str(e)}
|
||||
# Note: wishlist processing is async (batch submitted to executor), stats come via batch completion
|
||||
|
||||
def _auto_scan_watchlist(config):
|
||||
try:
|
||||
pre_state_id = id(watchlist_scan_state)
|
||||
_process_watchlist_scan_automatically(automation_id=config.get('_automation_id'))
|
||||
# Only report stats if a fresh scan actually ran (state dict was reassigned)
|
||||
if id(watchlist_scan_state) != pre_state_id:
|
||||
summary = watchlist_scan_state.get('summary', {})
|
||||
return {
|
||||
'status': 'completed',
|
||||
'artists_scanned': summary.get('total_artists', 0),
|
||||
'successful_scans': summary.get('successful_scans', 0),
|
||||
'new_tracks_found': summary.get('new_tracks_found', 0),
|
||||
'tracks_added_to_wishlist': summary.get('tracks_added_to_wishlist', 0),
|
||||
}
|
||||
return {'status': 'completed'}
|
||||
except Exception as e:
|
||||
return {'status': 'error', 'error': str(e)}
|
||||
|
|
@ -347,11 +359,12 @@ def _register_automation_handlers():
|
|||
phase='Timed out', log_line='Library scan timed out after 30 minutes', log_type='error')
|
||||
return {'status': 'error', 'reason': 'Timed out', '_manages_own_progress': True}
|
||||
|
||||
elapsed = round(time.time() - poll_start, 1)
|
||||
_update_automation_progress(automation_id, status='finished', progress=100,
|
||||
phase='Complete',
|
||||
log_line='Library scan completed', log_type='success')
|
||||
|
||||
return {'status': 'completed', '_manages_own_progress': True}
|
||||
return {'status': 'completed', '_manages_own_progress': True, 'scan_duration_seconds': elapsed}
|
||||
|
||||
except Exception as e:
|
||||
_update_automation_progress(automation_id, status='error',
|
||||
|
|
@ -659,7 +672,17 @@ def _register_automation_handlers():
|
|||
final_status = db_update_state.get('status', 'unknown')
|
||||
if final_status == 'error':
|
||||
return {'status': 'error', 'reason': db_update_state.get('error_message', 'Unknown error'), '_manages_own_progress': True}
|
||||
return {'status': 'completed', 'full_refresh': str(full), '_manages_own_progress': True}
|
||||
with db_update_lock:
|
||||
stats = {
|
||||
'status': 'completed', 'full_refresh': str(full), '_manages_own_progress': True,
|
||||
'artists': db_update_state.get('total', 0),
|
||||
'albums': db_update_state.get('total_albums', 0),
|
||||
'tracks': db_update_state.get('total_tracks', 0),
|
||||
'removed_artists': db_update_state.get('removed_artists', 0),
|
||||
'removed_albums': db_update_state.get('removed_albums', 0),
|
||||
'removed_tracks': db_update_state.get('removed_tracks', 0),
|
||||
}
|
||||
return stats
|
||||
|
||||
def _auto_deep_scan_library(config):
|
||||
global _db_update_automation_id
|
||||
|
|
@ -703,7 +726,17 @@ def _register_automation_handlers():
|
|||
final_status = db_update_state.get('status', 'unknown')
|
||||
if final_status == 'error':
|
||||
return {'status': 'error', 'reason': db_update_state.get('error_message', 'Unknown error'), '_manages_own_progress': True}
|
||||
return {'status': 'completed', '_manages_own_progress': True}
|
||||
with db_update_lock:
|
||||
stats = {
|
||||
'status': 'completed', '_manages_own_progress': True,
|
||||
'artists': db_update_state.get('total', 0),
|
||||
'albums': db_update_state.get('total_albums', 0),
|
||||
'tracks': db_update_state.get('total_tracks', 0),
|
||||
'removed_artists': db_update_state.get('removed_artists', 0),
|
||||
'removed_albums': db_update_state.get('removed_albums', 0),
|
||||
'removed_tracks': db_update_state.get('removed_tracks', 0),
|
||||
}
|
||||
return stats
|
||||
|
||||
def _auto_run_duplicate_cleaner(config):
|
||||
automation_id = config.get('_automation_id')
|
||||
|
|
@ -748,10 +781,18 @@ def _register_automation_handlers():
|
|||
|
||||
dupes = duplicate_cleaner_state.get('duplicates_found', 0)
|
||||
removed = duplicate_cleaner_state.get('deleted', 0)
|
||||
space_freed = duplicate_cleaner_state.get('space_freed', 0)
|
||||
scanned = duplicate_cleaner_state.get('files_scanned', 0)
|
||||
_update_automation_progress(automation_id, status='finished', progress=100,
|
||||
phase='Complete',
|
||||
log_line=f'Found {dupes} duplicates, removed {removed} files', log_type='success')
|
||||
return {'status': 'completed', '_manages_own_progress': True}
|
||||
return {
|
||||
'status': 'completed', '_manages_own_progress': True,
|
||||
'files_scanned': scanned,
|
||||
'duplicates_found': dupes,
|
||||
'files_deleted': removed,
|
||||
'space_freed_mb': round(space_freed / (1024 * 1024), 1),
|
||||
}
|
||||
|
||||
def _auto_clear_quarantine(config):
|
||||
import shutil as _shutil
|
||||
|
|
@ -848,7 +889,13 @@ def _register_automation_handlers():
|
|||
_update_automation_progress(automation_id, status='finished', progress=100,
|
||||
phase='Complete',
|
||||
log_line=f'Quality scan complete — {issues} issues found', log_type='success')
|
||||
return {'status': 'completed', 'scope': scope, '_manages_own_progress': True}
|
||||
return {
|
||||
'status': 'completed', 'scope': scope, '_manages_own_progress': True,
|
||||
'tracks_scanned': quality_scanner_state.get('processed', 0),
|
||||
'quality_met': quality_scanner_state.get('quality_met', 0),
|
||||
'low_quality': issues,
|
||||
'matched': quality_scanner_state.get('matched', 0),
|
||||
}
|
||||
|
||||
def _auto_backup_database(config):
|
||||
import sqlite3, glob as _glob
|
||||
|
|
@ -14175,6 +14222,8 @@ def _db_update_finished_callback(total_artists, total_albums, total_tracks, succ
|
|||
with db_update_lock:
|
||||
db_update_state["status"] = "finished"
|
||||
db_update_state["phase"] = f"Completed: {successful} successful, {failed} failed{removal_msg}."
|
||||
db_update_state["total_albums"] = total_albums
|
||||
db_update_state["total_tracks"] = total_tracks
|
||||
db_update_state["removed_artists"] = removed_artists
|
||||
db_update_state["removed_albums"] = removed_albums
|
||||
db_update_state["removed_tracks"] = removed_tracks
|
||||
|
|
|
|||
|
|
@ -16385,7 +16385,7 @@ const TOOL_HELP_CONTENT = {
|
|||
<p>Creates a timestamped backup of SoulSync's SQLite database. Uses the SQLite backup API for a safe hot-copy while the app is running.</p>
|
||||
|
||||
<h4>Retention</h4>
|
||||
<p>Keeps the last 3 backups automatically. Older backups are cleaned up to save disk space.</p>
|
||||
<p>Keeps the last 5 backups automatically. Older backups are cleaned up to save disk space.</p>
|
||||
|
||||
<h4>Good for</h4>
|
||||
<ul>
|
||||
|
|
@ -16395,6 +16395,139 @@ const TOOL_HELP_CONTENT = {
|
|||
</ul>
|
||||
`
|
||||
},
|
||||
'auto-refresh_beatport_cache': {
|
||||
title: 'Refresh Beatport Cache',
|
||||
content: `
|
||||
<h4>What does this action do?</h4>
|
||||
<p>Scrapes the Beatport homepage for top charts and caches the results locally. Keeps the Beatport charts page loading instantly without needing to scrape on every visit.</p>
|
||||
|
||||
<h4>Cache duration</h4>
|
||||
<p>Cache lasts 24 hours. This action refreshes it early so it's always warm when you visit the charts page.</p>
|
||||
|
||||
<h4>Good for</h4>
|
||||
<ul>
|
||||
<li>Keeping Beatport charts available instantly</li>
|
||||
<li>Scheduling daily cache refreshes (e.g. every morning)</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
'auto-clean_search_history': {
|
||||
title: 'Clean Search History',
|
||||
content: `
|
||||
<h4>What does this action do?</h4>
|
||||
<p>Removes old search queries from Soulseek. This keeps your search history clean and prevents buildup over time.</p>
|
||||
|
||||
<h4>Good for</h4>
|
||||
<ul>
|
||||
<li>Periodic housekeeping</li>
|
||||
<li>Keeping Soulseek search history tidy</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
'auto-clean_completed_downloads': {
|
||||
title: 'Clean Completed Downloads',
|
||||
content: `
|
||||
<h4>What does this action do?</h4>
|
||||
<p>Clears completed downloads from the transfer list and removes any empty directories left behind in the staging folder.</p>
|
||||
|
||||
<h4>Good for</h4>
|
||||
<ul>
|
||||
<li>Automatic cleanup after batch downloads</li>
|
||||
<li>Preventing staging folder clutter</li>
|
||||
<li>Chaining after a batch complete trigger</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
'auto-deep_scan_library': {
|
||||
title: 'Deep Scan Library',
|
||||
content: `
|
||||
<h4>What does this action do?</h4>
|
||||
<p>Walks your entire media server library and compares it against SoulSync's database. Adds any new tracks found and removes stale entries that no longer exist on the server.</p>
|
||||
|
||||
<h4>How is this different from Database Update?</h4>
|
||||
<ul>
|
||||
<li><strong>Database Update:</strong> Incremental — only looks for new artists/albums added since last update</li>
|
||||
<li><strong>Deep Scan:</strong> Full comparison — checks every track on the server against the database, catches anything missed</li>
|
||||
</ul>
|
||||
|
||||
<h4>Safety</h4>
|
||||
<ul>
|
||||
<li>Never overwrites existing enrichment data (genres, Spotify IDs, artwork)</li>
|
||||
<li>Only inserts tracks that don't already exist in the database</li>
|
||||
<li>Stale track removal has a 50% safety threshold — if more than half the library appears missing, removal is skipped</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
// ==================== Notification/Then-Action Help ====================
|
||||
|
||||
'auto-discord_webhook': {
|
||||
title: 'Discord Webhook',
|
||||
content: `
|
||||
<h4>What does this then-action do?</h4>
|
||||
<p>Sends a notification to a Discord channel via webhook when the automation's action completes.</p>
|
||||
|
||||
<h4>Configuration</h4>
|
||||
<ul>
|
||||
<li><strong>Webhook URL:</strong> The Discord webhook URL for your channel (found in Channel Settings → Integrations → Webhooks)</li>
|
||||
<li><strong>Message Template:</strong> Custom message with variable placeholders</li>
|
||||
</ul>
|
||||
|
||||
<h4>Available variables</h4>
|
||||
<p>Use these in your message template:</p>
|
||||
<ul>
|
||||
<li><code>{time}</code> — When the automation ran</li>
|
||||
<li><code>{name}</code> — Automation name</li>
|
||||
<li><code>{run_count}</code> — How many times this automation has run</li>
|
||||
<li><code>{status}</code> — Result status of the action</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
'auto-pushbullet': {
|
||||
title: 'Pushbullet',
|
||||
content: `
|
||||
<h4>What does this then-action do?</h4>
|
||||
<p>Sends a push notification to your phone or desktop via Pushbullet when the automation's action completes.</p>
|
||||
|
||||
<h4>Configuration</h4>
|
||||
<ul>
|
||||
<li><strong>API Key:</strong> Your Pushbullet access token (found in Pushbullet Settings → Access Tokens)</li>
|
||||
<li><strong>Message Template:</strong> Custom message with variable placeholders</li>
|
||||
</ul>
|
||||
|
||||
<h4>Available variables</h4>
|
||||
<p>Use these in your message template:</p>
|
||||
<ul>
|
||||
<li><code>{time}</code> — When the automation ran</li>
|
||||
<li><code>{name}</code> — Automation name</li>
|
||||
<li><code>{run_count}</code> — How many times this automation has run</li>
|
||||
<li><code>{status}</code> — Result status of the action</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
'auto-telegram': {
|
||||
title: 'Telegram',
|
||||
content: `
|
||||
<h4>What does this then-action do?</h4>
|
||||
<p>Sends a message to a Telegram chat via bot when the automation's action completes.</p>
|
||||
|
||||
<h4>Configuration</h4>
|
||||
<ul>
|
||||
<li><strong>Bot Token:</strong> Your Telegram bot token (from @BotFather)</li>
|
||||
<li><strong>Chat ID:</strong> The chat/group ID to send messages to</li>
|
||||
<li><strong>Message Template:</strong> Custom message with variable placeholders</li>
|
||||
</ul>
|
||||
|
||||
<h4>Available variables</h4>
|
||||
<p>Use these in your message template:</p>
|
||||
<ul>
|
||||
<li><code>{time}</code> — When the automation ran</li>
|
||||
<li><code>{name}</code> — Automation name</li>
|
||||
<li><code>{run_count}</code> — How many times this automation has run</li>
|
||||
<li><code>{status}</code> — Result status of the action</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
// ==================== Signal System Help ====================
|
||||
|
||||
|
|
@ -43723,7 +43856,7 @@ function renderAutomationCard(a) {
|
|||
const _timerTriggers = ['schedule', 'daily_time', 'weekly_time'];
|
||||
if (a.next_run && a.enabled && _timerTriggers.includes(a.trigger_type)) metaParts.push('<span class="auto-next-run" data-next="' + _escAttr(a.next_run) + '">Next: ' + _autoTimeUntil(a.next_run) + '</span>');
|
||||
if (!_timerTriggers.includes(a.trigger_type) && a.enabled) metaParts.push('Listening');
|
||||
if (a.run_count) metaParts.push('<span class="auto-runs-link" onclick="event.stopPropagation(); showAutomationHistory(' + a.id + ', \'' + _escAttr(a.name) + '\')" title="View run history">Runs: ' + a.run_count + '</span>');
|
||||
if (a.run_count) metaParts.push('<span class="auto-runs-link" onclick="event.stopPropagation(); showAutomationHistory(' + a.id + ', \'' + _escAttr(a.name) + '\', \'' + _escAttr(a.action_type || '') + '\')" title="View run history">Runs: ' + a.run_count + '</span>');
|
||||
if (a.last_error) metaParts.push('Error: ' + _esc(a.last_error));
|
||||
|
||||
const deleteBtn = a.is_system ? '' :
|
||||
|
|
@ -43964,7 +44097,88 @@ async function runAutomation(id) {
|
|||
} catch (err) { showToast('Error: ' + err.message, 'error'); }
|
||||
}
|
||||
|
||||
async function showAutomationHistory(automationId, automationName) {
|
||||
const _RESULT_DISPLAY_MAP = {
|
||||
'start_database_update': [
|
||||
{ key: 'artists', label: 'Artists' },
|
||||
{ key: 'albums', label: 'Albums' },
|
||||
{ key: 'tracks', label: 'Tracks' },
|
||||
{ key: 'removed_artists', label: 'Removed Artists', hideZero: true },
|
||||
{ key: 'removed_albums', label: 'Removed Albums', hideZero: true },
|
||||
{ key: 'removed_tracks', label: 'Removed Tracks', hideZero: true },
|
||||
],
|
||||
'deep_scan_library': [
|
||||
{ key: 'artists', label: 'Artists' },
|
||||
{ key: 'albums', label: 'Albums' },
|
||||
{ key: 'tracks', label: 'Tracks' },
|
||||
{ key: 'removed_artists', label: 'Removed Artists', hideZero: true },
|
||||
{ key: 'removed_albums', label: 'Removed Albums', hideZero: true },
|
||||
{ key: 'removed_tracks', label: 'Removed Tracks', hideZero: true },
|
||||
],
|
||||
'scan_watchlist': [
|
||||
{ key: 'artists_scanned', label: 'Artists Scanned' },
|
||||
{ key: 'successful_scans', label: 'Successful' },
|
||||
{ key: 'new_tracks_found', label: 'New Tracks' },
|
||||
{ key: 'tracks_added_to_wishlist', label: 'Added to Wishlist' },
|
||||
],
|
||||
'run_duplicate_cleaner': [
|
||||
{ key: 'files_scanned', label: 'Files Scanned' },
|
||||
{ key: 'duplicates_found', label: 'Duplicates Found' },
|
||||
{ key: 'files_deleted', label: 'Files Deleted' },
|
||||
{ key: 'space_freed_mb', label: 'Space Freed (MB)' },
|
||||
],
|
||||
'start_quality_scan': [
|
||||
{ key: 'tracks_scanned', label: 'Tracks Scanned' },
|
||||
{ key: 'quality_met', label: 'Quality Met' },
|
||||
{ key: 'low_quality', label: 'Low Quality' },
|
||||
{ key: 'matched', label: 'Added to Wishlist' },
|
||||
],
|
||||
'scan_library': [
|
||||
{ key: 'scan_duration_seconds', label: 'Duration (s)' },
|
||||
],
|
||||
'backup_database': [
|
||||
{ key: 'size_mb', label: 'Backup Size (MB)' },
|
||||
],
|
||||
'refresh_mirrored': [
|
||||
{ key: 'refreshed', label: 'Playlists Refreshed' },
|
||||
{ key: 'errors', label: 'Errors', hideZero: true },
|
||||
],
|
||||
'clear_quarantine': [
|
||||
{ key: 'removed', label: 'Items Removed' },
|
||||
],
|
||||
'cleanup_wishlist': [
|
||||
{ key: 'removed', label: 'Duplicates Removed' },
|
||||
],
|
||||
};
|
||||
|
||||
function _renderResultStats(resultJson, actionType) {
|
||||
if (!resultJson || typeof resultJson !== 'object') return '';
|
||||
var fields = _RESULT_DISPLAY_MAP[actionType];
|
||||
var items = [];
|
||||
if (fields) {
|
||||
fields.forEach(function(f) {
|
||||
var val = resultJson[f.key];
|
||||
if (val == null) return;
|
||||
if (f.hideZero && (val === 0 || val === '0')) return;
|
||||
items.push({ label: f.label, value: val });
|
||||
});
|
||||
} else {
|
||||
// Generic fallback: show all non-status, non-underscore keys
|
||||
Object.keys(resultJson).forEach(function(k) {
|
||||
if (k === 'status' || k.startsWith('_')) return;
|
||||
var label = k.replace(/_/g, ' ').replace(/\b\w/g, function(c) { return c.toUpperCase(); });
|
||||
items.push({ label: label, value: resultJson[k] });
|
||||
});
|
||||
}
|
||||
if (items.length === 0) return '';
|
||||
var html = '<div class="history-stats-grid">';
|
||||
items.forEach(function(it) {
|
||||
html += '<div class="history-stat-item"><div class="history-stat-label">' + _esc(it.label) + '</div><div class="history-stat-value">' + _esc(String(it.value)) + '</div></div>';
|
||||
});
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
async function showAutomationHistory(automationId, automationName, actionType) {
|
||||
let modal = document.getElementById('automation-history-modal');
|
||||
if (!modal) {
|
||||
modal = document.createElement('div');
|
||||
|
|
@ -44003,6 +44217,9 @@ async function showAutomationHistory(automationId, automationName) {
|
|||
if (hasLogs) html += '<span class="history-expand-icon">▼</span>';
|
||||
html += '</div>';
|
||||
if (summary) html += '<div class="history-summary">' + summary + '</div>';
|
||||
if (entry.result_json && typeof entry.result_json === 'object') {
|
||||
html += _renderResultStats(entry.result_json, actionType);
|
||||
}
|
||||
if (hasLogs) {
|
||||
html += '<div id="' + entryId + '" class="history-log-section">';
|
||||
entry.log_lines.forEach(function(log) {
|
||||
|
|
|
|||
|
|
@ -29597,6 +29597,22 @@ body {
|
|||
.history-log-error { color: #ef4444; }
|
||||
.history-log-warning { color: #facc15; }
|
||||
|
||||
.history-stats-grid {
|
||||
display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 8px; padding: 10px 14px;
|
||||
border-top: 1px solid rgba(255,255,255,0.06);
|
||||
}
|
||||
.history-stat-item {
|
||||
background: rgba(0,0,0,0.25); border-radius: 8px; padding: 8px 12px;
|
||||
}
|
||||
.history-stat-label {
|
||||
font-size: 11px; color: rgba(255,255,255,0.45); margin-bottom: 2px;
|
||||
}
|
||||
.history-stat-value {
|
||||
font-size: 14px; font-weight: 600; color: #e0e0e0;
|
||||
}
|
||||
.history-log-skip { color: rgba(255,255,255,0.35); }
|
||||
|
||||
.history-total {
|
||||
text-align: center; padding: 12px 0 4px; font-size: 11px; color: rgba(255,255,255,0.3);
|
||||
}
|
||||
Loading…
Reference in a new issue