Fixed Spotify stuff
This commit is contained in:
@@ -1,7 +1,17 @@
|
|||||||
"""
|
"""
|
||||||
Unified import helper for importing music content across different services.
|
Unified import helper for importing music content across different services.
|
||||||
This module provides consistent import functionality for tracks, albums, and playlists
|
This module provides consistent import functionality for tracks, albums, and playlists
|
||||||
from various music streaming services like Spotify and Deezer.
|
from various music streaming services like Spoti try:
|
||||||
|
imported_songs = ImportHelper.import_spotify_playlist(spotify_client, item_id)
|
||||||
|
if not imported_songs or imported_songs.get('imported_count', 0) == 0:
|
||||||
|
current_app.logger.warning(f"Spotify playlist import returned empty result for playlist ID: {item_id}")
|
||||||
|
return {
|
||||||
|
'imported_count': 0,
|
||||||
|
'skipped_count': 0,
|
||||||
|
'error_count': 1,
|
||||||
|
'errors': [f"No songs found in Spotify playlist {item_id} or playlist import failed."]
|
||||||
|
}
|
||||||
|
return imported_songser.
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
@@ -240,7 +250,7 @@ class ImportHelper:
|
|||||||
elif item_type.lower() == 'playlist':
|
elif item_type.lower() == 'playlist':
|
||||||
try:
|
try:
|
||||||
imported_songs = ImportHelper.import_spotify_playlist(spotify_client, item_id)
|
imported_songs = ImportHelper.import_spotify_playlist(spotify_client, item_id)
|
||||||
if not imported_songs or len(imported_songs) == 0:
|
if not imported_songs or imported_songs.get('imported_count', 0) == 0:
|
||||||
current_app.logger.warning(f"Spotify playlist import returned empty result for playlist ID: {item_id}")
|
current_app.logger.warning(f"Spotify playlist import returned empty result for playlist ID: {item_id}")
|
||||||
return {
|
return {
|
||||||
'imported_count': 0,
|
'imported_count': 0,
|
||||||
@@ -278,8 +288,9 @@ class ImportHelper:
|
|||||||
'error_count': 1,
|
'error_count': 1,
|
||||||
'errors': ["Deezer client not configured."]
|
'errors': ["Deezer client not configured."]
|
||||||
}
|
}
|
||||||
|
lastfm_api_key = current_app.config.get('LASTFM_API_KEY')
|
||||||
|
|
||||||
lastfm_api_key = current_app.config.get('LASTFM_API_KEY') if item_type.lower() == 'track':
|
if item_type.lower() == 'track':
|
||||||
song, was_new = deezer_client.import_track(item_id, lastfm_api_key=lastfm_api_key)
|
song, was_new = deezer_client.import_track(item_id, lastfm_api_key=lastfm_api_key)
|
||||||
if song:
|
if song:
|
||||||
if was_new:
|
if was_new:
|
||||||
@@ -535,9 +546,9 @@ class ImportHelper:
|
|||||||
# Fetch audio features
|
# Fetch audio features
|
||||||
ImportHelper._fetch_audio_features_for_song(sp, song, track_id, token=authlib_token_for_request) # Pass token
|
ImportHelper._fetch_audio_features_for_song(sp, song, track_id, token=authlib_token_for_request) # Pass token
|
||||||
|
|
||||||
# Final commit
|
# Final commit db.session.commit()
|
||||||
db.session.commit()
|
|
||||||
result['imported_count'] += 1
|
result['imported_count'] += 1
|
||||||
|
result['song_id'] = song.id # Add the song ID to the result
|
||||||
current_app.logger.info(f"Successfully imported Spotify track {track_id} as '{song.title}' with ID {song.id}")
|
current_app.logger.info(f"Successfully imported Spotify track {track_id} as '{song.title}' with ID {song.id}")
|
||||||
else:
|
else:
|
||||||
result['errors'].append(f"Failed to create song object for track {track_id}")
|
result['errors'].append(f"Failed to create song object for track {track_id}")
|
||||||
@@ -703,7 +714,8 @@ class ImportHelper:
|
|||||||
'imported_count': 0,
|
'imported_count': 0,
|
||||||
'skipped_count': 0,
|
'skipped_count': 0,
|
||||||
'error_count': 0,
|
'error_count': 0,
|
||||||
'errors': []
|
'errors': [],
|
||||||
|
'imported_song_ids': [] # Track IDs of successfully imported songs
|
||||||
}
|
}
|
||||||
|
|
||||||
if not current_user or not current_user.is_authenticated or not current_user.spotify_token:
|
if not current_user or not current_user.is_authenticated or not current_user.spotify_token:
|
||||||
@@ -783,7 +795,6 @@ class ImportHelper:
|
|||||||
result['errors'].append(f"Found a track with no ID in playlist {playlist_id}.")
|
result['errors'].append(f"Found a track with no ID in playlist {playlist_id}.")
|
||||||
result['error_count'] +=1
|
result['error_count'] +=1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Call import_spotify_track. It will handle its own token.
|
# Call import_spotify_track. It will handle its own token.
|
||||||
track_import_result = ImportHelper.import_spotify_track(sp, track_id)
|
track_import_result = ImportHelper.import_spotify_track(sp, track_id)
|
||||||
|
|
||||||
@@ -793,6 +804,10 @@ class ImportHelper:
|
|||||||
if track_import_result.get('errors'):
|
if track_import_result.get('errors'):
|
||||||
result['errors'].extend(track_import_result['errors'])
|
result['errors'].extend(track_import_result['errors'])
|
||||||
|
|
||||||
|
# Track the song ID if it was successfully imported
|
||||||
|
if track_import_result.get('imported_count', 0) > 0 and track_import_result.get('song_id'):
|
||||||
|
result['imported_song_ids'].append(track_import_result['song_id'])
|
||||||
|
|
||||||
tracks_url = tracks_data.get('next')
|
tracks_url = tracks_data.get('next')
|
||||||
if tracks_url:
|
if tracks_url:
|
||||||
current_app.logger.info(f"Next page of tracks for playlist '{playlist_name}' at: {tracks_url}")
|
current_app.logger.info(f"Next page of tracks for playlist '{playlist_name}' at: {tracks_url}")
|
||||||
|
|||||||
+52
-3
@@ -323,9 +323,58 @@ class SystemSetting(db.Model):
|
|||||||
db.session.add(setting)
|
db.session.add(setting)
|
||||||
else:
|
else:
|
||||||
setting.value = value
|
setting.value = value
|
||||||
db.session.commit()
|
db.session.commit() @staticmethod
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def all_settings():
|
def all_settings():
|
||||||
return {s.key: s.value for s in SystemSetting.query.all()}
|
return {s.key: s.value for s in SystemSetting.query.all()}
|
||||||
|
|
||||||
|
|
||||||
|
class ImportJobRecord(db.Model):
|
||||||
|
"""
|
||||||
|
Database model for tracking import jobs
|
||||||
|
"""
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
service_name = db.Column(db.String(50), nullable=False)
|
||||||
|
item_type = db.Column(db.String(20), nullable=False)
|
||||||
|
item_id = db.Column(db.String(255), nullable=False)
|
||||||
|
priority = db.Column(db.Integer, nullable=False, default=10)
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||||
|
status = db.Column(db.String(20), default='pending') # pending, processing, completed, failed
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
|
started_at = db.Column(db.DateTime)
|
||||||
|
completed_at = db.Column(db.DateTime)
|
||||||
|
error_message = db.Column(db.Text)
|
||||||
|
imported_count = db.Column(db.Integer, default=0)
|
||||||
|
skipped_count = db.Column(db.Integer, default=0)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
user = db.relationship('User', backref=db.backref('import_jobs', lazy=True))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"ImportJobRecord(id={self.id}, service={self.service_name}, type={self.item_type}, item_id={self.item_id}, status={self.status})"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def duration(self):
|
||||||
|
"""Calculate the job duration in seconds."""
|
||||||
|
if self.started_at and self.completed_at:
|
||||||
|
return (self.completed_at - self.started_at).total_seconds()
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def item_url(self):
|
||||||
|
"""Generate a URL to the imported item based on service and type."""
|
||||||
|
if self.service_name == 'spotify':
|
||||||
|
if self.item_type == 'playlist':
|
||||||
|
return f"https://open.spotify.com/playlist/{self.item_id}"
|
||||||
|
elif self.item_type == 'album':
|
||||||
|
return f"https://open.spotify.com/album/{self.item_id}"
|
||||||
|
elif self.item_type == 'track':
|
||||||
|
return f"https://open.spotify.com/track/{self.item_id}"
|
||||||
|
elif self.service_name == 'deezer':
|
||||||
|
if self.item_type == 'playlist':
|
||||||
|
return f"https://www.deezer.com/playlist/{self.item_id}"
|
||||||
|
elif self.item_type == 'album':
|
||||||
|
return f"https://www.deezer.com/album/{self.item_id}"
|
||||||
|
elif self.item_type == 'track':
|
||||||
|
return f"https://www.deezer.com/track/{self.item_id}"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -353,22 +353,56 @@ def get_songs_from_deezer_playlist(playlist_id):
|
|||||||
def get_songs_from_spotify_playlist(playlist_id):
|
def get_songs_from_spotify_playlist(playlist_id):
|
||||||
"""
|
"""
|
||||||
Fetch songs from a Spotify playlist, properly import them with metadata, and return them
|
Fetch songs from a Spotify playlist, properly import them with metadata, and return them
|
||||||
|
Always returns the songs in the playlist, even if all already exist in the DB.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
songs_per_round = current_app.config.get('SONGS_PER_ROUND', 10)
|
songs_per_round = current_app.config.get('SONGS_PER_ROUND', 10)
|
||||||
|
import_result = ImportHelper.import_item(
|
||||||
imported_songs = ImportHelper.import_item(
|
|
||||||
item_id=playlist_id,
|
item_id=playlist_id,
|
||||||
item_type='playlist',
|
item_type='playlist',
|
||||||
source='spotify',
|
service_name='spotify',
|
||||||
oauth_spotify=oauth.spotify
|
oauth_spotify=oauth.spotify
|
||||||
)
|
)
|
||||||
|
|
||||||
if not imported_songs:
|
from musicround.models import Song
|
||||||
current_app.logger.warning(f"No songs returned from ImportHelper.import_item for Spotify playlist {playlist_id}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
# If we have imported_song_ids, use them (these are DB IDs)
|
||||||
|
if import_result.get('imported_song_ids'):
|
||||||
|
song_db_ids = import_result['imported_song_ids']
|
||||||
|
imported_songs = Song.query.filter(Song.id.in_(song_db_ids)).all()
|
||||||
return imported_songs[:songs_per_round]
|
return imported_songs[:songs_per_round]
|
||||||
|
|
||||||
|
# If no imported_song_ids, fetch all Spotify IDs from the playlist and get those songs from DB
|
||||||
|
# Use the Spotify API directly to get the playlist track IDs
|
||||||
|
sp = oauth.spotify
|
||||||
|
# Get playlist tracks (paginated)
|
||||||
|
all_spotify_ids = []
|
||||||
|
next_url = f'playlists/{playlist_id}/tracks'
|
||||||
|
authlib_token = {
|
||||||
|
'access_token': current_user.spotify_token,
|
||||||
|
'refresh_token': current_user.spotify_refresh_token,
|
||||||
|
'token_type': 'Bearer',
|
||||||
|
'expires_at': int(current_user.spotify_token_expiry.timestamp()) if current_user.spotify_token_expiry else None
|
||||||
|
}
|
||||||
|
while next_url:
|
||||||
|
resp = sp.get(next_url, token=authlib_token)
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
for item in data.get('items', []):
|
||||||
|
track = item.get('track')
|
||||||
|
if track and track.get('id'):
|
||||||
|
all_spotify_ids.append(track['id'])
|
||||||
|
next_url = data.get('next')
|
||||||
|
# If next_url is a full URL, convert to relative for sp.get
|
||||||
|
if next_url and next_url.startswith('https://api.spotify.com/v1/'):
|
||||||
|
next_url = next_url.replace('https://api.spotify.com/v1/', '')
|
||||||
|
if not all_spotify_ids:
|
||||||
|
current_app.logger.warning(f"No valid tracks found in Spotify playlist {playlist_id}")
|
||||||
|
return []
|
||||||
|
# Query all songs in DB with those Spotify IDs, preserving playlist order
|
||||||
|
songs_by_spotify_id = {s.spotify_id: s for s in Song.query.filter(Song.spotify_id.in_(all_spotify_ids)).all()}
|
||||||
|
ordered_songs = [songs_by_spotify_id[sid] for sid in all_spotify_ids if sid in songs_by_spotify_id]
|
||||||
|
return ordered_songs[:songs_per_round]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error(f"Error fetching or importing Spotify playlist {playlist_id}: {e}")
|
current_app.logger.error(f"Error fetching or importing Spotify playlist {playlist_id}: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import time
|
|||||||
import random
|
import random
|
||||||
from datetime import datetime # Add datetime import
|
from datetime import datetime # Add datetime import
|
||||||
from flask import Blueprint, render_template, redirect, url_for, request, current_app, flash, session, jsonify
|
from flask import Blueprint, render_template, redirect, url_for, request, current_app, flash, session, jsonify
|
||||||
from flask_login import current_user
|
from flask_login import current_user, login_required
|
||||||
from musicround.models import Song, db
|
from musicround.models import Song, db
|
||||||
from musicround.routes.import_songs import import_pl
|
from musicround.routes.import_songs import import_pl
|
||||||
from musicround.helpers.import_helper import ImportHelper
|
from musicround.helpers.import_helper import ImportHelper
|
||||||
@@ -140,20 +140,34 @@ def import_official_playlists():
|
|||||||
elif not auth_token:
|
elif not auth_token:
|
||||||
flash("No active Spotify session. Please connect your Spotify account.", "warning")
|
flash("No active Spotify session. Please connect your Spotify account.", "warning")
|
||||||
return redirect(url_for('users.spotify_link'))
|
return redirect(url_for('users.spotify_link'))
|
||||||
|
|
||||||
# Handle POST request for importing a playlist
|
# Handle POST request for importing a playlist
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
playlist_id = request.form['playlist_id']
|
playlist_id = request.form['playlist_id']
|
||||||
result = ImportHelper.import_item('spotify', 'playlist', playlist_id)
|
|
||||||
|
|
||||||
if result['imported_count'] > 0:
|
# Check if user is authenticated for queue system
|
||||||
flash(f'Successfully imported {result["imported_count"]} songs from official Spotify playlist!', 'success')
|
if not current_user.is_authenticated:
|
||||||
elif result['skipped_count'] > 0 and result['error_count'] == 0:
|
flash("Please log in to import playlists.", "warning")
|
||||||
flash(f'All {result["skipped_count"]} songs were already in the database.', 'info')
|
return redirect(url_for('users.login'))
|
||||||
elif result['error_count'] > 0:
|
|
||||||
flash(f'Encountered {result["error_count"]} errors during import.', 'warning')
|
# Get the import queue from app config
|
||||||
else:
|
queue = current_app.config.get('import_queue')
|
||||||
flash(f'Error importing playlist: {", ".join(result["errors"])}', 'danger')
|
if not queue:
|
||||||
|
flash("Import queue not initialized.", "danger")
|
||||||
|
return redirect(url_for('core.view_songs'))
|
||||||
|
|
||||||
|
# Create import job and add to queue
|
||||||
|
from musicround.helpers.import_queue import ImportJob
|
||||||
|
priority = int(request.form.get('priority', 10))
|
||||||
|
|
||||||
|
job = ImportJob(
|
||||||
|
priority=priority,
|
||||||
|
service_name='spotify',
|
||||||
|
item_type='playlist',
|
||||||
|
item_id=playlist_id,
|
||||||
|
user_id=current_user.id,
|
||||||
|
)
|
||||||
|
queue.add_job(job)
|
||||||
|
flash('Official Spotify playlist import queued successfully. You will be notified when it completes.', 'info')
|
||||||
|
|
||||||
return redirect(url_for('core.view_songs'))
|
return redirect(url_for('core.view_songs'))
|
||||||
|
|
||||||
@@ -280,21 +294,34 @@ def direct_official_playlists():
|
|||||||
# Initialize direct Spotify client with bearer token
|
# Initialize direct Spotify client with bearer token
|
||||||
from musicround.helpers.spotify_direct import SpotifyDirectClient
|
from musicround.helpers.spotify_direct import SpotifyDirectClient
|
||||||
direct_client = SpotifyDirectClient(bearer_token=bearer_token)
|
direct_client = SpotifyDirectClient(bearer_token=bearer_token)
|
||||||
|
|
||||||
# Handle POST request for importing a playlist
|
# Handle POST request for importing a playlist
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
playlist_id = request.form['playlist_id']
|
playlist_id = request.form['playlist_id']
|
||||||
# Use the new unified ImportHelper
|
|
||||||
result = ImportHelper.import_item('spotify', 'playlist', playlist_id)
|
|
||||||
|
|
||||||
if result['imported_count'] > 0:
|
# Check if user is authenticated for queue system
|
||||||
flash(f'Successfully imported {result["imported_count"]} songs from official Spotify playlist!', 'success')
|
if not current_user.is_authenticated:
|
||||||
elif result['skipped_count'] > 0 and result['error_count'] == 0:
|
flash("Please log in to import playlists.", "warning")
|
||||||
flash(f'All {result["skipped_count"]} songs were already in the database.', 'info')
|
return redirect(url_for('users.login'))
|
||||||
elif result['error_count'] > 0:
|
|
||||||
flash(f'Encountered {result["error_count"]} errors during import.', 'warning')
|
# Get the import queue from app config
|
||||||
else:
|
queue = current_app.config.get('import_queue')
|
||||||
flash(f'Error importing playlist: {", ".join(result["errors"])}', 'danger')
|
if not queue:
|
||||||
|
flash("Import queue not initialized.", "danger")
|
||||||
|
return redirect(url_for('core.view_songs'))
|
||||||
|
|
||||||
|
# Create import job and add to queue
|
||||||
|
from musicround.helpers.import_queue import ImportJob
|
||||||
|
priority = int(request.form.get('priority', 10))
|
||||||
|
|
||||||
|
job = ImportJob(
|
||||||
|
priority=priority,
|
||||||
|
service_name='spotify',
|
||||||
|
item_type='playlist',
|
||||||
|
item_id=playlist_id,
|
||||||
|
user_id=current_user.id,
|
||||||
|
)
|
||||||
|
queue.add_job(job)
|
||||||
|
flash('Direct Spotify playlist import queued successfully. You will be notified when it completes.', 'info')
|
||||||
|
|
||||||
return redirect(url_for('core.view_songs'))
|
return redirect(url_for('core.view_songs'))
|
||||||
|
|
||||||
@@ -714,3 +741,94 @@ def update_direct_token():
|
|||||||
flash(f'Error validating token: {str(e)}', 'error')
|
flash(f'Error validating token: {str(e)}', 'error')
|
||||||
|
|
||||||
return redirect(return_url)
|
return redirect(return_url)
|
||||||
|
|
||||||
|
|
||||||
|
@import_bp.route('/queue-status')
|
||||||
|
@login_required
|
||||||
|
def queue_status():
|
||||||
|
"""
|
||||||
|
Display real-time status of the import queue for administrators
|
||||||
|
"""
|
||||||
|
# Check if user is an admin
|
||||||
|
if not current_user.is_admin():
|
||||||
|
flash('Admin access required for Import Queue view.', 'danger')
|
||||||
|
return redirect(url_for('core.index'))
|
||||||
|
|
||||||
|
# Helper function to get current time
|
||||||
|
from datetime import datetime
|
||||||
|
def now():
|
||||||
|
return datetime.utcnow()
|
||||||
|
|
||||||
|
# Get the import queue from app config
|
||||||
|
queue = current_app.config.get('import_queue')
|
||||||
|
if not queue:
|
||||||
|
flash("Import queue not initialized.", "danger")
|
||||||
|
return redirect(url_for('core.view_songs'))
|
||||||
|
|
||||||
|
# Access queue internals for display - this won't modify the queue
|
||||||
|
queue_size = queue._queue.qsize()
|
||||||
|
|
||||||
|
# Extract information about jobs in the queue (without removing them)
|
||||||
|
# This is a bit of a hack but necessary to see what's in the PriorityQueue
|
||||||
|
# without removing items
|
||||||
|
queue_snapshot = []
|
||||||
|
if hasattr(queue._queue, 'queue'):
|
||||||
|
# Make a copy of the internal queue list
|
||||||
|
with queue._lock: # Ensure thread safety while accessing the queue
|
||||||
|
queue_items = list(queue._queue.queue)
|
||||||
|
|
||||||
|
for priority, counter, job in queue_items:
|
||||||
|
queue_snapshot.append({
|
||||||
|
'priority': priority,
|
||||||
|
'counter': counter,
|
||||||
|
'service': job.service_name,
|
||||||
|
'type': job.item_type,
|
||||||
|
'item_id': job.item_id,
|
||||||
|
'user_id': job.user_id
|
||||||
|
})
|
||||||
|
|
||||||
|
# Get active and recent jobs from database if available
|
||||||
|
active_jobs = []
|
||||||
|
recent_jobs = []
|
||||||
|
|
||||||
|
# Check if ImportJobRecord is defined
|
||||||
|
try:
|
||||||
|
from musicround.models import ImportJobRecord
|
||||||
|
|
||||||
|
# Get last 50 jobs from the database, sorted by most recent first
|
||||||
|
recent_jobs = ImportJobRecord.query.order_by(ImportJobRecord.created_at.desc()).limit(50).all()
|
||||||
|
|
||||||
|
# Get the active jobs (status='processing')
|
||||||
|
active_jobs = ImportJobRecord.query.filter_by(status='processing').all()
|
||||||
|
except (ImportError, AttributeError):
|
||||||
|
# ImportJobRecord might not be defined yet, handle this case
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Get some basic stats
|
||||||
|
stats = {
|
||||||
|
'queue_size': queue_size,
|
||||||
|
'active_jobs': len(active_jobs),
|
||||||
|
'completed_today': 0,
|
||||||
|
'failed_today': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# If we have ImportJobRecord, get some stats
|
||||||
|
if recent_jobs:
|
||||||
|
import datetime
|
||||||
|
today = datetime.datetime.utcnow().date()
|
||||||
|
for job in recent_jobs:
|
||||||
|
if job.completed_at and job.completed_at.date() == today:
|
||||||
|
if job.status == 'completed':
|
||||||
|
stats['completed_today'] += 1
|
||||||
|
elif job.status == 'failed':
|
||||||
|
stats['failed_today'] += 1
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
'import_queue_status.html',
|
||||||
|
stats=stats,
|
||||||
|
active_jobs=active_jobs,
|
||||||
|
recent_jobs=recent_jobs,
|
||||||
|
queue_snapshot=queue_snapshot,
|
||||||
|
queue=queue,
|
||||||
|
now=now
|
||||||
|
)
|
||||||
@@ -165,12 +165,16 @@
|
|||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
<i class="fas fa-download mr-2"></i> Backup Manager
|
<i class="fas fa-download mr-2"></i> Backup Manager
|
||||||
</span>
|
</span>
|
||||||
</a></li>
|
</a></li> <li><a class="block px-4 py-2 hover:bg-navy-50" href="{{ url_for('users.system_health') }}">
|
||||||
<li><a class="block px-4 py-2 hover:bg-navy-50" href="{{ url_for('users.system_health') }}">
|
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
<i class="fas fa-heartbeat mr-2"></i> System Health
|
<i class="fas fa-heartbeat mr-2"></i> System Health
|
||||||
</span>
|
</span>
|
||||||
</a></li>
|
</a></li>
|
||||||
|
<li><a class="block px-4 py-2 hover:bg-navy-50" href="{{ url_for('import.queue_status') }}">
|
||||||
|
<span class="flex items-center">
|
||||||
|
<i class="fas fa-tasks mr-2"></i> Import Queue
|
||||||
|
</span>
|
||||||
|
</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
Reference in New Issue
Block a user