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 });
|
||||
});
|
||||
|
||||
// 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) => {
|
||||
@@ -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);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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 & 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.");
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user