diff --git a/backend/main.py b/backend/main.py index 644328b..0aea418 100644 --- a/backend/main.py +++ b/backend/main.py @@ -9,7 +9,7 @@ import pytz from .database import engine, Base, get_db, SessionLocal from .auth import get_password_hash, generate_totp_secret, get_totp_uri, verify_password, verify_totp, create_access_token, verify_token -from .schemas import AdminCreate, LoginRequest, ProjectCreate, ProjectResponse, ProjectRename, MessageCreate, MessageResponse, PasswordChange, ModelReplacementRequest, LogRequest +from .schemas import AdminCreate, LoginRequest, ProjectCreate, ProjectResponse, ProjectRename, MessageCreate, MessageResponse, PasswordChange, ModelReplacementRequest, LogRequest, SystemSettingsUpdate, SystemSettingsResponse from .models import User, Project, Message, SystemSettings, AIModel, FinancialLog from .orchestrator import run_orchestrator, sync_providers_models, sync_finances, activity_logs, log_activity @@ -29,13 +29,11 @@ async def scheduler_task(): if (now.hour == 0 or now.hour == 12) and now.minute == 0: db = SessionLocal() try: - # 1. Sync des modèles settings = db.query(SystemSettings).first() if settings: try: await sync_providers_models(db, settings, "Automatique") except: pass - # 2. Nettoyage automatique des discussions > 7 jours non épinglées cutoff = datetime.now(timezone.utc) - timedelta(days=7) old_projects = db.query(Project).filter(Project.is_pinned == False, Project.created_at < cutoff).all() if old_projects: @@ -89,7 +87,9 @@ def login(login_data: LoginRequest, response: Response, db: Session = Depends(ge user = db.query(User).filter(User.username == login_data.username).first() if not user or not verify_password(login_data.password, user.hashed_password): raise HTTPException(status_code=401, detail="Identifiants incorrects.") if not verify_totp(user.totp_secret, login_data.totp_code): raise HTTPException(status_code=401, detail="2FA invalide.") - response.set_cookie(key="session_token", value=create_access_token(data={"sub": user.username}), httponly=True, max_age=604800, samesite="lax") + + # Session valide 8 heures (28800 secondes) + response.set_cookie(key="session_token", value=create_access_token(data={"sub": user.username}), httponly=True, max_age=28800, samesite="lax") return {"message": "Connexion réussie"} @app.get("/dashboard") @@ -164,6 +164,23 @@ async def upload_avatar(file: UploadFile = File(...), db: Session = Depends(get_ db.commit() return {"message": "Avatar mis à jour", "avatar_path": current_user.avatar_path} +# --- ROUTES SUPER-ADMIN --- +@app.get("/api/settings", response_model=SystemSettingsResponse) +def get_settings(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): + if not current_user.is_superadmin: raise HTTPException(status_code=403, detail="Super-Admin requis.") + return db.query(SystemSettings).first() + +@app.put("/api/settings") +def update_settings(settings_data: SystemSettingsUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): + if not current_user.is_superadmin: raise HTTPException(status_code=403, detail="Super-Admin requis.") + s = db.query(SystemSettings).first() + if not s: raise HTTPException(status_code=404) + for k, v in settings_data.dict(exclude_unset=True).items(): + setattr(s, k, v) + db.commit() + log_activity("Configuration système mise à jour par le Super-Admin.") + return {"message": "Paramètres mis à jour avec succès."} + @app.get("/api/projects", response_model=List[ProjectResponse]) def get_projects(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): return db.query(Project).filter(Project.user_id == current_user.id).order_by(Project.created_at.desc()).all() @@ -205,7 +222,6 @@ def get_messages(project_id: int, db: Session = Depends(get_db), current_user: U @app.post("/api/projects/{project_id}/messages", response_model=List[MessageResponse]) async def create_message(project_id: int, message: MessageCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): - # 1. Gestion de l'auto-renommage à la 2ème requête p = db.query(Project).filter(Project.id == project_id).first() if p and p.title == "Nouvelle discussion": user_msgs = db.query(Message).filter(Message.project_id == project_id, Message.role == "user").order_by(Message.created_at.asc()).all() @@ -216,7 +232,6 @@ async def create_message(project_id: int, message: MessageCreate, db: Session = p.title = new_title + ("..." if len(first_content) > 35 else "") db.commit() - # 2. Gestion des fichiers extracted_files_data = [] files_names = [] @@ -330,9 +345,23 @@ def export_models(db: Session = Depends(get_db), current_user: User = Depends(ge md_content += f"## Fournisseur : {prov.upper()}\n\n" prov_models = [m for m in models if m.provider == prov] for m in prov_models: - price_info = "**GRATUIT**" if m.is_free else f"In: ${m.pricing_prompt:.2f} / Out:${m.pricing_completion:.2f}" + price_info = "**GRATUIT**" if m.is_free else f"In: ${m.pricing_prompt:.2f} / Out: ${m.pricing_completion:.2f}" ctx_info = f"{int(m.context_length/1000)}k" desc = m.description_fr.replace('\n', ' ') if m.description_fr else "" md_content += f"- **{m.name or m.model_id}** (`{m.model_id}`)\n" md_content += f" - *Domaine :* {m.domain}\n" - md_ \ No newline at end of file + md_content += f" - *Prix (1M tokens) :* {price_info}\n" + md_content += f" - *Contexte :* {ctx_info}\n" + md_content += f" - *Description :* {desc}\n\n" + + zip_io = io.BytesIO() + with zipfile.ZipFile(zip_io, mode='w', compression=zipfile.ZIP_DEFLATED) as zf: + zf.writestr("models_export.csv", csv_io.getvalue().encode('utf-8')) + zf.writestr(f"{datetime.now().strftime('%Y%m%d')}-extraction-modeles.md", md_content.encode('utf-8')) + + zip_io.seek(0) + return Response( + content=zip_io.getvalue(), + media_type="application/zip", + headers={"Content-Disposition": f"attachment; filename=aethas38_models_{datetime.now().strftime('%Y%m%d')}.zip"} + ) \ No newline at end of file diff --git a/backend/orchestrator.py b/backend/orchestrator.py index b8d1280..7466370 100644 --- a/backend/orchestrator.py +++ b/backend/orchestrator.py @@ -206,7 +206,8 @@ async def ask_agent(client, model_id, messages, provider="openrouter"): kwargs = {"model": model_id, "messages": messages} if provider == "openrouter": kwargs["extra_headers"] = {"HTTP-Referer": "https://aethas38.duckdns.org", "X-Title": "AETHAS38 Orchestrator"} - kwargs["extra_body"] = {"plugins": [{"id": "context-compression"}]} + # Activation native du plugin web-search et de compression + kwargs["extra_body"] = {"plugins": [{"id": "context-compression"}, {"id": "web-search"}]} resp = await client.chat.completions.create(**kwargs) return resp.choices[0].message.content @@ -225,7 +226,6 @@ async def run_orchestrator(db: Session, history: list, settings: SystemSettings, log_activity(f"Démarrage de l'orchestration. Modèle Prompteur: {p_mod}") p_client, p_prov = get_client_for_model(db, p_mod, settings) - # --- WORKFLOW MAP-REDUCE : PRÉ-TRAITEMENT SÉQUENTIEL & CHUNKING --- files_context = "" if extracted_files: log_activity(f"Traitement Map-Reduce de {len(extracted_files)} fichier(s) joint(s).") @@ -270,7 +270,6 @@ async def run_orchestrator(db: Session, history: list, settings: SystemSettings, files_context = "".join(file_analyses) user_prompt = f"{original_user_text}\n\nVoici les données pré-traitées des fichiers joints :\n{files_context}" - # --- OPTIMISATION & TRADUCTION --- log_activity(f"Optimisation/Traduction de la requête via Prompteur...") prompt_system = "You are an expert prompt engineer. Translate and optimize the user request and any file context into clear, precise English tailored for AI execution. Keep all code blocks intact." optimized = await ask_agent(p_client, p_mod, [{"role": "system", "content": prompt_system}, {"role": "user", "content": user_prompt}], p_prov) diff --git a/backend/schemas.py b/backend/schemas.py index e0e6a82..8108ab3 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -20,6 +20,26 @@ class AdminCreate(BaseModel): cloudflare_api_token: Optional[str] = None huggingface_api_key: Optional[str] = None +class SystemSettingsBase(BaseModel): + smtp_host: Optional[str] = None + smtp_port: Optional[int] = None + smtp_user: Optional[str] = None + smtp_password: Optional[str] = None + openrouter_api_key: Optional[str] = None + openrouter_management_key: Optional[str] = None + groq_api_key: Optional[str] = None + gemini_api_key: Optional[str] = None + deepseek_api_key: Optional[str] = None + mistral_api_key: Optional[str] = None + cloudflare_account_id: Optional[str] = None + cloudflare_api_token: Optional[str] = None + huggingface_api_key: Optional[str] = None + +class SystemSettingsUpdate(SystemSettingsBase): pass + +class SystemSettingsResponse(SystemSettingsBase): + class Config: from_attributes = True + class LoginRequest(BaseModel): username: str password: str diff --git a/frontend/dashboard.html b/frontend/dashboard.html index e1b7396..64e8a7e 100644 --- a/frontend/dashboard.html +++ b/frontend/dashboard.html @@ -23,6 +23,12 @@
+ +
+ + {{ toastMessage }} +
+
@@ -65,7 +71,7 @@
- +
@@ -242,8 +248,33 @@
-

Espace Réservé Super-Admin

-

Gestion globale du système.

+

Espace Réservé Super-Admin - Clés API & SMTP

+ +
{{ adminSettingsMessage }}
+ +
+ +
+
+
+
+
+
+
+
+
+ + +

Configuration SMTP

+
+
+
+
+
+ +
@@ -308,7 +339,6 @@ - @@ -349,17 +379,21 @@
{{ part.language || 'code' }} - +
+ + + +
{{ part.content }}
- +
- - + +
@@ -509,16 +543,30 @@ const isLoading = ref(false); const isAiThinking = ref(false); const isSyncing = ref(false); - const isSavingSettings = ref(false); + + // TOAST (Non bloquant) + const toastVisible = ref(false); + const toastMessage = ref(""); + const showToast = (msg) => { + toastMessage.value = msg; + toastVisible.value = true; + setTimeout(() => { toastVisible.value = false; }, 5000); // Disparaît après 5 secondes + }; const isAssignModalOpen = ref(false); const assignForm = reactive({ modelId: '', projectId: '', role: 'worker1' }); const assignMessage = ref(""); - const isSettingsModalOpen = ref(false); // Ajout crucial : initialisation de la variable du modal paramètres + const isSettingsModalOpen = ref(false); + // FORMULAIRES SETTINGS const passForm = reactive({ old: '', new: '', confirm: '' }); const settingsMessage = ref(""); + const isSavingSettings = ref(false); + + const adminSettings = reactive({}); + const adminSettingsMessage = ref(""); + const isSavingAdminSettings = ref(false); const modelInfo = reactive({ last_sync_date: null, last_sync_type: null, models: [] }); const financesList = ref([]); @@ -591,9 +639,39 @@ const autoResize = () => { const el = promptTextarea.value; if(el){ el.style.height = 'auto'; el.style.height = el.scrollHeight + 'px'; }}; const scrollToBottom = () => { nextTick(() => { const c = document.getElementById('chat-container'); if (c) c.scrollTop = c.scrollHeight; }); }; + const fetchAdminSettings = async () => { + try { + const res = await fetch('/api/settings'); + if (res.ok) { + const data = await res.json(); + Object.assign(adminSettings, data); + } + } catch(e) {} + }; + + const saveAdminSettings = async () => { + isSavingAdminSettings.value = true; + adminSettingsMessage.value = ""; + try { + const res = await fetch('/api/settings', { + method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(adminSettings) + }); + if (res.ok) { + adminSettingsMessage.value = "Paramètres sauvegardés avec succès."; + setTimeout(() => adminSettingsMessage.value="", 3000); + } else { + adminSettingsMessage.value = "Erreur lors de la sauvegarde."; + } + } catch (e) { + adminSettingsMessage.value = "Erreur réseau."; + } + isSavingAdminSettings.value = false; + }; + const openSettings = () => { activeView.value = 'settings'; if(window.innerWidth < 768) isSidebarOpen.value = false; + if(user.value.is_superadmin) fetchAdminSettings(); nextTick(() => lucide.createIcons()); }; @@ -726,7 +804,7 @@ if(res.ok) { await fetchModelsInfo(); await fetchFinances(); - alert("Synchronisation réussie !"); + showToast("Synchronisation réussie !"); } else if (res.status === 401) { alert("Votre session a expiré."); window.location.href = '/login'; } else { @@ -799,7 +877,7 @@ selectedConcatenator.value = extractModel('Concateneur'); await validateConfigAndReplace(); - alert("Configuration IA importée et appliquée avec succès !"); + showToast("Configuration IA importée et appliquée avec succès !"); }; reader.readAsText(file); event.target.value = null; @@ -868,7 +946,14 @@ const removeFile = (index) => { attachedFiles.value.splice(index, 1); }; const exportProject = (format) => { if(activeProject.value) window.open(`/api/projects/${activeProject.value.id}/export?format=${format}`, '_blank'); }; - const copyToClipboard = async (text) => { try { await navigator.clipboard.writeText(text); alert("Copié !"); } catch (e) {} }; + + // COPIE REMPLACEE PAR UN TOAST (NON BLOQUANT) + const copyToClipboard = async (text) => { + try { + await navigator.clipboard.writeText(text); + showToast("Texte copié dans le presse-papier !"); + } catch (e) {} + }; const downloadFile = (content, hint, mimeType = 'text/plain;charset=utf-8') => { let fname = hint ? hint.trim() : 'code.txt'; @@ -939,8 +1024,12 @@ return; } const userText = prompt.value.trim(); + + // REINITIALISATION DE LA HAUTEUR DU TEXTAREA prompt.value = ""; - autoResize(); + nextTick(() => { + if (promptTextarea.value) promptTextarea.value.style.height = 'auto'; + }); const fileNames = attachedFiles.value.map(f => f.name).join(", "); messages.value.push({ id: Date.now(), role: 'user', content: userText + (attachedFiles.value.length > 0 ? `\n[Fichiers joints: ${fileNames}]` : "") }); @@ -1015,12 +1104,14 @@ projects, pinnedProjects, recentProjects, activeProject, messages, isAiThinking, createNewProject, selectProject, renameProject, sendMessage, isLoading, togglePin, deleteProject, openSettings, closeSettings, passForm, settingsMessage, isSavingSettings, changePassword, uploadAvatar, + adminSettings, adminSettingsMessage, isSavingAdminSettings, saveAdminSettings, modelInfo, financesList, filterSearch, filterDomain, filterPricing, filterProvider, uniqueProviders, filteredModels, getFilteredModelsForCol, formatDate, syncModels, isSyncing, workers, selectedPrompter, selectedConcatenator, colPricing, colDomain, downloadTemplate, exportCSV, handleConfigImport, isAssignModalOpen, openAssignModal, closeAssignModal, assignForm, assignMessage, confirmAssignModel, - isSettingsModalOpen, // EXPORTATION OBLIGATOIRE DE LA VARIABLE ICI + isSettingsModalOpen, attachedFiles, handleFileUpload, handlePaste, removeFile, - exportProject, copyToClipboard, downloadFile, downloadMessage, parseMessage, systemLogs + exportProject, copyToClipboard, downloadFile, downloadMessage, parseMessage, systemLogs, + toastVisible, toastMessage, showToast }; } }).mount('#app');