2b9883d99f
Build and Push / build (push) Successful in 1m9s
When Radarr, Sonarr, or Overseerr is missing a URL or API key, the stats API now returns 428 and the dashboard renders a full-page setup form instead of the empty shell + fetch-error UI. The form reuses the existing service/discord inputs (extracted out of the settings modal so both can share them), and the background poller skips silently until setup is complete. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
96 lines
3.1 KiB
TypeScript
96 lines
3.1 KiB
TypeScript
/**
|
|
* Persistent settings store.
|
|
*
|
|
* Settings are read from data/settings.json when present, with process.env
|
|
* values used as fallbacks. This means existing .env.local setups keep working
|
|
* with no changes; the UI just provides an alternative way to configure them.
|
|
*/
|
|
|
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
import { join } from "path";
|
|
|
|
const DATA_DIR = join(process.cwd(), "data");
|
|
const SETTINGS_PATH = join(DATA_DIR, "settings.json");
|
|
|
|
export interface ServiceConfig {
|
|
url: string;
|
|
apiKey: string;
|
|
}
|
|
|
|
export interface DiscordConfig {
|
|
webhookUrl: string;
|
|
}
|
|
|
|
export interface AppSettings {
|
|
radarr: ServiceConfig;
|
|
sonarr: ServiceConfig;
|
|
seerr: ServiceConfig;
|
|
tautulli: ServiceConfig;
|
|
discord: DiscordConfig;
|
|
}
|
|
|
|
interface StoredSettings {
|
|
radarr?: Partial<ServiceConfig>;
|
|
sonarr?: Partial<ServiceConfig>;
|
|
seerr?: Partial<ServiceConfig>;
|
|
tautulli?: Partial<ServiceConfig>;
|
|
discord?: Partial<DiscordConfig>;
|
|
}
|
|
|
|
function readFile(): StoredSettings {
|
|
try {
|
|
return JSON.parse(readFileSync(SETTINGS_PATH, "utf-8")) as StoredSettings;
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
/** Returns the merged settings (file values override env vars). */
|
|
export function getSettings(): AppSettings {
|
|
const f = readFile();
|
|
return {
|
|
radarr: {
|
|
url: f.radarr?.url ?? process.env.RADARR_URL ?? "",
|
|
apiKey: f.radarr?.apiKey ?? process.env.RADARR_API ?? "",
|
|
},
|
|
sonarr: {
|
|
url: f.sonarr?.url ?? process.env.SONARR_URL ?? "",
|
|
apiKey: f.sonarr?.apiKey ?? process.env.SONARR_API ?? "",
|
|
},
|
|
seerr: {
|
|
url: f.seerr?.url ?? process.env.SEERR_URL ?? "",
|
|
apiKey: f.seerr?.apiKey ?? process.env.SEERR_API ?? "",
|
|
},
|
|
tautulli: {
|
|
url: f.tautulli?.url ?? process.env.TAUTULLI_URL ?? "",
|
|
apiKey: f.tautulli?.apiKey ?? process.env.TAUTULLI_API ?? "",
|
|
},
|
|
discord: {
|
|
webhookUrl: f.discord?.webhookUrl ?? process.env.DISCORD_WEBHOOK ?? "",
|
|
},
|
|
};
|
|
}
|
|
|
|
/** Returns true iff the three services required for the dashboard to function (Radarr, Sonarr, Overseerr/Jellyseerr) have both a URL and API key. */
|
|
export function isConfigured(s: AppSettings = getSettings()): boolean {
|
|
const filled = (c: ServiceConfig) => c.url.trim() !== "" && c.apiKey.trim() !== "";
|
|
return filled(s.radarr) && filled(s.sonarr) && filled(s.seerr);
|
|
}
|
|
|
|
/** Saves the provided settings to disk and returns the merged result. */
|
|
export function saveSettings(settings: AppSettings): AppSettings {
|
|
if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
|
|
|
|
// Strip trailing slashes from URLs for consistency
|
|
const clean: StoredSettings = {
|
|
radarr: { url: settings.radarr.url.replace(/\/+$/, ""), apiKey: settings.radarr.apiKey },
|
|
sonarr: { url: settings.sonarr.url.replace(/\/+$/, ""), apiKey: settings.sonarr.apiKey },
|
|
seerr: { url: settings.seerr.url.replace(/\/+$/, ""), apiKey: settings.seerr.apiKey },
|
|
tautulli: { url: settings.tautulli.url.replace(/\/+$/, ""), apiKey: settings.tautulli.apiKey },
|
|
discord: { webhookUrl: settings.discord.webhookUrl.trim() },
|
|
};
|
|
|
|
writeFileSync(SETTINGS_PATH, JSON.stringify(clean, null, 2), "utf-8");
|
|
return getSettings();
|
|
}
|