diff --git a/server/api/setup.ts b/server/api/settings.ts similarity index 86% rename from server/api/setup.ts rename to server/api/settings.ts index 46195c8..0838e8b 100644 --- a/server/api/setup.ts +++ b/server/api/settings.ts @@ -37,6 +37,10 @@ app.post("/jellyfin", async (c) => { return c.json({ ok: true }); }); +// Persist values BEFORE testing the connection. The previous behaviour +// silently dropped what the user typed when the test failed (e.g. Sonarr +// not yet reachable), making the field appear to "forget" the input on +// reload. Save first, surface the test result as a warning the UI can show. app.post("/radarr", async (c) => { const body = await c.req.json<{ url?: string; api_key?: string }>(); const url = body.url?.replace(/\/$/, ""); @@ -47,14 +51,12 @@ app.post("/radarr", async (c) => { return c.json({ ok: false, error: "URL and API key are required" }, 400); } - const result = await testRadarr({ url, apiKey }); - if (!result.ok) return c.json({ ok: false, error: result.error }); - setConfig("radarr_url", url); setConfig("radarr_api_key", apiKey); setConfig("radarr_enabled", "1"); - return c.json({ ok: true }); + const result = await testRadarr({ url, apiKey }); + return c.json({ ok: result.ok, saved: true, testError: result.ok ? undefined : result.error }); }); app.post("/sonarr", async (c) => { @@ -67,14 +69,12 @@ app.post("/sonarr", async (c) => { return c.json({ ok: false, error: "URL and API key are required" }, 400); } - const result = await testSonarr({ url, apiKey }); - if (!result.ok) return c.json({ ok: false, error: result.error }); - setConfig("sonarr_url", url); setConfig("sonarr_api_key", apiKey); setConfig("sonarr_enabled", "1"); - return c.json({ ok: true }); + const result = await testSonarr({ url, apiKey }); + return c.json({ ok: result.ok, saved: true, testError: result.ok ? undefined : result.error }); }); app.post("/subtitle-languages", async (c) => { diff --git a/server/index.tsx b/server/index.tsx index b7f77d7..5e6f71b 100644 --- a/server/index.tsx +++ b/server/index.tsx @@ -6,7 +6,7 @@ import executeRoutes from "./api/execute"; import pathsRoutes from "./api/paths"; import reviewRoutes from "./api/review"; import scanRoutes from "./api/scan"; -import setupRoutes from "./api/setup"; +import settingsRoutes from "./api/settings"; import subtitlesRoutes from "./api/subtitles"; import { getDb } from "./db/index"; import { log } from "./lib/log"; @@ -34,7 +34,7 @@ import pkg from "../package.json"; app.get("/api/version", (c) => c.json({ version: pkg.version })); app.route("/api/dashboard", dashboardRoutes); -app.route("/api/setup", setupRoutes); +app.route("/api/settings", settingsRoutes); app.route("/api/scan", scanRoutes); app.route("/api/review", reviewRoutes); app.route("/api/execute", executeRoutes); diff --git a/src/features/scan/ScanPage.tsx b/src/features/scan/ScanPage.tsx index ba00159..f8778d9 100644 --- a/src/features/scan/ScanPage.tsx +++ b/src/features/scan/ScanPage.tsx @@ -67,7 +67,7 @@ export function ScanPage() { const navigate = useNavigate(); const [status, setStatus] = useState(null); const [stats, setStats] = useState(null); - const [setupChecked, setSetupChecked] = useState(false); + const [configChecked, setConfigChecked] = useState(false); const [limit, setLimit] = useState(""); const [log, setLog] = useState([]); const [statusLabel, setStatusLabel] = useState(""); @@ -87,13 +87,13 @@ export function ScanPage() { .get("/api/dashboard") .then((d) => { setStats(d.stats); - if (!setupChecked) { - setSetupChecked(true); + if (!configChecked) { + setConfigChecked(true); if (!d.setupComplete) navigate({ to: "/settings" }); } }) - .catch(() => setSetupChecked(true)); - }, [navigate, setupChecked]); + .catch(() => setConfigChecked(true)); + }, [navigate, configChecked]); useEffect(() => { loadStats(); }, [loadStats]); diff --git a/src/features/setup/SetupPage.tsx b/src/features/settings/SettingsPage.tsx similarity index 82% rename from src/features/setup/SetupPage.tsx rename to src/features/settings/SettingsPage.tsx index 378601e..bd1d7ba 100644 --- a/src/features/setup/SetupPage.tsx +++ b/src/features/settings/SettingsPage.tsx @@ -5,12 +5,20 @@ import { Select } from "~/shared/components/ui/select"; import { api } from "~/shared/lib/api"; import { LANG_NAMES } from "~/shared/lib/lang"; -interface SetupData { +interface SettingsData { config: Record; envLocked: string[]; } -let setupCache: SetupData | null = null; +/** Server response from /api/settings/{jellyfin,radarr,sonarr}. */ +interface SaveResult { + ok: boolean; // connection test passed + saved?: boolean; // values were persisted (true even when test failed) + testError?: string; + error?: string; // validation error (URL/API key required) +} + +let settingsCache: SettingsData | null = null; const LANGUAGE_OPTIONS = Object.entries(LANG_NAMES).map(([code, label]) => ({ code, label })); @@ -163,21 +171,29 @@ function ConnSection({ urlKey: string; apiKey: string; urlPlaceholder: string; - onSave: (url: string, apiKey: string) => Promise; + onSave: (url: string, apiKey: string) => Promise; }) { const [url, setUrl] = useState(cfg[urlKey] ?? ""); const [key, setKey] = useState(cfg[apiKeyProp] ?? ""); - const [status, setStatus] = useState<{ ok: boolean; error?: string } | null>(null); + const [status, setStatus] = useState< + | { kind: "saved" } + | { kind: "saved-test-failed"; error: string } + | { kind: "validation-error"; error: string } + | { kind: "request-error"; error: string } + | null + >(null); const [saving, setSaving] = useState(false); const save = async () => { setSaving(true); setStatus(null); try { - await onSave(url, key); - setStatus({ ok: true }); + const result = await onSave(url, key); + if (result.saved && result.ok) setStatus({ kind: "saved" }); + else if (result.saved) setStatus({ kind: "saved-test-failed", error: result.testError ?? "Connection test failed" }); + else setStatus({ kind: "validation-error", error: result.error ?? "Save failed" }); } catch (e) { - setStatus({ ok: false, error: String(e) }); + setStatus({ kind: "request-error", error: String(e) }); } setSaving(false); }; @@ -209,11 +225,14 @@ function ConnSection({ - {status && ( - - {status.ok ? "✓ Saved" : `✗ ${status.error ?? "Connection failed"}`} + {status?.kind === "saved" && ✓ Saved & connected} + {status?.kind === "saved-test-failed" && ( + + ⚠ Saved, but connection test failed: {status.error} )} + {status?.kind === "validation-error" && ✗ {status.error}} + {status?.kind === "request-error" && ✗ {status.error}} ); @@ -221,9 +240,9 @@ function ConnSection({ // ─── Setup page ─────────────────────────────────────────────────────────────── -export function SetupPage() { - const [data, setData] = useState(setupCache); - const [loading, setLoading] = useState(setupCache === null); +export function SettingsPage() { + const [data, setData] = useState(settingsCache); + const [loading, setLoading] = useState(settingsCache === null); const [clearStatus, setClearStatus] = useState(""); const [subLangs, setSubLangs] = useState([]); const [subSaved, setSubSaved] = useState(""); @@ -232,11 +251,11 @@ export function SetupPage() { const langsLoadedRef = useRef(false); const load = useCallback(() => { - if (!setupCache) setLoading(true); + if (!settingsCache) setLoading(true); api - .get("/api/setup") + .get("/api/settings") .then((d) => { - setupCache = d; + settingsCache = d; setData(d); if (!langsLoadedRef.current) { setSubLangs(JSON.parse(d.config.subtitle_languages ?? '["eng","deu","spa"]')); @@ -255,23 +274,20 @@ export function SetupPage() { const { config: cfg, envLocked: envLockedArr } = data; const locked = new Set(envLockedArr); - const saveJellyfin = async (url: string, apiKey: string): Promise => { - await api.post("/api/setup/jellyfin", { url, api_key: apiKey }); - }; - const saveRadarr = async (url: string, apiKey: string): Promise => { - await api.post("/api/setup/radarr", { url, api_key: apiKey }); - }; - const saveSonarr = async (url: string, apiKey: string): Promise => { - await api.post("/api/setup/sonarr", { url, api_key: apiKey }); - }; + const saveJellyfin = (url: string, apiKey: string) => + api.post("/api/settings/jellyfin", { url, api_key: apiKey }); + const saveRadarr = (url: string, apiKey: string) => + api.post("/api/settings/radarr", { url, api_key: apiKey }); + const saveSonarr = (url: string, apiKey: string) => + api.post("/api/settings/sonarr", { url, api_key: apiKey }); const saveSubtitleLangs = async () => { - await api.post("/api/setup/subtitle-languages", { langs: subLangs }); + await api.post("/api/settings/subtitle-languages", { langs: subLangs }); setSubSaved("Saved."); setTimeout(() => setSubSaved(""), 2000); }; const saveAudioLangs = async () => { - await api.post("/api/setup/audio-languages", { langs: audLangs }); + await api.post("/api/settings/audio-languages", { langs: audLangs }); setAudSaved("Saved."); setTimeout(() => setAudSaved(""), 2000); }; @@ -283,7 +299,7 @@ export function SetupPage() { ) ) return; - await api.post("/api/setup/clear-scan"); + await api.post("/api/settings/clear-scan"); setClearStatus("Cleared."); }; diff --git a/src/routes/settings.tsx b/src/routes/settings.tsx index fd96734..8e18d99 100644 --- a/src/routes/settings.tsx +++ b/src/routes/settings.tsx @@ -1,6 +1,6 @@ import { createFileRoute } from "@tanstack/react-router"; -import { SetupPage } from "~/features/setup/SetupPage"; +import { SettingsPage } from "~/features/settings/SettingsPage"; export const Route = createFileRoute("/settings")({ - component: SetupPage, + component: SettingsPage, });