From 0e4c276816b1797da837a6e55242b09ec40b831f Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 21 Feb 2026 12:09:21 +0100 Subject: [PATCH] feat: implement UI-driven database configuration --- src/app/(dashboard)/settings/page.tsx | 140 ++++++++++++++++-- src/db/index.ts | 20 ++- src/features/settings/actions.ts | 52 +++++++ .../settings/store/use-db-settings-store.ts | 53 +++++++ 4 files changed, 248 insertions(+), 17 deletions(-) create mode 100644 src/features/settings/actions.ts create mode 100644 src/features/settings/store/use-db-settings-store.ts diff --git a/src/app/(dashboard)/settings/page.tsx b/src/app/(dashboard)/settings/page.tsx index 3627c0c..03e6341 100644 --- a/src/app/(dashboard)/settings/page.tsx +++ b/src/app/(dashboard)/settings/page.tsx @@ -5,6 +5,9 @@ import { Save, Eye, EyeOff, CheckCircle, AlertCircle, Loader2, RefreshCw, Sun, M import { useTheme } from "next-themes"; import { useGridStore } from "@/features/grid/store/use-grid-store"; import { useScoreSettingsStore } from "@/features/score/store/use-score-settings-store"; +import { useDbSettingsStore } from "@/features/settings/store/use-db-settings-store"; +import { testDatabaseConnection, saveDatabaseSettings, getSavedDatabaseConfig } from "@/features/settings/actions"; +import { useEffect } from "react"; interface OpenRouterModel { id: string; name: string; free: boolean; } @@ -50,11 +53,45 @@ export default function SettingsPage() { const [showKey, setShowKey] = useState(false); const [apiKey, setApiKey] = useState(""); const [dbStatus, setDbStatus] = useState<"idle" | "testing" | "ok" | "error">("idle"); - const [saveStatus, setSaveStatus] = useState<"idle" | "saved">("idle"); + const [dbError, setDbError] = useState(null); + const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "saved">("idle"); const [models, setModels] = useState([]); const [selectedModel, setSelectedModel] = useState("google/gemini-flash-1.5"); const [modelsStatus, setModelsStatus] = useState<"idle" | "loading" | "ok" | "error">("idle"); + const { + host, setHost, + port, setPort, + database, setDatabase, + user, setUser, + password, setPassword, + ssl, setSsl, + getDatabaseUrl + } = useDbSettingsStore(); + + useEffect(() => { + // Optionnel : On pourrait charger la config serveur au montage si on veut écraser le localStorage + // Pour l'instant on laisse le localStorage faire son travail, mais on offre une fonction de reset + }, []); + + const reloadFromServer = async () => { + const config = await getSavedDatabaseConfig(); + if (config && 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); + } + } + }; + const fetchModels = useCallback(async (key: string) => { if (!key.trim()) return; setModelsStatus("loading"); @@ -69,10 +106,30 @@ export default function SettingsPage() { } }, []); - const testConnection = async () => { + const testDb = async () => { setDbStatus("testing"); - await new Promise((r) => setTimeout(r, 1200)); - setDbStatus("error"); + 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); + } }; return ( @@ -85,38 +142,90 @@ export default function SettingsPage() { {/* 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 — vérifiez les paramètres +
+ Connexion refusée + {dbError && {dbError}} +
)} + {dbStatus === "ok" && ( + Connexion réussie ! + )} +
+
+
@@ -281,12 +390,13 @@ export default function SettingsPage() { {/* Save */}
diff --git a/src/db/index.ts b/src/db/index.ts index f4cb416..17f8182 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -1,10 +1,26 @@ -import { drizzle } from "drizzle-orm/pg-proxy"; // Or node-postgres if using directly import { drizzle as nodeDrizzle } from "drizzle-orm/node-postgres"; import { Pool } from "pg"; import * as schema from "./schema"; +import fs from "fs"; +import path from "path"; + +let connectionString = process.env.DATABASE_URL; + +// On tente de lire la config sauvegardée via l'UI +try { + const CONFIG_PATH = path.join(process.cwd(), ".db-config.json"); + if (fs.existsSync(CONFIG_PATH)) { + const config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8")); + if (config.url) { + connectionString = config.url; + } + } +} catch (err) { + console.error("Error reading database config file:", err); +} const pool = new Pool({ - connectionString: process.env.DATABASE_URL, + connectionString, }); export const db = nodeDrizzle(pool, { schema }); diff --git a/src/features/settings/actions.ts b/src/features/settings/actions.ts new file mode 100644 index 0000000..31791ce --- /dev/null +++ b/src/features/settings/actions.ts @@ -0,0 +1,52 @@ +"use server"; + +import { Pool } from "pg"; +import fs from "fs/promises"; +import path from "path"; + +const CONFIG_FILE = path.join(process.cwd(), ".db-config.json"); + +export interface DbConfig { + url: string; +} + +export async function testDatabaseConnection(url: string) { + console.log("Testing connection to:", url.replace(/:([^@]+)@/, ":****@")); + const pool = new Pool({ + connectionString: url, + connectionTimeoutMillis: 5000, + }); + + try { + const client = await pool.connect(); + await client.query("SELECT 1"); + client.release(); + return { success: true }; + } catch (error: any) { + console.error("Database connection test failed:", error.message); + return { success: false, error: error.message }; + } finally { + await pool.end(); + } +} + +export async function saveDatabaseSettings(url: string) { + try { + const config: DbConfig = { url }; + await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2)); + console.log("Database configuration saved to:", CONFIG_FILE); + return { success: true }; + } catch (error: any) { + console.error("Failed to save database configuration:", error.message); + return { success: false, error: error.message }; + } +} + +export async function getSavedDatabaseConfig(): Promise { + try { + const data = await fs.readFile(CONFIG_FILE, "utf-8"); + return JSON.parse(data); + } catch { + return null; + } +} diff --git a/src/features/settings/store/use-db-settings-store.ts b/src/features/settings/store/use-db-settings-store.ts new file mode 100644 index 0000000..2c0d55a --- /dev/null +++ b/src/features/settings/store/use-db-settings-store.ts @@ -0,0 +1,53 @@ +"use client"; + +import { create } from "zustand"; +import { persist } from "zustand/middleware"; + +export interface DatabaseSettings { + host: string; + port: string; + database: string; + user: string; + password?: string; + ssl: boolean; +} + +interface DatabaseSettingsState extends DatabaseSettings { + setHost: (host: string) => void; + setPort: (port: string) => void; + setDatabase: (database: string) => void; + setUser: (user: string) => void; + setPassword: (password: string) => void; + setSsl: (ssl: boolean) => void; + getDatabaseUrl: () => string; +} + +export const useDbSettingsStore = create()( + persist( + (set, get) => ({ + host: "localhost", + port: "5432", + database: "collectflow", + user: "postgres", + password: "", + ssl: false, + + setHost: (host) => set({ host }), + setPort: (port) => set({ port }), + setDatabase: (database) => set({ database }), + setUser: (user) => set({ user }), + setPassword: (password) => set({ password }), + setSsl: (ssl) => set({ ssl }), + + getDatabaseUrl: () => { + const { user, password, host, port, database, ssl } = get(); + const auth = password ? `${user}:${password}` : user; + const url = `postgres://${auth}@${host}:${port}/${database}${ssl ? "?sslmode=require" : ""}`; + return url; + }, + }), + { + name: "collectflow-db-settings", + } + ) +);