This commit is contained in:
ponzischeme89
2026-08-22 21:47:54 +12:00
parent d2f7f84cbf
commit 9a1f44da46
27 changed files with 452 additions and 110 deletions
+6 -3
View File
@@ -757,15 +757,16 @@ export interface GoroutineReport {
/* ---------- gateway settings ---------- */
/** GatewaySettings is the operator's overrides. Every field is optional in meaning: an
* empty string or a zero means "whatever the container was started with", and -1 means
* off for the three that can be switched off. */
/** GatewaySettings is the operator's server document. Environment-backed fields use an
* empty string or zero for "whatever the container was started with"; notification
* display is native to this document and defaults to home only. */
export interface GatewaySettings {
timezone: string;
logLevel: string;
sessionIdleDays: number;
sonarrAlertMinutes: number;
radarrAlertMinutes: number;
notificationDisplay: string;
embyHealthSeconds: number;
slowRequestMillis: number;
librarySyncMinutes: number;
@@ -781,6 +782,7 @@ export interface GatewaySettingValues {
sessionIdleDays: number;
sonarrAlertMinutes: number;
radarrAlertMinutes: number;
notificationDisplay: string;
embyHealthSeconds: number;
slowRequestMillis: number;
librarySyncMinutes: number;
@@ -791,6 +793,7 @@ export interface GatewaySettingsResponse {
deployed: GatewaySettingValues;
effective: GatewaySettingValues;
logLevels: string[] | null;
notificationDisplays: string[] | null;
version: string;
}
+40 -13
View File
@@ -13,10 +13,9 @@ import { Banner, Button, Card, Field, KeyValue, Loading, Note, PageHead, Tag } f
* has not been switched on. That is why it is reached from the account menu rather than
* from the rail: it belongs beside "signed in as", not beside the household's content.
*
* The whole page is an amendment to `.env`. Every field is blank by default and blank
* means "whatever this container was started with", which is printed beside it — so an
* operator can always see what they are overriding, and clearing a field is a real undo
* rather than a value they have to remember. */
* Most fields amend `.env`: blank means "whatever this container was started with", which
* is printed beside the field. Notification display is server-native instead and has a
* Home-only default. */
/** OVERRIDE_OFF is the wire value for "switched off", which the three window settings need
* to distinguish from an empty field. See store.GatewaySettingsOff. */
@@ -46,12 +45,21 @@ function describe(value: number, unit: string): string {
return `${value} ${unit}${value === 1 ? '' : 's'}`;
}
function notificationDisplayLabel(value: string): string {
switch (value) {
case 'everywhere': return 'Everywhere';
case 'off': return 'Off';
default: return 'Home only';
}
}
interface Draft {
timezone: string;
logLevel: string;
sessionIdleDays: string;
sonarrAlertMinutes: string;
radarrAlertMinutes: string;
notificationDisplay: string;
embyHealthSeconds: string;
slowRequestMillis: string;
librarySyncMinutes: string;
@@ -64,6 +72,7 @@ function draftFrom(settings: GatewaySettings): Draft {
sessionIdleDays: numberFieldValue(settings.sessionIdleDays),
sonarrAlertMinutes: numberFieldValue(settings.sonarrAlertMinutes),
radarrAlertMinutes: numberFieldValue(settings.radarrAlertMinutes),
notificationDisplay: settings.notificationDisplay || 'home_only',
embyHealthSeconds: numberFieldValue(settings.embyHealthSeconds),
slowRequestMillis: numberFieldValue(settings.slowRequestMillis),
librarySyncMinutes: numberFieldValue(settings.librarySyncMinutes),
@@ -95,6 +104,7 @@ export function SettingsPage() {
sessionIdleDays: parseNumberField(draft.sessionIdleDays, false),
sonarrAlertMinutes: parseNumberField(draft.sonarrAlertMinutes, true),
radarrAlertMinutes: parseNumberField(draft.radarrAlertMinutes, true),
notificationDisplay: draft.notificationDisplay,
embyHealthSeconds: parseNumberField(draft.embyHealthSeconds, true),
slowRequestMillis: parseNumberField(draft.slowRequestMillis, true),
librarySyncMinutes: parseNumberField(draft.librarySyncMinutes, true),
@@ -117,9 +127,9 @@ export function SettingsPage() {
() => api.post<GatewaySettingsResponse>('/admin/api/gateway-settings', {
timezone: '', logLevel: '', sessionIdleDays: 0,
sonarrAlertMinutes: 0, radarrAlertMinutes: 0, embyHealthSeconds: 0, slowRequestMillis: 0,
librarySyncMinutes: 0,
librarySyncMinutes: 0, notificationDisplay: 'home_only',
}),
'Every setting is back to what this container was deployed with.',
'Every setting is back to its deployed or server default.',
);
if (saved) setDraft(draftFrom(saved.settings));
await reload();
@@ -128,6 +138,7 @@ export function SettingsPage() {
const deployed = data?.deployed;
const effective = data?.effective;
const levels = data?.logLevels ?? [];
const notificationDisplays = data?.notificationDisplays ?? ['everywhere', 'home_only', 'off'];
return (
<>
@@ -158,13 +169,14 @@ export function SettingsPage() {
{ label: 'Catalogue sweep', value: describe(effective.librarySyncMinutes, 'minute') },
{ label: 'Episode alert window', value: describe(effective.sonarrAlertMinutes, 'minute') },
{ label: 'Film alert window', value: describe(effective.radarrAlertMinutes, 'minute') },
{ label: 'Notification display', value: notificationDisplayLabel(effective.notificationDisplay) },
]}
/>
</Card>
<Card
title="Overrides"
intro="Leave a field empty to use the value this container was deployed with, shown beneath it. Changes take effect immediately — nothing here needs a restart."
title="Server settings"
intro="Environment-backed fields can be left empty to use the deployed value shown beneath them. Changes take effect immediately — nothing here needs a restart."
icon="sliders"
tone="note"
footer={
@@ -173,7 +185,7 @@ export function SettingsPage() {
Save settings
</Button>
<Button busy={busy === 'clear'} onClick={() => void clearAll()}>
Use deployed values
Use defaults
</Button>
</>
}
@@ -262,6 +274,22 @@ export function SettingsPage() {
</Field>
</div>
<div className="fields">
<Field
label="Notification display"
hint="Controls informational banners for every viewer and television. Everywhere also permits playback overlays; Home only keeps them off active movies, episodes and trailers; Off hides them throughout Memby."
>
<select
value={draft.notificationDisplay}
onChange={(event) => set('notificationDisplay', event.target.value)}
>
{notificationDisplays.map((value) => (
<option key={value} value={value}>{notificationDisplayLabel(value)}</option>
))}
</select>
</Field>
</div>
<div className="fields">
<Field
label="Episode alert window (minutes)"
@@ -291,10 +319,9 @@ export function SettingsPage() {
</div>
<Note tone="note">
These override the deployed configuration in the database, so they survive a
restart but a deployment rewrites <code>.env</code>, not this, and the two can
then disagree. Anything meant to be permanent belongs in <code>.env.example</code>{' '}
as well.
These settings live in the database and survive a restart. A deployment rewrites
<code>.env</code>, not this document, so environment-backed values can then disagree;
permanent environment changes belong in <code>.env.example</code> as well.
</Note>
{data?.settings.updatedBy ? (
<Note>