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
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:
@@ -37,6 +37,10 @@ app.post("/jellyfin", async (c) => {
|
|||||||
return c.json({ ok: true });
|
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) => {
|
app.post("/radarr", async (c) => {
|
||||||
const body = await c.req.json<{ url?: string; api_key?: string }>();
|
const body = await c.req.json<{ url?: string; api_key?: string }>();
|
||||||
const url = body.url?.replace(/\/$/, "");
|
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);
|
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_url", url);
|
||||||
setConfig("radarr_api_key", apiKey);
|
setConfig("radarr_api_key", apiKey);
|
||||||
setConfig("radarr_enabled", "1");
|
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) => {
|
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);
|
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_url", url);
|
||||||
setConfig("sonarr_api_key", apiKey);
|
setConfig("sonarr_api_key", apiKey);
|
||||||
setConfig("sonarr_enabled", "1");
|
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) => {
|
app.post("/subtitle-languages", async (c) => {
|
||||||
@@ -6,7 +6,7 @@ import executeRoutes from "./api/execute";
|
|||||||
import pathsRoutes from "./api/paths";
|
import pathsRoutes from "./api/paths";
|
||||||
import reviewRoutes from "./api/review";
|
import reviewRoutes from "./api/review";
|
||||||
import scanRoutes from "./api/scan";
|
import scanRoutes from "./api/scan";
|
||||||
import setupRoutes from "./api/setup";
|
import settingsRoutes from "./api/settings";
|
||||||
import subtitlesRoutes from "./api/subtitles";
|
import subtitlesRoutes from "./api/subtitles";
|
||||||
import { getDb } from "./db/index";
|
import { getDb } from "./db/index";
|
||||||
import { log } from "./lib/log";
|
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.get("/api/version", (c) => c.json({ version: pkg.version }));
|
||||||
app.route("/api/dashboard", dashboardRoutes);
|
app.route("/api/dashboard", dashboardRoutes);
|
||||||
app.route("/api/setup", setupRoutes);
|
app.route("/api/settings", settingsRoutes);
|
||||||
app.route("/api/scan", scanRoutes);
|
app.route("/api/scan", scanRoutes);
|
||||||
app.route("/api/review", reviewRoutes);
|
app.route("/api/review", reviewRoutes);
|
||||||
app.route("/api/execute", executeRoutes);
|
app.route("/api/execute", executeRoutes);
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export function ScanPage() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [status, setStatus] = useState<ScanStatus | null>(null);
|
const [status, setStatus] = useState<ScanStatus | null>(null);
|
||||||
const [stats, setStats] = useState<DashboardStats | null>(null);
|
const [stats, setStats] = useState<DashboardStats | null>(null);
|
||||||
const [setupChecked, setSetupChecked] = useState(false);
|
const [configChecked, setConfigChecked] = useState(false);
|
||||||
const [limit, setLimit] = useState("");
|
const [limit, setLimit] = useState("");
|
||||||
const [log, setLog] = useState<LogEntry[]>([]);
|
const [log, setLog] = useState<LogEntry[]>([]);
|
||||||
const [statusLabel, setStatusLabel] = useState("");
|
const [statusLabel, setStatusLabel] = useState("");
|
||||||
@@ -87,13 +87,13 @@ export function ScanPage() {
|
|||||||
.get<DashboardData>("/api/dashboard")
|
.get<DashboardData>("/api/dashboard")
|
||||||
.then((d) => {
|
.then((d) => {
|
||||||
setStats(d.stats);
|
setStats(d.stats);
|
||||||
if (!setupChecked) {
|
if (!configChecked) {
|
||||||
setSetupChecked(true);
|
setConfigChecked(true);
|
||||||
if (!d.setupComplete) navigate({ to: "/settings" });
|
if (!d.setupComplete) navigate({ to: "/settings" });
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => setSetupChecked(true));
|
.catch(() => setConfigChecked(true));
|
||||||
}, [navigate, setupChecked]);
|
}, [navigate, configChecked]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadStats();
|
loadStats();
|
||||||
}, [loadStats]);
|
}, [loadStats]);
|
||||||
|
|||||||
@@ -5,12 +5,20 @@ import { Select } from "~/shared/components/ui/select";
|
|||||||
import { api } from "~/shared/lib/api";
|
import { api } from "~/shared/lib/api";
|
||||||
import { LANG_NAMES } from "~/shared/lib/lang";
|
import { LANG_NAMES } from "~/shared/lib/lang";
|
||||||
|
|
||||||
interface SetupData {
|
interface SettingsData {
|
||||||
config: Record<string, string>;
|
config: Record<string, string>;
|
||||||
envLocked: 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 }));
|
const LANGUAGE_OPTIONS = Object.entries(LANG_NAMES).map(([code, label]) => ({ code, label }));
|
||||||
|
|
||||||
@@ -163,21 +171,29 @@ function ConnSection({
|
|||||||
urlKey: string;
|
urlKey: string;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
urlPlaceholder: string;
|
urlPlaceholder: string;
|
||||||
onSave: (url: string, apiKey: string) => Promise<void>;
|
onSave: (url: string, apiKey: string) => Promise<SaveResult>;
|
||||||
}) {
|
}) {
|
||||||
const [url, setUrl] = useState(cfg[urlKey] ?? "");
|
const [url, setUrl] = useState(cfg[urlKey] ?? "");
|
||||||
const [key, setKey] = useState(cfg[apiKeyProp] ?? "");
|
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 [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
setStatus(null);
|
setStatus(null);
|
||||||
try {
|
try {
|
||||||
await onSave(url, key);
|
const result = await onSave(url, key);
|
||||||
setStatus({ ok: true });
|
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) {
|
} catch (e) {
|
||||||
setStatus({ ok: false, error: String(e) });
|
setStatus({ kind: "request-error", error: String(e) });
|
||||||
}
|
}
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
};
|
};
|
||||||
@@ -209,11 +225,14 @@ function ConnSection({
|
|||||||
<Button onClick={save} disabled={saving || (locked.has(urlKey) && locked.has(apiKeyProp))}>
|
<Button onClick={save} disabled={saving || (locked.has(urlKey) && locked.has(apiKeyProp))}>
|
||||||
{saving ? "Saving…" : "Test & Save"}
|
{saving ? "Saving…" : "Test & Save"}
|
||||||
</Button>
|
</Button>
|
||||||
{status && (
|
{status?.kind === "saved" && <span className="text-sm text-green-700">✓ Saved & connected</span>}
|
||||||
<span className={`text-sm ${status.ok ? "text-green-700" : "text-red-600"}`}>
|
{status?.kind === "saved-test-failed" && (
|
||||||
{status.ok ? "✓ Saved" : `✗ ${status.error ?? "Connection failed"}`}
|
<span className="text-sm text-amber-700" title={status.error}>
|
||||||
|
⚠ Saved, but connection test failed: {status.error}
|
||||||
</span>
|
</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>
|
</div>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
);
|
);
|
||||||
@@ -221,9 +240,9 @@ function ConnSection({
|
|||||||
|
|
||||||
// ─── Setup page ───────────────────────────────────────────────────────────────
|
// ─── Setup page ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function SetupPage() {
|
export function SettingsPage() {
|
||||||
const [data, setData] = useState<SetupData | null>(setupCache);
|
const [data, setData] = useState<SettingsData | null>(settingsCache);
|
||||||
const [loading, setLoading] = useState(setupCache === null);
|
const [loading, setLoading] = useState(settingsCache === null);
|
||||||
const [clearStatus, setClearStatus] = useState("");
|
const [clearStatus, setClearStatus] = useState("");
|
||||||
const [subLangs, setSubLangs] = useState<string[]>([]);
|
const [subLangs, setSubLangs] = useState<string[]>([]);
|
||||||
const [subSaved, setSubSaved] = useState("");
|
const [subSaved, setSubSaved] = useState("");
|
||||||
@@ -232,11 +251,11 @@ export function SetupPage() {
|
|||||||
const langsLoadedRef = useRef(false);
|
const langsLoadedRef = useRef(false);
|
||||||
|
|
||||||
const load = useCallback(() => {
|
const load = useCallback(() => {
|
||||||
if (!setupCache) setLoading(true);
|
if (!settingsCache) setLoading(true);
|
||||||
api
|
api
|
||||||
.get<SetupData>("/api/setup")
|
.get<SettingsData>("/api/settings")
|
||||||
.then((d) => {
|
.then((d) => {
|
||||||
setupCache = d;
|
settingsCache = d;
|
||||||
setData(d);
|
setData(d);
|
||||||
if (!langsLoadedRef.current) {
|
if (!langsLoadedRef.current) {
|
||||||
setSubLangs(JSON.parse(d.config.subtitle_languages ?? '["eng","deu","spa"]'));
|
setSubLangs(JSON.parse(d.config.subtitle_languages ?? '["eng","deu","spa"]'));
|
||||||
@@ -255,23 +274,20 @@ export function SetupPage() {
|
|||||||
|
|
||||||
const { config: cfg, envLocked: envLockedArr } = data;
|
const { config: cfg, envLocked: envLockedArr } = data;
|
||||||
const locked = new Set(envLockedArr);
|
const locked = new Set(envLockedArr);
|
||||||
const saveJellyfin = async (url: string, apiKey: string): Promise<void> => {
|
const saveJellyfin = (url: string, apiKey: string) =>
|
||||||
await api.post("/api/setup/jellyfin", { url, api_key: apiKey });
|
api.post<SaveResult>("/api/settings/jellyfin", { url, api_key: apiKey });
|
||||||
};
|
const saveRadarr = (url: string, apiKey: string) =>
|
||||||
const saveRadarr = async (url: string, apiKey: string): Promise<void> => {
|
api.post<SaveResult>("/api/settings/radarr", { url, api_key: apiKey });
|
||||||
await api.post("/api/setup/radarr", { url, api_key: apiKey });
|
const saveSonarr = (url: string, apiKey: string) =>
|
||||||
};
|
api.post<SaveResult>("/api/settings/sonarr", { url, api_key: apiKey });
|
||||||
const saveSonarr = async (url: string, apiKey: string): Promise<void> => {
|
|
||||||
await api.post("/api/setup/sonarr", { url, api_key: apiKey });
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveSubtitleLangs = async () => {
|
const saveSubtitleLangs = async () => {
|
||||||
await api.post("/api/setup/subtitle-languages", { langs: subLangs });
|
await api.post("/api/settings/subtitle-languages", { langs: subLangs });
|
||||||
setSubSaved("Saved.");
|
setSubSaved("Saved.");
|
||||||
setTimeout(() => setSubSaved(""), 2000);
|
setTimeout(() => setSubSaved(""), 2000);
|
||||||
};
|
};
|
||||||
const saveAudioLangs = async () => {
|
const saveAudioLangs = async () => {
|
||||||
await api.post("/api/setup/audio-languages", { langs: audLangs });
|
await api.post("/api/settings/audio-languages", { langs: audLangs });
|
||||||
setAudSaved("Saved.");
|
setAudSaved("Saved.");
|
||||||
setTimeout(() => setAudSaved(""), 2000);
|
setTimeout(() => setAudSaved(""), 2000);
|
||||||
};
|
};
|
||||||
@@ -283,7 +299,7 @@ export function SetupPage() {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
await api.post("/api/setup/clear-scan");
|
await api.post("/api/settings/clear-scan");
|
||||||
setClearStatus("Cleared.");
|
setClearStatus("Cleared.");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { SetupPage } from "~/features/setup/SetupPage";
|
import { SettingsPage } from "~/features/settings/SettingsPage";
|
||||||
|
|
||||||
export const Route = createFileRoute("/settings")({
|
export const Route = createFileRoute("/settings")({
|
||||||
component: SetupPage,
|
component: SettingsPage,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user