Add activity feed and toasts to dashboard
Introduces a global activity feed system with API endpoints for recent activities and toasts, and integrates activity tracking for key backend events (downloads, syncs, database updates, etc.). Updates the dashboard frontend to periodically fetch and display activity feed items and show toasts for recent actions. Improves error logging and reduces noise for expected 404s in Soulseek client. Adds related CSS for activity feed separators.
This commit is contained in:
parent
7a7480ba60
commit
94d1c82674
4 changed files with 314 additions and 6 deletions
|
|
@ -310,10 +310,13 @@ class SoulseekClient:
|
|||
else:
|
||||
# Enhanced error logging for better debugging
|
||||
error_detail = response_text if response_text.strip() else "No error details provided"
|
||||
logger.error(f"API request failed: HTTP {response.status} ({response.reason}) - {error_detail}")
|
||||
|
||||
# Log the specific endpoint and method for debugging
|
||||
logger.debug(f"Failed request: {method} {url}")
|
||||
# Reduce noise for expected 404s during search cleanup
|
||||
if response.status == 404 and 'searches/' in url and method == 'DELETE':
|
||||
logger.debug(f"Search not found for deletion (expected): {url}")
|
||||
else:
|
||||
logger.error(f"API request failed: HTTP {response.status} ({response.reason}) - {error_detail}")
|
||||
logger.debug(f"Failed request: {method} {url}")
|
||||
|
||||
return None
|
||||
|
||||
|
|
@ -1055,7 +1058,8 @@ class SoulseekClient:
|
|||
if success:
|
||||
logger.debug(f"Successfully deleted search {search_id}")
|
||||
else:
|
||||
logger.warning(f"Failed to delete search {search_id}")
|
||||
# Don't log warnings for failed deletions - they're often just 404s for already-removed searches
|
||||
logger.debug(f"Search deletion returned false (likely already removed): {search_id}")
|
||||
|
||||
return success
|
||||
|
||||
|
|
|
|||
196
web_server.py
196
web_server.py
|
|
@ -1443,6 +1443,61 @@ def get_system_stats():
|
|||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
# Global activity tracking storage
|
||||
activity_feed = []
|
||||
activity_feed_lock = threading.Lock()
|
||||
|
||||
@app.route('/api/activity/feed')
|
||||
def get_activity_feed():
|
||||
"""Get recent activity feed for dashboard"""
|
||||
try:
|
||||
with activity_feed_lock:
|
||||
# Return last 10 activities in reverse chronological order
|
||||
return jsonify({'activities': activity_feed[-10:][::-1]})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/activity/toasts')
|
||||
def get_recent_toasts():
|
||||
"""Get recent activities that should show toasts"""
|
||||
try:
|
||||
import time
|
||||
current_time = time.time()
|
||||
|
||||
with activity_feed_lock:
|
||||
# Return activities from last 10 seconds that should show toasts
|
||||
recent_toasts = [
|
||||
activity for activity in activity_feed
|
||||
if activity.get('show_toast', True) and
|
||||
(current_time - activity.get('timestamp', 0)) <= 10
|
||||
]
|
||||
return jsonify({'toasts': recent_toasts})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
def add_activity_item(icon: str, title: str, subtitle: str, time_ago: str = "Now", show_toast: bool = True):
|
||||
"""Add activity item to the feed (replicates dashboard.py functionality)"""
|
||||
try:
|
||||
import time
|
||||
activity_item = {
|
||||
'icon': icon,
|
||||
'title': title,
|
||||
'subtitle': subtitle,
|
||||
'time': time_ago,
|
||||
'timestamp': time.time(),
|
||||
'show_toast': show_toast
|
||||
}
|
||||
|
||||
with activity_feed_lock:
|
||||
activity_feed.append(activity_item)
|
||||
# Keep only last 20 items to prevent memory growth
|
||||
if len(activity_feed) > 20:
|
||||
activity_feed.pop(0)
|
||||
|
||||
print(f"📝 Activity: {icon} {title} - {subtitle}")
|
||||
except Exception as e:
|
||||
print(f"Error adding activity item: {e}")
|
||||
|
||||
@app.route('/api/settings', methods=['GET', 'POST'])
|
||||
def handle_settings():
|
||||
global tidal_client # Declare that we might modify the global instance
|
||||
|
|
@ -1674,6 +1729,10 @@ def start_download():
|
|||
print(f"Failed to start track download: {e}")
|
||||
continue
|
||||
|
||||
# Add activity for album download start
|
||||
album_name = data.get('album_name', 'Unknown Album')
|
||||
add_activity_item("📥", "Album Download Started", f"'{album_name}' - {started_downloads} tracks", "Now")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": f"Started {started_downloads} downloads from album"
|
||||
|
|
@ -1691,6 +1750,9 @@ def start_download():
|
|||
download_id = asyncio.run(soulseek_client.download(username, filename, file_size))
|
||||
|
||||
if download_id:
|
||||
# Extract track name from filename for activity
|
||||
track_name = filename.split('/')[-1] if '/' in filename else filename.split('\\')[-1] if '\\' in filename else filename
|
||||
add_activity_item("📥", "Track Download Started", f"'{track_name}'", "Now")
|
||||
return jsonify({"success": True, "message": "Download started"})
|
||||
else:
|
||||
return jsonify({"error": "Failed to start download"}), 500
|
||||
|
|
@ -1932,6 +1994,44 @@ def clear_finished_downloads():
|
|||
print(f"Error clearing finished downloads: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@app.route('/api/searches/clear-all', methods=['POST'])
|
||||
def clear_all_searches():
|
||||
"""
|
||||
Clear all searches from slskd search history.
|
||||
"""
|
||||
try:
|
||||
success = asyncio.run(soulseek_client.clear_all_searches())
|
||||
if success:
|
||||
add_activity_item("🧹", "Search Cleanup", "All search history cleared manually", "Now")
|
||||
return jsonify({"success": True, "message": "All searches cleared."})
|
||||
else:
|
||||
return jsonify({"success": False, "error": "Backend failed to clear searches."}), 500
|
||||
except Exception as e:
|
||||
print(f"Error clearing searches: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@app.route('/api/searches/maintain', methods=['POST'])
|
||||
def maintain_search_history():
|
||||
"""
|
||||
Maintain search history by keeping only recent searches.
|
||||
"""
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
keep_searches = data.get('keep_searches', 50)
|
||||
trigger_threshold = data.get('trigger_threshold', 200)
|
||||
|
||||
success = asyncio.run(soulseek_client.maintain_search_history_with_buffer(
|
||||
keep_searches=keep_searches, trigger_threshold=trigger_threshold
|
||||
))
|
||||
if success:
|
||||
add_activity_item("🧹", "Search Maintenance", f"Search history maintained (keeping {keep_searches} searches)", "Now")
|
||||
return jsonify({"success": True, "message": f"Search history maintained (keeping {keep_searches} searches)."})
|
||||
else:
|
||||
return jsonify({"success": False, "error": "Backend failed to maintain search history."}), 500
|
||||
except Exception as e:
|
||||
print(f"Error maintaining search history: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@app.route('/api/artists')
|
||||
def get_artists():
|
||||
# Placeholder: returns mock artist data
|
||||
|
|
@ -4956,6 +5056,10 @@ def get_version_info():
|
|||
def _simple_monitor_task():
|
||||
"""The actual monitoring task that runs in the background thread."""
|
||||
print("🔄 Simple background monitor started")
|
||||
last_search_cleanup = 0 # Force initial cleanup on first run
|
||||
search_cleanup_interval = 3600 # 1 hour
|
||||
initial_cleanup_done = False
|
||||
|
||||
while True:
|
||||
try:
|
||||
with matched_context_lock:
|
||||
|
|
@ -4966,6 +5070,33 @@ def _simple_monitor_task():
|
|||
with app.app_context():
|
||||
get_download_status()
|
||||
|
||||
# Automatic search cleanup every hour (or initial cleanup)
|
||||
current_time = time.time()
|
||||
should_cleanup = (current_time - last_search_cleanup > search_cleanup_interval) or not initial_cleanup_done
|
||||
|
||||
if should_cleanup:
|
||||
try:
|
||||
if not initial_cleanup_done:
|
||||
print("🔍 [Auto Cleanup] Performing initial search cleanup in background...")
|
||||
initial_cleanup_done = True
|
||||
else:
|
||||
print("🔍 [Auto Cleanup] Starting scheduled search cleanup...")
|
||||
|
||||
success = asyncio.run(soulseek_client.maintain_search_history_with_buffer(
|
||||
keep_searches=50, trigger_threshold=200
|
||||
))
|
||||
if success:
|
||||
cleanup_type = "Initial search history maintenance" if last_search_cleanup == 0 else "Automatic search history maintenance completed"
|
||||
add_activity_item("🧹", "Search Cleanup", cleanup_type, "Now")
|
||||
print("✅ [Auto Cleanup] Search history maintenance completed")
|
||||
else:
|
||||
print("⚠️ [Auto Cleanup] Search history maintenance returned false")
|
||||
last_search_cleanup = current_time
|
||||
except Exception as cleanup_error:
|
||||
print(f"❌ [Auto Cleanup] Error in automatic search cleanup: {cleanup_error}")
|
||||
last_search_cleanup = current_time # Still update to avoid spam
|
||||
initial_cleanup_done = True # Mark as done even on error to avoid blocking
|
||||
|
||||
time.sleep(1)
|
||||
except Exception as e:
|
||||
print(f"❌ Simple monitor error: {e}")
|
||||
|
|
@ -5196,6 +5327,10 @@ def _db_update_finished_callback(total_artists, total_albums, total_tracks, succ
|
|||
db_update_state["status"] = "finished"
|
||||
db_update_state["phase"] = f"Completed: {successful} successful, {failed} failed."
|
||||
|
||||
# Add activity for database update completion
|
||||
summary = f"{total_tracks} tracks, {total_albums} albums, {total_artists} artists processed"
|
||||
add_activity_item("✅", "Database Update Complete", summary, "Now")
|
||||
|
||||
# WISHLIST CLEANUP: Automatically clean up wishlist after database update
|
||||
try:
|
||||
print("📋 [DB Update] Database update completed, starting automatic wishlist cleanup...")
|
||||
|
|
@ -5208,6 +5343,9 @@ def _db_update_error_callback(error_message):
|
|||
with db_update_lock:
|
||||
db_update_state["status"] = "error"
|
||||
db_update_state["error_message"] = error_message
|
||||
|
||||
# Add activity for database update error
|
||||
add_activity_item("❌", "Database Update Failed", error_message, "Now")
|
||||
|
||||
def _run_db_update_task(full_refresh, server_type):
|
||||
"""The actual function that runs in the background thread."""
|
||||
|
|
@ -5305,6 +5443,9 @@ def start_wishlist_missing_downloads():
|
|||
|
||||
print(f"🔧 [Manual-Wishlist] Sanitized {len(wishlist_tracks)} tracks from wishlist service")
|
||||
|
||||
# Add activity for wishlist download start
|
||||
add_activity_item("📥", "Wishlist Download Started", f"{len(wishlist_tracks)} tracks", "Now")
|
||||
|
||||
batch_id = str(uuid.uuid4())
|
||||
|
||||
# Use "wishlist" as the playlist_id for consistency in the modal system
|
||||
|
|
@ -5466,6 +5607,11 @@ def start_database_update():
|
|||
"progress": 0, "current_item": "", "processed": 0, "total": 0, "error_message": ""
|
||||
})
|
||||
|
||||
# Add activity for database update start
|
||||
update_type = "Full" if full_refresh else "Incremental"
|
||||
server_name = active_server.capitalize()
|
||||
add_activity_item("🗄️", "Database Update", f"Starting {update_type.lower()} update from {server_name}...", "Now")
|
||||
|
||||
# Submit the worker function to the executor
|
||||
db_update_executor.submit(_run_db_update_task, full_refresh, active_server)
|
||||
|
||||
|
|
@ -5895,6 +6041,10 @@ def _process_failed_tracks_to_wishlist_exact_with_auto_completion(batch_id):
|
|||
total_failed = completion_summary.get('total_failed', 0)
|
||||
print(f"🎉 [Auto-Wishlist] Background processing complete: {tracks_added} added to wishlist, {total_failed} failed")
|
||||
|
||||
# Add activity for wishlist processing
|
||||
if tracks_added > 0:
|
||||
add_activity_item("⭐", "Wishlist Updated", f"{tracks_added} failed tracks added to wishlist", "Now")
|
||||
|
||||
# Mark auto-processing as complete
|
||||
with wishlist_timer_lock:
|
||||
wishlist_auto_processing = False
|
||||
|
|
@ -5952,9 +6102,11 @@ def _on_download_completed(batch_id, task_id, success=True):
|
|||
if task_status == 'cancelled':
|
||||
download_batches[batch_id]['cancelled_tracks'].add(task.get('track_index', 0))
|
||||
print(f"🚫 [Batch Manager] Added cancelled track to batch tracking: {track_info['track_name']}")
|
||||
add_activity_item("🚫", "Download Cancelled", f"'{track_info['track_name']}'", "Now")
|
||||
elif task_status == 'failed':
|
||||
download_batches[batch_id]['permanently_failed_tracks'].append(track_info)
|
||||
print(f"❌ [Batch Manager] Added failed track to batch tracking: {track_info['track_name']}")
|
||||
add_activity_item("❌", "Download Failed", f"'{track_info['track_name']}'", "Now")
|
||||
|
||||
# WISHLIST REMOVAL: Handle successful downloads for wishlist removal
|
||||
if success and task_id in download_tasks:
|
||||
|
|
@ -5963,6 +6115,11 @@ def _on_download_completed(batch_id, task_id, success=True):
|
|||
track_info = task.get('track_info', {})
|
||||
print(f"📋 [Batch Manager] Successful download - checking wishlist removal for task {task_id}")
|
||||
|
||||
# Add activity for successful download
|
||||
track_name = track_info.get('name', 'Unknown Track')
|
||||
artist_name = track_info.get('artists', [{}])[0].get('name', 'Unknown Artist') if track_info.get('artists') else 'Unknown Artist'
|
||||
add_activity_item("📥", "Download Complete", f"'{track_name}' by {artist_name}", "Now")
|
||||
|
||||
# Try to remove from wishlist using track info
|
||||
if track_info:
|
||||
# Create a context-like structure for the wishlist removal function
|
||||
|
|
@ -6019,6 +6176,11 @@ def _on_download_completed(batch_id, task_id, success=True):
|
|||
# Mark batch as complete and process wishlist outside of lock to prevent deadlocks
|
||||
batch['phase'] = 'complete'
|
||||
|
||||
# Add activity for batch completion
|
||||
playlist_name = batch.get('playlist_name', 'Unknown Playlist')
|
||||
successful_downloads = finished_count - len(batch.get('permanently_failed_tracks', []))
|
||||
add_activity_item("✅", "Download Batch Complete", f"'{playlist_name}' - {successful_downloads} tracks downloaded", "Now")
|
||||
|
||||
# Update YouTube playlist phase to 'download_complete' if this is a YouTube playlist
|
||||
playlist_id = batch.get('playlist_id')
|
||||
if playlist_id and playlist_id.startswith('youtube_'):
|
||||
|
|
@ -6892,6 +7054,10 @@ def start_playlist_missing_downloads(playlist_id):
|
|||
if not missing_tracks:
|
||||
return jsonify({"success": False, "error": "No missing tracks provided"}), 400
|
||||
|
||||
# Add activity for playlist download missing start
|
||||
playlist_name = data.get('playlist_name', f'Playlist {playlist_id}')
|
||||
add_activity_item("📥", "Missing Tracks Download Started", f"'{playlist_name}' - {len(missing_tracks)} tracks", "Now")
|
||||
|
||||
try:
|
||||
batch_id = str(uuid.uuid4())
|
||||
|
||||
|
|
@ -7519,7 +7685,8 @@ def cancel_task_v2():
|
|||
|
||||
print(f"🔍 [Atomic Cancel] Task {task_id} state: status='{current_status}', original_status='{original_status}', download_id='{download_id}', username='{username}'")
|
||||
print(f"🔍 [Atomic Cancel] Download ID type: {type(download_id)}, length: {len(str(download_id)) if download_id else 0}")
|
||||
print(f"🔍 [Atomic Cancel] Download ID looks like filename: {download_id and ('/' in str(download_id) or '\\' in str(download_id))}")
|
||||
backslash = '\\'
|
||||
print(f"🔍 [Atomic Cancel] Download ID looks like filename: {download_id and ('/' in str(download_id) or backslash in str(download_id))}")
|
||||
|
||||
if download_id and username:
|
||||
# Always try to cancel in slskd - doesn't matter what status it was
|
||||
|
|
@ -8641,6 +8808,9 @@ def start_tidal_sync(playlist_id):
|
|||
sync_playlist_id = f"tidal_{playlist_id}"
|
||||
playlist_name = state['playlist'].name # Tidal playlist object has .name attribute
|
||||
|
||||
# Add activity for sync start
|
||||
add_activity_item("🔄", "Tidal Sync Started", f"'{playlist_name}' - {len(spotify_tracks)} tracks", "Now")
|
||||
|
||||
# Update Tidal state
|
||||
state['phase'] = 'syncing'
|
||||
state['sync_playlist_id'] = sync_playlist_id
|
||||
|
|
@ -8697,8 +8867,13 @@ def get_tidal_sync_status(playlist_id):
|
|||
if sync_state.get('status') == 'finished':
|
||||
state['phase'] = 'sync_complete'
|
||||
state['sync_progress'] = sync_state.get('progress', {})
|
||||
# Add activity for sync completion
|
||||
playlist_name = state.get('playlist', {}).get('name', 'Unknown Playlist')
|
||||
add_activity_item("🔄", "Sync Complete", f"Tidal playlist '{playlist_name}' synced successfully", "Now")
|
||||
elif sync_state.get('status') == 'error':
|
||||
state['phase'] = 'discovered' # Revert on error
|
||||
playlist_name = state.get('playlist', {}).get('name', 'Unknown Playlist')
|
||||
add_activity_item("❌", "Sync Failed", f"Tidal playlist '{playlist_name}' sync failed", "Now")
|
||||
|
||||
return jsonify(response)
|
||||
|
||||
|
|
@ -9025,6 +9200,9 @@ def start_youtube_sync(url_hash):
|
|||
sync_playlist_id = f"youtube_{url_hash}"
|
||||
playlist_name = state['playlist']['name']
|
||||
|
||||
# Add activity for sync start
|
||||
add_activity_item("🔄", "YouTube Sync Started", f"'{playlist_name}' - {len(spotify_tracks)} tracks", "Now")
|
||||
|
||||
# Update YouTube state
|
||||
state['phase'] = 'syncing'
|
||||
state['sync_playlist_id'] = sync_playlist_id
|
||||
|
|
@ -9081,8 +9259,13 @@ def get_youtube_sync_status(url_hash):
|
|||
if sync_state.get('status') == 'finished':
|
||||
state['phase'] = 'sync_complete'
|
||||
state['sync_progress'] = sync_state.get('progress', {})
|
||||
# Add activity for sync completion
|
||||
playlist_name = state.get('playlist', {}).get('name', 'Unknown Playlist')
|
||||
add_activity_item("🔄", "Sync Complete", f"YouTube playlist '{playlist_name}' synced successfully", "Now")
|
||||
elif sync_state.get('status') == 'error':
|
||||
state['phase'] = 'discovered' # Revert on error
|
||||
playlist_name = state.get('playlist', {}).get('name', 'Unknown Playlist')
|
||||
add_activity_item("❌", "Sync Failed", f"YouTube playlist '{playlist_name}' sync failed", "Now")
|
||||
|
||||
return jsonify(response)
|
||||
|
||||
|
|
@ -9531,6 +9714,9 @@ def start_playlist_sync():
|
|||
if not all([playlist_id, playlist_name, tracks_json]):
|
||||
return jsonify({"success": False, "error": "Missing playlist_id, name, or tracks."}), 400
|
||||
|
||||
# Add activity for sync start
|
||||
add_activity_item("🔄", "Spotify Sync Started", f"'{playlist_name}' - {len(tracks_json)} tracks", "Now")
|
||||
|
||||
print(f"⏱️ [TIMING] Request parsed at {time.strftime('%H:%M:%S')} (took {(time.time()-request_start_time)*1000:.1f}ms)")
|
||||
|
||||
with sync_lock:
|
||||
|
|
@ -10269,7 +10455,7 @@ if __name__ == '__main__':
|
|||
# Start simple background monitor when server starts
|
||||
print("🔧 Starting simple background monitor...")
|
||||
start_simple_background_monitor()
|
||||
print("✅ Simple background monitor started")
|
||||
print("✅ Simple background monitor started (includes automatic search cleanup)")
|
||||
|
||||
# Start automatic wishlist processing when server starts
|
||||
print("🔧 Starting automatic wishlist processing...")
|
||||
|
|
@ -10285,4 +10471,10 @@ if __name__ == '__main__':
|
|||
import time
|
||||
app.start_time = time.time()
|
||||
|
||||
# Add startup activity
|
||||
add_activity_item("🚀", "System Started", "SoulSync Web UI Server initialized", "Now")
|
||||
|
||||
# Add a test activity to verify the system is working
|
||||
add_activity_item("🔧", "Debug Test", "Activity feed system test", "Now")
|
||||
|
||||
app.run(host='0.0.0.0', port=5001, debug=True)
|
||||
|
|
|
|||
|
|
@ -6859,6 +6859,15 @@ async function loadDashboardData() {
|
|||
// Start periodic refresh of service status and system stats (every 10 seconds)
|
||||
setInterval(fetchAndUpdateServiceStatus, 10000);
|
||||
setInterval(fetchAndUpdateSystemStats, 10000);
|
||||
|
||||
// Initial load of activity feed
|
||||
await fetchAndUpdateActivityFeed();
|
||||
|
||||
// Start periodic refresh of activity feed (every 5 seconds for responsiveness)
|
||||
setInterval(fetchAndUpdateActivityFeed, 5000);
|
||||
|
||||
// Start periodic toast checking (every 3 seconds)
|
||||
setInterval(checkForActivityToasts, 3000);
|
||||
|
||||
// Also check the status of any ongoing update when the page loads
|
||||
await checkAndUpdateDbProgress();
|
||||
|
|
@ -12090,6 +12099,103 @@ function updateStatCard(cardId, value, subtitle) {
|
|||
}
|
||||
}
|
||||
|
||||
async function fetchAndUpdateActivityFeed() {
|
||||
try {
|
||||
const response = await fetch('/api/activity/feed');
|
||||
if (!response.ok) {
|
||||
console.warn('Activity feed response not ok:', response.status, response.statusText);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Activity feed data received:', data);
|
||||
updateActivityFeed(data.activities || []);
|
||||
|
||||
} catch (error) {
|
||||
console.warn('Could not fetch activity feed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function updateActivityFeed(activities) {
|
||||
const feedContainer = document.getElementById('dashboard-activity-feed');
|
||||
if (!feedContainer) {
|
||||
console.warn('Activity feed container not found!');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Updating activity feed with', activities.length, 'activities:', activities);
|
||||
|
||||
// Clear existing content
|
||||
feedContainer.innerHTML = '';
|
||||
|
||||
if (activities.length === 0) {
|
||||
console.log('No activities found, showing placeholder');
|
||||
// Show placeholder if no activities
|
||||
feedContainer.innerHTML = `
|
||||
<div class="activity-item">
|
||||
<span class="activity-icon">📊</span>
|
||||
<div class="activity-text-content">
|
||||
<p class="activity-title">System Started</p>
|
||||
<p class="activity-subtitle">Dashboard initialized successfully</p>
|
||||
</div>
|
||||
<p class="activity-time">Now</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Add activities (limit to 5 most recent)
|
||||
activities.slice(0, 5).forEach((activity, index) => {
|
||||
const activityElement = document.createElement('div');
|
||||
activityElement.className = 'activity-item';
|
||||
activityElement.innerHTML = `
|
||||
<span class="activity-icon">${escapeHtml(activity.icon)}</span>
|
||||
<div class="activity-text-content">
|
||||
<p class="activity-title">${escapeHtml(activity.title)}</p>
|
||||
<p class="activity-subtitle">${escapeHtml(activity.subtitle)}</p>
|
||||
</div>
|
||||
<p class="activity-time">${escapeHtml(activity.time)}</p>
|
||||
`;
|
||||
|
||||
feedContainer.appendChild(activityElement);
|
||||
|
||||
// Add separator between items (except after last item)
|
||||
if (index < activities.slice(0, 5).length - 1) {
|
||||
const separator = document.createElement('div');
|
||||
separator.className = 'activity-separator';
|
||||
feedContainer.appendChild(separator);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function checkForActivityToasts() {
|
||||
try {
|
||||
const response = await fetch('/api/activity/toasts');
|
||||
if (!response.ok) return;
|
||||
|
||||
const data = await response.json();
|
||||
const toasts = data.toasts || [];
|
||||
|
||||
toasts.forEach(activity => {
|
||||
// Convert activity to toast type based on icon/title
|
||||
let toastType = 'info';
|
||||
if (activity.icon === '✅' || activity.title.includes('Complete')) {
|
||||
toastType = 'success';
|
||||
} else if (activity.icon === '❌' || activity.title.includes('Failed') || activity.title.includes('Error')) {
|
||||
toastType = 'error';
|
||||
} else if (activity.icon === '🚫' || activity.title.includes('Cancelled')) {
|
||||
toastType = 'warning';
|
||||
}
|
||||
|
||||
// Show toast with activity info
|
||||
showToast(`${activity.title}: ${activity.subtitle}`, toastType);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
// Silently fail for toast checking to avoid spam
|
||||
}
|
||||
}
|
||||
|
||||
// --- Watchlist Functions ---
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -823,6 +823,12 @@ body {
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
.activity-separator {
|
||||
height: 1px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
|
|
|
|||
Loading…
Reference in a new issue