Spaces:
Sleeping
Sleeping
yonnel
Enhance environment configuration; implement lazy initialization for vector updater and improve error handling in imports
b8ca8ae
from fastapi import APIRouter, HTTPException, Depends, BackgroundTasks | |
from fastapi.responses import HTMLResponse | |
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials | |
import os | |
import logging | |
from datetime import datetime | |
from typing import Optional | |
# Import avec gestion des erreurs pour les imports relatifs | |
try: | |
from ..services.vector_updater import VectorUpdater | |
except ImportError: | |
try: | |
from app.services.vector_updater import VectorUpdater | |
except ImportError: | |
# Import direct pour quand l'application est lancée depuis le répertoire racine | |
import sys | |
sys.path.append(os.path.dirname(os.path.dirname(__file__))) | |
from services.vector_updater import VectorUpdater | |
logger = logging.getLogger(__name__) | |
router = APIRouter(prefix="/admin", tags=["admin"]) | |
security = HTTPBearer() | |
# Instance globale du updater - créée de manière paresseuse pour éviter les erreurs d'import | |
vector_updater = None | |
def get_vector_updater(): | |
"""Get vector updater instance (lazy initialization)""" | |
global vector_updater | |
if vector_updater is None: | |
vector_updater = VectorUpdater() | |
return vector_updater | |
def verify_admin_token(credentials: HTTPAuthorizationCredentials = Depends(security)): | |
"""Vérification du token admin""" | |
admin_token = os.getenv('ADMIN_TOKEN') | |
if not admin_token or credentials.credentials != admin_token: | |
raise HTTPException(status_code=403, detail="Invalid admin token") | |
return credentials.credentials | |
async def admin_dashboard(): | |
"""Interface web d'administration""" | |
html_content = """ | |
<!DOCTYPE html> | |
<html lang="fr"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Karl Movie Vector - Admin</title> | |
<style> | |
body { font-family: Arial, sans-serif; margin: 40px; background: #f5f5f5; } | |
.container { max-width: 1000px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } | |
h1 { color: #333; text-align: center; margin-bottom: 30px; } | |
.card { background: #f8f9fa; padding: 20px; margin: 20px 0; border-radius: 5px; border-left: 4px solid #007bff; } | |
.status { padding: 10px; margin: 10px 0; border-radius: 4px; } | |
.status.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; } | |
.status.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; } | |
.status.warning { background: #fff3cd; color: #856404; border: 1px solid #ffeaa7; } | |
.status.info { background: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb; } | |
button { background: #007bff; color: white; border: none; padding: 12px 24px; border-radius: 4px; cursor: pointer; margin: 5px; font-size: 14px; } | |
button:hover { background: #0056b3; } | |
button:disabled { background: #6c757d; cursor: not-allowed; } | |
.danger { background: #dc3545; } | |
.danger:hover { background: #c82333; } | |
.loading { display: none; color: #007bff; margin: 10px 0; } | |
.log { background: #f8f9fa; border: 1px solid #dee2e6; padding: 15px; margin: 15px 0; border-radius: 4px; font-family: monospace; font-size: 12px; max-height: 400px; overflow-y: auto; white-space: pre-line; } | |
.config-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } | |
.config-item { background: white; padding: 10px; border-radius: 4px; border: 1px solid #dee2e6; } | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<h1>🎬 Karl Movie Vector - Administration</h1> | |
<div class="card"> | |
<h3>📊 Statut du Système</h3> | |
<div id="status-info"></div> | |
<button onclick="loadStatus()">🔄 Actualiser le Statut</button> | |
</div> | |
<div class="card"> | |
<h3>🚀 Mise à jour des Vecteurs</h3> | |
<p>Déclenchez manuellement la mise à jour des vecteurs de films depuis TMDB.</p> | |
<div> | |
<button onclick="updateVectors()" id="updateBtn">🔄 Mettre à jour (Conditionnel)</button> | |
<button onclick="forceUpdateVectors()" id="forceUpdateBtn" class="danger">⚡ Forcer la Mise à jour</button> | |
</div> | |
<div id="update-loading" class="loading">⏳ Mise à jour en cours...</div> | |
<div id="update-result"></div> | |
</div> | |
<div class="card"> | |
<h3>📋 Logs de Mise à jour</h3> | |
<button onclick="loadLogs()">📄 Charger les Logs</button> | |
<button onclick="clearLogs()">🗑️ Effacer l'affichage</button> | |
<div id="logs" class="log">Cliquez sur "Charger les Logs" pour voir les logs...</div> | |
</div> | |
</div> | |
<script> | |
const API_BASE = '/admin'; | |
let ADMIN_TOKEN = localStorage.getItem('admin_token'); | |
if (!ADMIN_TOKEN) { | |
ADMIN_TOKEN = prompt('Token Admin:'); | |
if (ADMIN_TOKEN) { | |
localStorage.setItem('admin_token', ADMIN_TOKEN); | |
} | |
} | |
const headers = { | |
'Authorization': `Bearer ${ADMIN_TOKEN}`, | |
'Content-Type': 'application/json' | |
}; | |
async function apiCall(endpoint, method = 'GET') { | |
try { | |
const response = await fetch(`${API_BASE}${endpoint}`, { | |
method, | |
headers | |
}); | |
if (response.status === 403) { | |
localStorage.removeItem('admin_token'); | |
location.reload(); | |
return; | |
} | |
if (!response.ok) { | |
throw new Error(`HTTP ${response.status}: ${response.statusText}`); | |
} | |
return await response.json(); | |
} catch (error) { | |
console.error('API Error:', error); | |
throw error; | |
} | |
} | |
async function loadStatus() { | |
try { | |
const status = await apiCall('/status'); | |
const statusDiv = document.getElementById('status-info'); | |
let statusClass = 'info'; | |
if (status.is_updating) statusClass = 'warning'; | |
else if (!status.hf_configured) statusClass = 'error'; | |
else if (status.last_update_result && status.last_update_result.success) statusClass = 'success'; | |
let lastUpdateInfo = 'Aucune'; | |
if (status.last_update_result) { | |
const result = status.last_update_result; | |
if (result.success) { | |
lastUpdateInfo = `✅ ${result.count} films (${new Date(result.timestamp).toLocaleString()})`; | |
} else { | |
lastUpdateInfo = `❌ Erreur: ${result.error}`; | |
} | |
} | |
statusDiv.innerHTML = ` | |
<div class="status ${statusClass}"> | |
<div class="config-grid"> | |
<div class="config-item"> | |
<strong>Statut:</strong> ${status.is_updating ? '🔄 Mise à jour en cours...' : '✅ Prêt'} | |
</div> | |
<div class="config-item"> | |
<strong>HF Configuré:</strong> ${status.hf_configured ? '✅ Oui' : '❌ Non'} | |
</div> | |
<div class="config-item"> | |
<strong>Auto-update:</strong> ${status.auto_update_enabled ? '✅ Activé' : '❌ Désactivé'} | |
</div> | |
<div class="config-item"> | |
<strong>Intervalle:</strong> ${status.update_interval_hours}h | |
</div> | |
<div class="config-item"> | |
<strong>Taille batch:</strong> ${status.batch_size} | |
</div> | |
<div class="config-item"> | |
<strong>Limite films:</strong> ${status.max_movies_limit} | |
</div> | |
<div class="config-item"> | |
<strong>Logs:</strong> ${status.logs_count} entrées | |
</div> | |
<div class="config-item"> | |
<strong>Dernière MAJ:</strong> ${lastUpdateInfo} | |
</div> | |
</div> | |
</div> | |
`; | |
} catch (error) { | |
document.getElementById('status-info').innerHTML = ` | |
<div class="status error">❌ Erreur: ${error.message}</div> | |
`; | |
} | |
} | |
async function updateVectors() { | |
const btn = document.getElementById('updateBtn'); | |
const loading = document.getElementById('update-loading'); | |
const result = document.getElementById('update-result'); | |
btn.disabled = true; | |
loading.style.display = 'block'; | |
result.innerHTML = ''; | |
try { | |
const response = await apiCall('/update-vectors', 'POST'); | |
result.innerHTML = ` | |
<div class="status ${response.success ? 'success' : 'warning'}"> | |
${response.success ? '✅' : '⚠️'} ${response.message} | |
</div> | |
`; | |
// Actualiser le statut après quelques secondes | |
if (response.success) { | |
setTimeout(loadStatus, 2000); | |
} | |
} catch (error) { | |
result.innerHTML = ` | |
<div class="status error">❌ Erreur: ${error.message}</div> | |
`; | |
} finally { | |
btn.disabled = false; | |
loading.style.display = 'none'; | |
} | |
} | |
async function forceUpdateVectors() { | |
if (!confirm('Êtes-vous sûr de vouloir forcer la mise à jour ? Cela peut prendre plusieurs minutes et consommer des crédits API.')) { | |
return; | |
} | |
const btn = document.getElementById('forceUpdateBtn'); | |
const loading = document.getElementById('update-loading'); | |
const result = document.getElementById('update-result'); | |
btn.disabled = true; | |
loading.style.display = 'block'; | |
result.innerHTML = ''; | |
try { | |
const response = await apiCall('/force-update-vectors', 'POST'); | |
result.innerHTML = ` | |
<div class="status ${response.success ? 'success' : 'error'}"> | |
${response.success ? '✅' : '❌'} ${response.message} | |
</div> | |
`; | |
// Actualiser le statut après quelques secondes | |
if (response.success) { | |
setTimeout(loadStatus, 2000); | |
} | |
} catch (error) { | |
result.innerHTML = ` | |
<div class="status error">❌ Erreur: ${error.message}</div> | |
`; | |
} finally { | |
btn.disabled = false; | |
loading.style.display = 'none'; | |
} | |
} | |
async function loadLogs() { | |
try { | |
const response = await apiCall('/logs'); | |
const logsDiv = document.getElementById('logs'); | |
if (response.logs && response.logs.length > 0) { | |
logsDiv.innerHTML = response.logs.join('\\n'); | |
logsDiv.scrollTop = logsDiv.scrollHeight; | |
} else { | |
logsDiv.innerHTML = 'Aucun log disponible'; | |
} | |
} catch (error) { | |
document.getElementById('logs').innerHTML = `Erreur: ${error.message}`; | |
} | |
} | |
function clearLogs() { | |
document.getElementById('logs').innerHTML = 'Logs effacés (rechargez pour voir les nouveaux logs)'; | |
} | |
// Charger le statut au démarrage | |
if (ADMIN_TOKEN) { | |
loadStatus(); | |
// Auto-refresh du statut toutes les 30 secondes | |
setInterval(loadStatus, 30000); | |
} | |
</script> | |
</body> | |
</html> | |
""" | |
return HTMLResponse(content=html_content) | |
async def get_status(token: str = Depends(verify_admin_token)): | |
"""Obtenir le statut du système""" | |
return get_vector_updater().get_update_status() | |
async def update_vectors(background_tasks: BackgroundTasks, token: str = Depends(verify_admin_token)): | |
"""Déclencher une mise à jour si nécessaire""" | |
vector_updater = get_vector_updater() | |
if vector_updater.is_updating: | |
return {"success": False, "message": "Une mise à jour est déjà en cours"} | |
# Lancer la mise à jour en arrière-plan | |
background_tasks.add_task(vector_updater.update_vectors_if_needed) | |
return {"success": True, "message": "Mise à jour programmée (vérification des conditions)"} | |
async def force_update_vectors(background_tasks: BackgroundTasks, token: str = Depends(verify_admin_token)): | |
"""Forcer la mise à jour des vecteurs""" | |
vector_updater = get_vector_updater() | |
if vector_updater.is_updating: | |
return {"success": False, "message": "Une mise à jour est déjà en cours"} | |
# Lancer la mise à jour forcée en arrière-plan | |
background_tasks.add_task(vector_updater.force_update_vectors) | |
return {"success": True, "message": "Mise à jour forcée programmée"} | |
async def get_logs(token: str = Depends(verify_admin_token)): | |
"""Obtenir les logs de mise à jour""" | |
try: | |
logs = get_vector_updater().get_logs() | |
return {"logs": logs} | |
except Exception as e: | |
return {"logs": [f"Erreur de lecture des logs: {e}"]} |