"use client"; import { useState, useCallback } from "react"; import { Save, Eye, EyeOff, CheckCircle, AlertCircle, Loader2, RefreshCw, Sun, Moon, RotateCcw } from "lucide-react"; import { useTheme } from "next-themes"; import { useGridStore } from "@/features/grid/store/use-grid-store"; import { useDbSettingsStore } from "@/features/settings/store/use-db-settings-store"; import { testDatabaseConnection, saveDatabaseSettings, getSavedDatabaseConfig, saveQlikSettings, testQlikConnection, saveFfApiSettings, testFfApiConnection } from "@/features/settings/actions"; import { useEffect } from "react"; import { UserManagement } from "@/features/admin/components/user-management"; import { ApiKeyManagement } from "@/features/admin/components/api-key-management"; import { ApiConnectionInfo } from "@/features/admin/components/api-connection-info"; import { GridWarmup } from "@/features/admin/components/grid-warmup"; import { ServerLogs } from "@/features/settings/components/server-logs"; function Section({ title, subtitle, children }: { title: string; subtitle?: string; children: React.ReactNode }) { return (

{title}

{subtitle &&

{subtitle}

}
{children}
); } function Field({ label, hint, children }: { label: string; hint?: string; children: React.ReactNode }) { return (
{children} {hint &&

{hint}

}
); } interface FfSyncTable { nom: string; derniereSync: string; nbLignes?: number; statut?: string; erreur?: string | null; } interface FfSyncStatus { lastSync: string; tables: FfSyncTable[]; } function FfApiStatusSection() { const [status, setStatus] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [url, setUrl] = useState(""); const [savedUrl, setSavedUrl] = useState(null); const [saving, setSaving] = useState(false); const [testedUrl, setTestedUrl] = useState(null); // Charge l'URL enregistrée pour la préremplir (vide = valeur par défaut). useEffect(() => { getSavedDatabaseConfig() .then((cfg) => { setSavedUrl(cfg?.ffApiBaseUrl ?? null); if (cfg?.ffApiBaseUrl) setUrl(cfg.ffApiBaseUrl); }) .catch(() => { /* réglage optionnel : on laisse le champ vide */ }); }, []); const runTest = useCallback((candidate?: string) => { setLoading(true); setError(null); setStatus(null); testFfApiConnection(candidate) .then((res) => { setTestedUrl(res.url); if (res.success) setStatus(res.status as FfSyncStatus); else setError(res.error ?? "Échec inconnu"); }) .catch((e) => setError(e instanceof Error ? e.message : "Erreur inconnue")) .finally(() => setLoading(false)); }, []); useEffect(() => { runTest(); }, [runTest]); const save = async () => { setSaving(true); setError(null); try { const res = await saveFfApiSettings(url); if (!res.success) { setError(res.error ?? "Enregistrement impossible"); return; } setSavedUrl(url.trim() || null); runTest(url); } finally { setSaving(false); } }; return (
setUrl(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") void save(); }} className="apple-input w-full" />
{status && Connexion OK} {error && {error}}
{/* L'URL réellement appelée : évite de croire qu'on teste celle du champ alors que le réglage enregistré ou la variable d'environnement prime. */} {testedUrl && (

Adresse testée : {testedUrl} {!savedUrl && <> — valeur par défaut, aucun réglage enregistré}

)} {status && (

Dernière sync globale : {(() => { const d = new Date(status.lastSync); return isNaN(d.getTime()) ? (status.lastSync ?? "—") : d.toLocaleString("fr-FR"); })()}

{status.tables?.length > 0 && (
{status.tables.map((t) => ( {/* Une table en erreur est précisément ce qu'on vient chercher ici. */} ))}
Table Sync Lignes État
{t.nom} {(() => { const d = new Date(t.derniereSync); return isNaN(d.getTime()) ? (t.derniereSync ?? "—") : d.toLocaleString("fr-FR"); })()} {t.nbLignes?.toLocaleString("fr-FR") ?? "—"} {t.statut === "ok" ? ok : {t.statut ?? "—"}}
)}
)}
); } function QlikSettingsSection() { const [host, setHost] = useState(""); const [user, setUser] = useState(""); const [password, setPassword] = useState(""); const [showPwd, setShowPwd] = useState(false); const [saving, setSaving] = useState(false); const [testing, setTesting] = useState(false); const [status, setStatus] = useState<{ type: "ok" | "err"; msg: string } | null>(null); useEffect(() => { getSavedDatabaseConfig().then((c) => { if (!c) return; setHost(c.qlikHost ?? ""); setUser(c.qlikUser ?? ""); setPassword(c.qlikPassword ?? ""); }); }, []); const handleSave = async () => { setSaving(true); setStatus(null); const res = await saveQlikSettings(host.trim(), user.trim(), password); setSaving(false); setStatus(res.success ? { type: "ok", msg: "Réglages Qlik enregistrés" } : { type: "err", msg: res.error || "Erreur" }); }; const handleTest = async () => { setTesting(true); setStatus(null); const res = await testQlikConnection(host.trim(), user.trim(), password); setTesting(false); setStatus(res.success ? { type: "ok", msg: "Connexion Qlik OK" } : { type: "err", msg: res.error || "Échec connexion" }); }; return (
setHost(e.target.value)} className="apple-input font-mono" /> setUser(e.target.value)} className="apple-input font-mono" />
setPassword(e.target.value)} className="apple-input font-mono pr-10" />
{status && (
{status.type === "ok" ? : } {status.msg}
)}
); } export default function SettingsPage() { const { resolvedTheme, setTheme } = useTheme(); const isDark = resolvedTheme === "dark"; const displayDensity = useGridStore((s) => s.displayDensity); const setDisplayDensity = useGridStore((s) => s.setDisplayDensity); const [dbStatus, setDbStatus] = useState<"idle" | "testing" | "ok" | "error">("idle"); const [dbError, setDbError] = useState(null); const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "saved">("idle"); const [isMounted, setIsMounted] = useState(false); const { host, setHost, port, setPort, database, setDatabase, user, setUser, password, setPassword, ssl, setSsl, getDatabaseUrl } = useDbSettingsStore(); const reloadFromServer = useCallback(async () => { const config = await getSavedDatabaseConfig(); if (config) { if (config.url) { // Parser l'URL pour remettre dans le store try { const url = new URL(config.url.replace("postgres://", "http://")); // URL parser helper setHost(url.hostname); setPort(url.port || "5432"); setDatabase(url.pathname.slice(1).split("?")[0]); setUser(url.username); setPassword(decodeURIComponent(url.password)); setSsl(config.url.includes("sslmode=require")); } catch (e) { console.error("Failed to parse saved URL", e); } } } }, [setDatabase, setHost, setPassword, setPort, setSsl, setUser]); useEffect(() => { // `isMounted` sert à éviter un écart d'hydratation : il doit donc être posé // au montage, ce que la règle déconseille en général mais qui est ici le but. // eslint-disable-next-line react-hooks/set-state-in-effect setIsMounted(true); reloadFromServer(); }, [reloadFromServer]); const testDb = async () => { setDbStatus("testing"); setDbError(null); const url = getDatabaseUrl(); const res = await testDatabaseConnection(url); if (res.success) { setDbStatus("ok"); } else { setDbStatus("error"); setDbError(res.error || "Erreur de connexion inconnue"); } }; const handleSave = async () => { setSaveStatus("saving"); const url = getDatabaseUrl(); const res = await saveDatabaseSettings(url); if (res.success) { setSaveStatus("saved"); setTimeout(() => setSaveStatus("idle"), 2500); } else { setSaveStatus("idle"); alert("Erreur lors de la sauvegarde : " + res.error); } }; if (!isMounted) { return null; } return (

Paramètres

Configuration de la connexion et des services.

{/* PostgreSQL */}
setHost(e.target.value)} className="apple-input font-mono" />
setPort(e.target.value)} className="apple-input font-mono" /> setDatabase(e.target.value)} className="apple-input font-mono" />
setUser(e.target.value)} className="apple-input font-mono" /> setPassword(e.target.value)} className="apple-input font-mono" />
setSsl(e.target.checked)} className="w-3.5 h-3.5 rounded accent-teal-500 cursor-pointer" />
{dbStatus === "error" && (
Connexion refusée {dbError && {dbError}}
)} {dbStatus === "ok" && ( Connexion réussie ! )}
{/* API FF Nancy */} {/* Apparence */}

Thème

Interface claire ou sombre

{/* Segment control */}

Densité de la grille

Hauteur de ligne par défaut

{/* Journaux serveur — diagnostic des extractions Qlik */}
{/* Gestion des Utilisateurs */}
{/* API publique /api/v1 — connexion puis gestion des clés */}
{/* Save Button — Floating/Sticky style at bottom */}
); }