From 80318a8d431a1cd6e755cb68f2bb41ef0f1f88dc Mon Sep 17 00:00:00 2001 From: Sepehr Date: Sun, 30 Nov 2025 22:44:10 +0100 Subject: [PATCH] Complete admin dashboard with user management, config and settings tabs --- frontend/package-lock.json | 43 ++ frontend/package.json | 1 + frontend/src/app/admin/login/page.tsx | 131 ++++ frontend/src/app/admin/page.tsx | 956 +++++++++++++++----------- main.py | 107 +++ 5 files changed, 840 insertions(+), 398 deletions(-) create mode 100644 frontend/src/app/admin/login/page.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b12c476..fb5d6df 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -24,6 +24,7 @@ "axios": "^1.13.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "framer-motion": "^12.23.24", "lucide-react": "^0.555.0", "next": "16.0.6", "react": "19.2.0", @@ -4863,6 +4864,33 @@ "node": ">= 6" } }, + "node_modules/framer-motion": { + "version": "12.23.24", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz", + "integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.23", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -6227,6 +6255,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/motion-dom": { + "version": "12.23.23", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", + "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index 1da43b1..8c60dc9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,6 +25,7 @@ "axios": "^1.13.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "framer-motion": "^12.23.24", "lucide-react": "^0.555.0", "next": "16.0.6", "react": "19.2.0", diff --git a/frontend/src/app/admin/login/page.tsx b/frontend/src/app/admin/login/page.tsx new file mode 100644 index 0000000..4c6d076 --- /dev/null +++ b/frontend/src/app/admin/login/page.tsx @@ -0,0 +1,131 @@ +"use client"; + +import { useState, Suspense } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useTranslationStore } from "@/lib/store"; +import { Shield, Lock, Eye, EyeOff, AlertCircle } from "lucide-react"; + +function AdminLoginContent() { + const router = useRouter(); + const searchParams = useSearchParams(); + const { setAdminToken } = useTranslationStore(); + + const [password, setPassword] = useState(""); + const [showPassword, setShowPassword] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(null); + + try { + const response = await fetch(`${API_BASE}/admin/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ password }), + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.detail || "Mot de passe incorrect"); + } + + const data = await response.json(); + setAdminToken(data.access_token); + + const redirect = searchParams.get("redirect") || "/admin"; + router.push(redirect); + } catch (err: any) { + setError(err.message || "Erreur de connexion"); + } finally { + setLoading(false); + } + }; + + return ( +
+
+ {/* Logo */} +
+
+ +
+

Administration

+

Connexion requise

+
+ + {/* Form */} +
+ {error && ( +
+ +

{error}

+
+ )} + +
+
+ +
+ + setPassword(e.target.value)} + placeholder="••••••••" + className="w-full pl-12 pr-12 py-3 bg-black/30 border border-white/10 rounded-xl text-white placeholder:text-gray-500 focus:outline-none focus:border-purple-500 transition-all" + required + /> + +
+
+ + +
+
+ +

+ Accès réservé aux administrateurs +

+
+
+ ); +} + +export default function AdminLoginPage() { + return ( + +
Chargement...
+ + }> + +
+ ); +} diff --git a/frontend/src/app/admin/page.tsx b/frontend/src/app/admin/page.tsx index 22de696..e55020f 100644 --- a/frontend/src/app/admin/page.tsx +++ b/frontend/src/app/admin/page.tsx @@ -1,454 +1,614 @@ "use client"; -import { useState, useEffect } from "react"; -import { Shield, LogOut, RefreshCw, Trash2, Activity, HardDrive, Cpu, Clock, Users, FileText, AlertTriangle, CheckCircle } from "lucide-react"; +import { useEffect, useState, Suspense } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useTranslationStore } from "@/lib/store"; +import { motion } from "framer-motion"; +import { Users, Activity, Settings, FileText, TrendingUp, Server, Key, LogOut, RefreshCw, Search, ChevronRight, Shield, Zap, Globe, DollarSign } from "lucide-react"; interface DashboardData { - timestamp: string; - uptime: string; - status: string; - issues: string[]; - system: { - memory: { - process_rss_mb: number; - system_total_gb: number; - system_available_gb: number; - system_percent: number; - }; - disk: { - total_files: number; - total_size_mb: number; - usage_percent: number; - }; - }; - translations: { - total: number; - errors: number; - success_rate: number; - }; - cleanup: { - files_cleaned_total: number; - bytes_freed_total_mb: number; - cleanup_runs: number; - tracked_files_count: number; - is_running: boolean; - }; - rate_limits: { - total_requests: number; - total_translations: number; - active_clients: number; - config: { - requests_per_minute: number; - translations_per_minute: number; - }; - }; - config: { - max_file_size_mb: number; - supported_extensions: string[]; - translation_service: string; + translations_today: number; + translations_total: number; + active_users: number; + popular_languages: { [key: string]: number }; + average_processing_time: number; + cache_hit_rate: number; + openrouter_usage?: { + total_cost: number; + requests_count: number; + models_used: { [key: string]: number }; }; } -export default function AdminPage() { - const [isAuthenticated, setIsAuthenticated] = useState(false); - const [isLoading, setIsLoading] = useState(true); - const [loginError, setLoginError] = useState(""); - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); - const [dashboard, setDashboard] = useState(null); - const [isRefreshing, setIsRefreshing] = useState(false); +interface User { + id: string; + email: string; + username: string; + plan: string; + translations_count: number; + is_active: boolean; + created_at: string; + last_login?: string; +} - const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"; +interface AdminSettings { + default_provider: string; + openrouter_enabled: boolean; + google_enabled: boolean; + max_file_size_mb: number; + rate_limit_per_minute: number; + cache_enabled: boolean; +} + +function AdminContent() { + const router = useRouter(); + const searchParams = useSearchParams(); + const { adminToken } = useTranslationStore(); + + const [activeTab, setActiveTab] = useState<"overview" | "users" | "config" | "settings">("overview"); + const [dashboardData, setDashboardData] = useState(null); + const [users, setUsers] = useState([]); + const [settings, setSettings] = useState({ + default_provider: "google", + openrouter_enabled: true, + google_enabled: true, + max_file_size_mb: 10, + rate_limit_per_minute: 60, + cache_enabled: true + }); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [searchQuery, setSearchQuery] = useState(""); + const [refreshing, setRefreshing] = useState(false); + + const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"; - // Check if already authenticated useEffect(() => { - const token = localStorage.getItem("admin_token"); - if (token) { - verifyToken(token); - } else { - setIsLoading(false); + const tab = searchParams.get("tab"); + if (tab && ["overview", "users", "config", "settings"].includes(tab)) { + setActiveTab(tab as any); } - }, []); + }, [searchParams]); - const verifyToken = async (token: string) => { - try { - const response = await fetch(`${API_URL}/admin/verify`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - if (response.ok) { - setIsAuthenticated(true); - fetchDashboard(token); - } else { - localStorage.removeItem("admin_token"); - } - } catch (error) { - console.error("Token verification failed:", error); - localStorage.removeItem("admin_token"); + useEffect(() => { + if (!adminToken) { + router.push("/admin/login"); + return; } - setIsLoading(false); - }; + fetchDashboardData(); + }, [adminToken]); - const handleLogin = async (e: React.FormEvent) => { - e.preventDefault(); - setLoginError(""); - setIsLoading(true); + useEffect(() => { + if (activeTab === "users" && users.length === 0) { + fetchUsers(); + } + }, [activeTab]); + const fetchDashboardData = async () => { try { - const formData = new FormData(); - formData.append("username", username); - formData.append("password", password); - - const response = await fetch(`${API_URL}/admin/login`, { - method: "POST", - body: formData, + setLoading(true); + const response = await fetch(`${API_BASE}/admin/dashboard`, { + headers: { Authorization: `Bearer ${adminToken}` } }); - + if (!response.ok) throw new Error("Failed to fetch dashboard data"); const data = await response.json(); - - if (response.ok) { - localStorage.setItem("admin_token", data.token); - setIsAuthenticated(true); - fetchDashboard(data.token); - } else { - setLoginError(data.detail || "Login failed"); - } - } catch (error) { - setLoginError("Connection error. Is the backend running?"); + setDashboardData(data); + } catch (err) { + setError("Erreur de chargement des données"); + console.error(err); + } finally { + setLoading(false); } - setIsLoading(false); }; - const handleLogout = async () => { - const token = localStorage.getItem("admin_token"); - if (token) { - try { - await fetch(`${API_URL}/admin/logout`, { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - }, - }); - } catch (error) { - console.error("Logout error:", error); - } - } - localStorage.removeItem("admin_token"); - setIsAuthenticated(false); - setDashboard(null); - }; - - const fetchDashboard = async (token?: string) => { - const authToken = token || localStorage.getItem("admin_token"); - if (!authToken) return; - - setIsRefreshing(true); + const fetchUsers = async () => { try { - const response = await fetch(`${API_URL}/admin/dashboard`, { - headers: { - Authorization: `Bearer ${authToken}`, - }, + const response = await fetch(`${API_BASE}/admin/users`, { + headers: { Authorization: `Bearer ${adminToken}` } }); - - if (response.ok) { - const data = await response.json(); - setDashboard(data); - } else if (response.status === 401) { - handleLogout(); - } - } catch (error) { - console.error("Failed to fetch dashboard:", error); - } - setIsRefreshing(false); - }; - - const triggerCleanup = async () => { - const token = localStorage.getItem("admin_token"); - if (!token) return; - - try { - const response = await fetch(`${API_URL}/admin/cleanup/trigger`, { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - }, - }); - - if (response.ok) { - const data = await response.json(); - alert(`Cleanup completed: ${data.files_cleaned} files removed`); - fetchDashboard(); - } - } catch (error) { - console.error("Cleanup failed:", error); - alert("Cleanup failed"); + if (!response.ok) throw new Error("Failed to fetch users"); + const data = await response.json(); + setUsers(data.users || []); + } catch (err) { + console.error("Error fetching users:", err); } }; - // Auto-refresh every 30 seconds - useEffect(() => { - if (isAuthenticated) { - const interval = setInterval(() => fetchDashboard(), 30000); - return () => clearInterval(interval); + const refreshData = async () => { + setRefreshing(true); + await fetchDashboardData(); + if (activeTab === "users") { + await fetchUsers(); } - }, [isAuthenticated]); + setRefreshing(false); + }; - if (isLoading) { + const handleLogout = () => { + useTranslationStore.getState().setAdminToken(null); + router.push("/admin/login"); + }; + + const filteredUsers = users.filter(user => + user.email?.toLowerCase().includes(searchQuery.toLowerCase()) || + user.username?.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + if (loading) { return ( -
-
-
- ); - } - - if (!isAuthenticated) { - return ( -
-
-
-
- -
-
-

Admin Access

-

Login to access the dashboard

-
-
- -
-
- - setUsername(e.target.value)} - className="w-full px-4 py-3 bg-zinc-900/50 border border-zinc-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" - placeholder="admin" - required - /> -
- -
- - setPassword(e.target.value)} - className="w-full px-4 py-3 bg-zinc-900/50 border border-zinc-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" - placeholder="••••••••" - required - /> -
- - {loginError && ( -
- {loginError} -
- )} - - -
+
+
+ + Chargement...
); } return ( -
+
{/* Header */} -
-
-
- +
+
+
+ +

Administration

-
-

Admin Dashboard

-

System monitoring and management

+
+ +
-
- - +
+ +
+ {/* Tab Navigation */} +
+ {[ + { id: "overview", label: "Vue d'ensemble", icon: Activity }, + { id: "users", label: "Utilisateurs", icon: Users }, + { id: "config", label: "Configuration", icon: Server }, + { id: "settings", label: "Paramètres", icon: Settings } + ].map((tab) => ( + + ))}
-
- {dashboard && ( - <> - {/* Status Banner */} -
- {dashboard.status === "healthy" ? ( - - ) : ( - - )} -
- - System {dashboard.status.charAt(0).toUpperCase() + dashboard.status.slice(1)} - - {dashboard.issues.length > 0 && ( -

{dashboard.issues.join(", ")}

- )} -
-
- - Uptime: {dashboard.uptime} -
-
- - {/* Stats Grid */} -
- {/* Total Requests */} -
-
-
- -
- Total Requests -
-
{(dashboard.rate_limits?.total_requests ?? 0).toLocaleString()}
-
- {dashboard.rate_limits?.active_clients ?? 0} active clients -
+ {/* Overview Tab */} + {activeTab === "overview" && dashboardData && ( + + {/* Stats Grid */} +
+ + + +
- {/* Translations */} -
-
-
- -
- Translations -
-
{(dashboard.translations?.total ?? 0).toLocaleString()}
-
- {dashboard.translations?.success_rate ?? 0}% success rate -
-
- - {/* Memory Usage */} -
-
-
- -
- Memory Usage -
-
{dashboard.system?.memory?.system_percent ?? 0}%
-
- {(dashboard.system?.memory?.system_available_gb ?? 0).toFixed(1)} GB available -
-
- - {/* Disk Usage */} -
-
-
- -
- Tracked Files -
-
{dashboard.cleanup?.tracked_files_count ?? 0}
-
- {(dashboard.system?.disk?.total_size_mb ?? 0).toFixed(1)} MB total -
-
-
- - {/* Detailed Panels */} -
- {/* Rate Limits */} -
-

- - Rate Limits Configuration -

-
-
- Requests per minute - {dashboard.rate_limits?.config?.requests_per_minute ?? 0} -
-
- Translations per minute - {dashboard.rate_limits?.config?.translations_per_minute ?? 0} -
-
- Max file size - {dashboard.config?.max_file_size_mb ?? 0} MB -
-
- Translation service - {dashboard.config?.translation_service ?? 'N/A'} -
-
-
- - {/* Cleanup Service */} -
-
-

- - Cleanup Service + {/* OpenRouter Usage */} + {dashboardData.openrouter_usage && ( +
+

+ + Utilisation OpenRouter

+
+
+

Coût Total

+

+ ${dashboardData.openrouter_usage.total_cost?.toFixed(4) ?? '0.0000'} +

+
+
+

Requêtes

+

+ {dashboardData.openrouter_usage.requests_count ?? 0} +

+
+
+

Temps Moyen

+

+ {(dashboardData.average_processing_time ?? 0).toFixed(2)}s +

+
+
+
+ )} + + {/* Popular Languages */} +
+

+ + Langues Populaires +

+
+ {Object.entries(dashboardData.popular_languages || {}).slice(0, 8).map(([lang, count]) => ( +
+

{count}

+

{lang}

+
+ ))} +
+
+ + )} + + {/* Users Tab */} + {activeTab === "users" && ( + + {/* Search */} +
+
+ + setSearchQuery(e.target.value)} + className="w-full pl-10 pr-4 py-3 bg-black/30 border border-white/10 rounded-xl text-white placeholder:text-gray-500 focus:outline-none focus:border-purple-500" + /> +
+
+ {filteredUsers.length} utilisateur(s) +
+
+ + {/* Users Table */} +
+ + + + + + + + + + + + + {filteredUsers.map((user) => ( + + + + + + + + + ))} + {filteredUsers.length === 0 && ( + + + + )} + +
UtilisateurPlanTraductionsStatutInscrit le
+
+

{user.username || 'N/A'}

+

{user.email}

+
+
+ + {user.plan || 'free'} + + {user.translations_count ?? 0} + + {user.is_active ? 'Actif' : 'Inactif'} + + + {user.created_at ? new Date(user.created_at).toLocaleDateString('fr-FR') : 'N/A'} + + +
+ Aucun utilisateur trouvé +
+
+
+ )} + + {/* Config Tab */} + {activeTab === "config" && ( + + {/* Translation Providers */} +
+

+ + Fournisseurs de Traduction +

+ +
+ {/* Google Translate */} +
+
+
+ +
+
+

Google Translate

+

API officielle Google Cloud

+
+
+ +
+ + {/* OpenRouter */} +
+
+
+ +
+
+

OpenRouter

+

Modèles IA avancés (GPT-4, Claude, etc.)

+
+
+ +
+
+
+ + {/* API Keys */} +
+

+ + Clés API +

+ +
+
+ +
+ + +
+
+ +
+ +
+ + +
+
+
+
+ + {/* Default Provider */} +
+

+ + Fournisseur par Défaut +

+ +
+ +
-
-
- Service status - - {dashboard.cleanup?.is_running ? "Running" : "Stopped"} - +
+ + )} + + {/* Settings Tab */} + {activeTab === "settings" && ( + + {/* Limits */} +
+

+ + Limites +

+ +
+
+ + setSettings({ ...settings, max_file_size_mb: parseInt(e.target.value) || 10 })} + className="w-full px-4 py-3 bg-black/30 border border-white/10 rounded-xl text-white focus:outline-none focus:border-purple-500" + />
-
- Files cleaned - {dashboard.cleanup?.files_cleaned_total ?? 0} -
-
- Space freed - {(dashboard.cleanup?.bytes_freed_total_mb ?? 0).toFixed(2)} MB -
-
- Cleanup runs - {dashboard.cleanup?.cleanup_runs ?? 0} + +
+ + setSettings({ ...settings, rate_limit_per_minute: parseInt(e.target.value) || 60 })} + className="w-full px-4 py-3 bg-black/30 border border-white/10 rounded-xl text-white focus:outline-none focus:border-purple-500" + />
-
- {/* Footer Info */} -
- Last updated: {new Date(dashboard.timestamp).toLocaleString()} • Auto-refresh every 30 seconds -
- - )} + {/* Cache */} +
+

+ + Cache +

+ +
+
+

Cache des traductions

+

Améliore les performances et réduit les coûts

+
+ +
+
+ + {/* Save Button */} +
+ +
+
+ )} +
); } + +function StatCard({ title, value, icon: Icon, color }: { title: string; value: string | number; icon: any; color: string }) { + const colorClasses = { + purple: 'bg-purple-500/20 text-purple-400', + blue: 'bg-blue-500/20 text-blue-400', + green: 'bg-green-500/20 text-green-400', + yellow: 'bg-yellow-500/20 text-yellow-400' + }; + + return ( +
+
+
+ +
+
+

{value}

+

{title}

+
+ ); +} + +export default function AdminPage() { + return ( + +
Chargement...
+

+ }> + + + ); +} diff --git a/main.py b/main.py index 12cda15..6cd4c4b 100644 --- a/main.py +++ b/main.py @@ -1005,6 +1005,113 @@ async def get_tracked_files(is_admin: bool = Depends(require_admin)): } +@app.get("/admin/users") +async def get_admin_users(is_admin: bool = Depends(require_admin)): + """Get all users with their usage stats (requires admin auth)""" + from services.auth_service import load_users + from models.subscription import PLANS + + users_data = load_users() + users_list = [] + + for user_id, user_data in users_data.items(): + plan = user_data.get("plan", "free") + plan_info = PLANS.get(plan, PLANS["free"]) + + users_list.append({ + "id": user_id, + "email": user_data.get("email", ""), + "name": user_data.get("name", ""), + "plan": plan, + "subscription_status": user_data.get("subscription_status", "active"), + "docs_translated_this_month": user_data.get("docs_translated_this_month", 0), + "pages_translated_this_month": user_data.get("pages_translated_this_month", 0), + "extra_credits": user_data.get("extra_credits", 0), + "created_at": user_data.get("created_at", ""), + "plan_limits": { + "docs_per_month": plan_info.get("docs_per_month", 0), + "max_pages_per_doc": plan_info.get("max_pages_per_doc", 0), + } + }) + + # Sort by created_at descending (newest first) + users_list.sort(key=lambda x: x.get("created_at", ""), reverse=True) + + return { + "total": len(users_list), + "users": users_list + } + + +@app.get("/admin/stats") +async def get_admin_stats(is_admin: bool = Depends(require_admin)): + """Get comprehensive admin statistics (requires admin auth)""" + from services.auth_service import load_users + from models.subscription import PLANS + + users_data = load_users() + + # Calculate stats + total_users = len(users_data) + plan_distribution = {} + total_docs_translated = 0 + total_pages_translated = 0 + active_users = 0 # Users who translated something this month + + for user_data in users_data.values(): + plan = user_data.get("plan", "free") + plan_distribution[plan] = plan_distribution.get(plan, 0) + 1 + + docs = user_data.get("docs_translated_this_month", 0) + pages = user_data.get("pages_translated_this_month", 0) + total_docs_translated += docs + total_pages_translated += pages + + if docs > 0: + active_users += 1 + + # Get cache stats + cache_stats = _translation_cache.get_stats() + + return { + "users": { + "total": total_users, + "active_this_month": active_users, + "by_plan": plan_distribution + }, + "translations": { + "docs_this_month": total_docs_translated, + "pages_this_month": total_pages_translated + }, + "cache": cache_stats, + "config": { + "translation_service": config.TRANSLATION_SERVICE, + "max_file_size_mb": config.MAX_FILE_SIZE_MB, + "supported_extensions": list(config.SUPPORTED_EXTENSIONS) + } + } + + +@app.post("/admin/config/provider") +async def update_default_provider( + provider: str = Form(...), + is_admin: bool = Depends(require_admin) +): + """Update the default translation provider (requires admin auth)""" + valid_providers = ["google", "openrouter", "ollama", "deepl", "libre", "openai"] + if provider not in valid_providers: + raise HTTPException(status_code=400, detail=f"Invalid provider. Must be one of: {valid_providers}") + + # Update config (in production, this would persist to database/env) + config.TRANSLATION_SERVICE = provider + + return { + "status": "success", + "message": f"Default provider updated to {provider}", + "provider": provider + } + + if __name__ == "__main__": import uvicorn uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) \ No newline at end of file