Add settings UI, Discord notifications, and alert detail improvements
- Settings modal (gear icon) lets you configure all service URLs and API keys from the dashboard; values persist to data/settings.json with process.env as fallback so existing .env.local setups keep working - Per-service Test button hits each service's status endpoint and reports the version on success - Discord webhook support: structured embeds per alert category (requesters, approval age, episode progress, watch-rate stats) sent on new/reopened alerts only — already-open alerts don't re-notify - Alert detail page restructured: prose descriptions replaced with labelled fields, episode progress bar for partial TV, watch-rate stat block, View in Radarr/Sonarr/Seerr action buttons, requester names link to Overseerr profiles, timestamps moved inline with status - Tab state is pure client state (no ?tab= in URL); router.back() used on alert detail for clean browser history Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 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 ?? "",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 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();
|
||||
}
|
||||
Reference in New Issue
Block a user