rename setup → settings throughout; persist arr creds even on test failure
All checks were successful
Build and Push Docker Image / build (push) Successful in 36s

Two cleanups:

1. Rename the page from 'Setup' to 'Settings' all the way down. The H1
   already said Settings; the file/component/api path were lying.
   - src/features/setup/ → src/features/settings/
   - SetupPage.tsx → SettingsPage.tsx, SetupPage → SettingsPage,
     SetupData → SettingsData, setupCache → settingsCache
   - server/api/setup.ts → server/api/settings.ts
   - /api/setup → /api/settings (only consumer is our frontend)
   - server/index.tsx import + route mount renamed
   - ScanPage's local setupChecked → configChecked

2. Sonarr (and Radarr) save flow: persist the values BEFORE running the
   connection test. The previous code returned early if the test failed,
   silently dropping what the user typed — explained the user's report
   that Sonarr 'forgets' the input. Now setConfig fires unconditionally
   on a valid (non-empty) URL+key; the test result is returned as
   { ok, saved, testError } so the UI can show 'Saved & connected' on
   success or '⚠ Saved, but connection test failed: …' on failure
   instead of erasing the input.

Note: setup_complete config key kept as-is — it represents 'has the user
configured Jellyfin' which is conceptually setup and not user-visible.
This commit is contained in:
2026-04-13 12:26:30 +02:00
parent e8f33c6224
commit 94a460be9d
5 changed files with 61 additions and 45 deletions

View File

@@ -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) => {

View File

@@ -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);

View File

@@ -67,7 +67,7 @@ export function ScanPage() {
const navigate = useNavigate();
const [status, setStatus] = useState<ScanStatus | null>(null);
const [stats, setStats] = useState<DashboardStats | null>(null);
const [setupChecked, setSetupChecked] = useState(false);
const [configChecked, setConfigChecked] = useState(false);
const [limit, setLimit] = useState("");
const [log, setLog] = useState<LogEntry[]>([]);
const [statusLabel, setStatusLabel] = useState("");
@@ -87,13 +87,13 @@ export function ScanPage() {
.get<DashboardData>("/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]);

View File

@@ -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<string, string>;
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<void>;
onSave: (url: string, apiKey: string) => Promise<SaveResult>;
}) {
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({
<Button onClick={save} disabled={saving || (locked.has(urlKey) && locked.has(apiKeyProp))}>
{saving ? "Saving…" : "Test & Save"}
</Button>
{status && (
<span className={`text-sm ${status.ok ? "text-green-700" : "text-red-600"}`}>
{status.ok ? "✓ Saved" : `${status.error ?? "Connection failed"}`}
{status?.kind === "saved" && <span className="text-sm text-green-700"> Saved &amp; connected</span>}
{status?.kind === "saved-test-failed" && (
<span className="text-sm text-amber-700" title={status.error}>
Saved, but connection test failed: {status.error}
</span>
)}
{status?.kind === "validation-error" && <span className="text-sm text-red-600"> {status.error}</span>}
{status?.kind === "request-error" && <span className="text-sm text-red-600"> {status.error}</span>}
</div>
</SectionCard>
);
@@ -221,9 +240,9 @@ function ConnSection({
// ─── Setup page ───────────────────────────────────────────────────────────────
export function SetupPage() {
const [data, setData] = useState<SetupData | null>(setupCache);
const [loading, setLoading] = useState(setupCache === null);
export function SettingsPage() {
const [data, setData] = useState<SettingsData | null>(settingsCache);
const [loading, setLoading] = useState(settingsCache === null);
const [clearStatus, setClearStatus] = useState("");
const [subLangs, setSubLangs] = useState<string[]>([]);
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<SetupData>("/api/setup")
.get<SettingsData>("/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<void> => {
await api.post("/api/setup/jellyfin", { url, api_key: apiKey });
};
const saveRadarr = async (url: string, apiKey: string): Promise<void> => {
await api.post("/api/setup/radarr", { url, api_key: apiKey });
};
const saveSonarr = async (url: string, apiKey: string): Promise<void> => {
await api.post("/api/setup/sonarr", { url, api_key: apiKey });
};
const saveJellyfin = (url: string, apiKey: string) =>
api.post<SaveResult>("/api/settings/jellyfin", { url, api_key: apiKey });
const saveRadarr = (url: string, apiKey: string) =>
api.post<SaveResult>("/api/settings/radarr", { url, api_key: apiKey });
const saveSonarr = (url: string, apiKey: string) =>
api.post<SaveResult>("/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.");
};

View File

@@ -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,
});