|
|
|
|
@@ -2,7 +2,7 @@
|
|
|
|
|
|
|
|
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
|
|
|
import { useRouter } from 'next/navigation'
|
|
|
|
|
import { authFetch, clearToken, getApiBase, getToken } from '../lib/auth'
|
|
|
|
|
import { authFetch, clearToken, getApiBase, getToken, getEventStreamToken } from '../lib/auth'
|
|
|
|
|
import AdminShell from '../ui/AdminShell'
|
|
|
|
|
|
|
|
|
|
type AdminSetting = {
|
|
|
|
|
@@ -19,6 +19,7 @@ type ServiceOptions = {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const SECTION_LABELS: Record<string, string> = {
|
|
|
|
|
magent: 'Magent',
|
|
|
|
|
jellyseerr: 'Jellyseerr',
|
|
|
|
|
jellyfin: 'Jellyfin',
|
|
|
|
|
artwork: 'Artwork cache',
|
|
|
|
|
@@ -32,9 +33,34 @@ const SECTION_LABELS: Record<string, string> = {
|
|
|
|
|
site: 'Site',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const BOOL_SETTINGS = new Set(['jellyfin_sync_to_arr', 'site_banner_enabled'])
|
|
|
|
|
const TEXTAREA_SETTINGS = new Set(['site_banner_message', 'site_changelog'])
|
|
|
|
|
const BOOL_SETTINGS = new Set([
|
|
|
|
|
'jellyfin_sync_to_arr',
|
|
|
|
|
'site_banner_enabled',
|
|
|
|
|
'magent_proxy_enabled',
|
|
|
|
|
'magent_proxy_trust_forwarded_headers',
|
|
|
|
|
'magent_ssl_bind_enabled',
|
|
|
|
|
'magent_notify_enabled',
|
|
|
|
|
'magent_notify_email_enabled',
|
|
|
|
|
'magent_notify_email_use_tls',
|
|
|
|
|
'magent_notify_email_use_ssl',
|
|
|
|
|
'magent_notify_discord_enabled',
|
|
|
|
|
'magent_notify_telegram_enabled',
|
|
|
|
|
'magent_notify_push_enabled',
|
|
|
|
|
'magent_notify_webhook_enabled',
|
|
|
|
|
])
|
|
|
|
|
const TEXTAREA_SETTINGS = new Set([
|
|
|
|
|
'site_banner_message',
|
|
|
|
|
'site_changelog',
|
|
|
|
|
'magent_ssl_certificate_pem',
|
|
|
|
|
'magent_ssl_private_key_pem',
|
|
|
|
|
])
|
|
|
|
|
const URL_SETTINGS = new Set([
|
|
|
|
|
'magent_application_url',
|
|
|
|
|
'magent_api_url',
|
|
|
|
|
'magent_proxy_base_url',
|
|
|
|
|
'magent_notify_discord_webhook_url',
|
|
|
|
|
'magent_notify_push_base_url',
|
|
|
|
|
'magent_notify_webhook_url',
|
|
|
|
|
'jellyseerr_base_url',
|
|
|
|
|
'jellyfin_base_url',
|
|
|
|
|
'jellyfin_public_url',
|
|
|
|
|
@@ -43,9 +69,20 @@ const URL_SETTINGS = new Set([
|
|
|
|
|
'prowlarr_base_url',
|
|
|
|
|
'qbittorrent_base_url',
|
|
|
|
|
])
|
|
|
|
|
const NUMBER_SETTINGS = new Set([
|
|
|
|
|
'magent_application_port',
|
|
|
|
|
'magent_api_port',
|
|
|
|
|
'magent_notify_email_smtp_port',
|
|
|
|
|
'requests_sync_ttl_minutes',
|
|
|
|
|
'requests_poll_interval_seconds',
|
|
|
|
|
'requests_delta_sync_interval_minutes',
|
|
|
|
|
'requests_cleanup_days',
|
|
|
|
|
])
|
|
|
|
|
const BANNER_TONES = ['info', 'warning', 'error', 'maintenance']
|
|
|
|
|
|
|
|
|
|
const SECTION_DESCRIPTIONS: Record<string, string> = {
|
|
|
|
|
magent:
|
|
|
|
|
'Application-level Magent settings including proxy, binding, TLS, and notification channels.',
|
|
|
|
|
jellyseerr: 'Connect the request system where users submit content.',
|
|
|
|
|
jellyfin: 'Control Jellyfin login and availability checks.',
|
|
|
|
|
artwork: 'Cache posters/backdrops and review artwork coverage.',
|
|
|
|
|
@@ -60,6 +97,7 @@ const SECTION_DESCRIPTIONS: Record<string, string> = {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const SETTINGS_SECTION_MAP: Record<string, string | null> = {
|
|
|
|
|
magent: 'magent',
|
|
|
|
|
jellyseerr: 'jellyseerr',
|
|
|
|
|
jellyfin: 'jellyfin',
|
|
|
|
|
artwork: null,
|
|
|
|
|
@@ -74,7 +112,151 @@ const SETTINGS_SECTION_MAP: Record<string, string | null> = {
|
|
|
|
|
site: 'site',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const MAGENT_SECTION_GROUPS: Array<{
|
|
|
|
|
key: string
|
|
|
|
|
title: string
|
|
|
|
|
description: string
|
|
|
|
|
keys: string[]
|
|
|
|
|
}> = [
|
|
|
|
|
{
|
|
|
|
|
key: 'magent-runtime',
|
|
|
|
|
title: 'Application',
|
|
|
|
|
description:
|
|
|
|
|
'Canonical application/API URLs and port defaults for the Magent UI/API endpoints.',
|
|
|
|
|
keys: [
|
|
|
|
|
'magent_application_url',
|
|
|
|
|
'magent_application_port',
|
|
|
|
|
'magent_api_url',
|
|
|
|
|
'magent_api_port',
|
|
|
|
|
'magent_bind_host',
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'magent-proxy',
|
|
|
|
|
title: 'Proxy',
|
|
|
|
|
description:
|
|
|
|
|
'Reverse proxy awareness and base URL handling when Magent sits behind Caddy/NGINX/Traefik.',
|
|
|
|
|
keys: [
|
|
|
|
|
'magent_proxy_enabled',
|
|
|
|
|
'magent_proxy_base_url',
|
|
|
|
|
'magent_proxy_trust_forwarded_headers',
|
|
|
|
|
'magent_proxy_forwarded_prefix',
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'magent-ssl',
|
|
|
|
|
title: 'Manual SSL Bind',
|
|
|
|
|
description:
|
|
|
|
|
'Optional direct TLS binding values. Paste PEM certificate and private key or provide file paths.',
|
|
|
|
|
keys: [
|
|
|
|
|
'magent_ssl_bind_enabled',
|
|
|
|
|
'magent_ssl_certificate_path',
|
|
|
|
|
'magent_ssl_private_key_path',
|
|
|
|
|
'magent_ssl_certificate_pem',
|
|
|
|
|
'magent_ssl_private_key_pem',
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'magent-notify-core',
|
|
|
|
|
title: 'Notifications',
|
|
|
|
|
description:
|
|
|
|
|
'Global notification controls and provider-independent defaults used by Magent messaging features.',
|
|
|
|
|
keys: ['magent_notify_enabled'],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'magent-notify-email',
|
|
|
|
|
title: 'Email',
|
|
|
|
|
description: 'SMTP configuration for email notifications.',
|
|
|
|
|
keys: [
|
|
|
|
|
'magent_notify_email_enabled',
|
|
|
|
|
'magent_notify_email_smtp_host',
|
|
|
|
|
'magent_notify_email_smtp_port',
|
|
|
|
|
'magent_notify_email_smtp_username',
|
|
|
|
|
'magent_notify_email_smtp_password',
|
|
|
|
|
'magent_notify_email_from_address',
|
|
|
|
|
'magent_notify_email_from_name',
|
|
|
|
|
'magent_notify_email_use_tls',
|
|
|
|
|
'magent_notify_email_use_ssl',
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'magent-notify-discord',
|
|
|
|
|
title: 'Discord',
|
|
|
|
|
description: 'Webhook settings for Discord notifications and feedback routing.',
|
|
|
|
|
keys: ['magent_notify_discord_enabled', 'magent_notify_discord_webhook_url'],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'magent-notify-telegram',
|
|
|
|
|
title: 'Telegram',
|
|
|
|
|
description: 'Bot token and chat target for Telegram notifications.',
|
|
|
|
|
keys: [
|
|
|
|
|
'magent_notify_telegram_enabled',
|
|
|
|
|
'magent_notify_telegram_bot_token',
|
|
|
|
|
'magent_notify_telegram_chat_id',
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'magent-notify-push',
|
|
|
|
|
title: 'Push / Mobile',
|
|
|
|
|
description:
|
|
|
|
|
'Generic push messaging configuration (ntfy, Gotify, Pushover, webhook-style push endpoints).',
|
|
|
|
|
keys: [
|
|
|
|
|
'magent_notify_push_enabled',
|
|
|
|
|
'magent_notify_push_provider',
|
|
|
|
|
'magent_notify_push_base_url',
|
|
|
|
|
'magent_notify_push_topic',
|
|
|
|
|
'magent_notify_push_token',
|
|
|
|
|
'magent_notify_push_user_key',
|
|
|
|
|
'magent_notify_push_device',
|
|
|
|
|
'magent_notify_webhook_enabled',
|
|
|
|
|
'magent_notify_webhook_url',
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const SETTING_LABEL_OVERRIDES: Record<string, string> = {
|
|
|
|
|
magent_application_url: 'Application URL',
|
|
|
|
|
magent_application_port: 'Application port',
|
|
|
|
|
magent_api_url: 'API URL',
|
|
|
|
|
magent_api_port: 'API port',
|
|
|
|
|
magent_bind_host: 'Bind host',
|
|
|
|
|
magent_proxy_enabled: 'Proxy support enabled',
|
|
|
|
|
magent_proxy_base_url: 'Proxy base URL',
|
|
|
|
|
magent_proxy_trust_forwarded_headers: 'Trust forwarded headers',
|
|
|
|
|
magent_proxy_forwarded_prefix: 'Forwarded path prefix',
|
|
|
|
|
magent_ssl_bind_enabled: 'Manual SSL bind enabled',
|
|
|
|
|
magent_ssl_certificate_path: 'Certificate path',
|
|
|
|
|
magent_ssl_private_key_path: 'Private key path',
|
|
|
|
|
magent_ssl_certificate_pem: 'Certificate (PEM)',
|
|
|
|
|
magent_ssl_private_key_pem: 'Private key (PEM)',
|
|
|
|
|
magent_notify_enabled: 'Notifications enabled',
|
|
|
|
|
magent_notify_email_enabled: 'Email notifications enabled',
|
|
|
|
|
magent_notify_email_smtp_host: 'SMTP host',
|
|
|
|
|
magent_notify_email_smtp_port: 'SMTP port',
|
|
|
|
|
magent_notify_email_smtp_username: 'SMTP username',
|
|
|
|
|
magent_notify_email_smtp_password: 'SMTP password',
|
|
|
|
|
magent_notify_email_from_address: 'From email address',
|
|
|
|
|
magent_notify_email_from_name: 'From display name',
|
|
|
|
|
magent_notify_email_use_tls: 'Use STARTTLS',
|
|
|
|
|
magent_notify_email_use_ssl: 'Use SSL/TLS (implicit)',
|
|
|
|
|
magent_notify_discord_enabled: 'Discord notifications enabled',
|
|
|
|
|
magent_notify_discord_webhook_url: 'Discord webhook URL',
|
|
|
|
|
magent_notify_telegram_enabled: 'Telegram notifications enabled',
|
|
|
|
|
magent_notify_telegram_bot_token: 'Telegram bot token',
|
|
|
|
|
magent_notify_telegram_chat_id: 'Telegram chat ID',
|
|
|
|
|
magent_notify_push_enabled: 'Push notifications enabled',
|
|
|
|
|
magent_notify_push_provider: 'Push provider',
|
|
|
|
|
magent_notify_push_base_url: 'Push provider/base URL',
|
|
|
|
|
magent_notify_push_topic: 'Topic / channel',
|
|
|
|
|
magent_notify_push_token: 'API token / password',
|
|
|
|
|
magent_notify_push_user_key: 'User key / recipient key',
|
|
|
|
|
magent_notify_push_device: 'Device / target',
|
|
|
|
|
magent_notify_webhook_enabled: 'Generic webhook notifications enabled',
|
|
|
|
|
magent_notify_webhook_url: 'Generic webhook URL',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const labelFromKey = (key: string) =>
|
|
|
|
|
SETTING_LABEL_OVERRIDES[key] ??
|
|
|
|
|
key
|
|
|
|
|
.replaceAll('_', ' ')
|
|
|
|
|
.replace('base url', 'URL')
|
|
|
|
|
@@ -115,6 +297,13 @@ type SettingsPageProps = {
|
|
|
|
|
section: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type SettingsSectionGroup = {
|
|
|
|
|
key: string
|
|
|
|
|
title: string
|
|
|
|
|
items: AdminSetting[]
|
|
|
|
|
description?: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function SettingsPage({ section }: SettingsPageProps) {
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
const [settings, setSettings] = useState<AdminSetting[]>([])
|
|
|
|
|
@@ -308,26 +497,56 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
|
|
|
|
}
|
|
|
|
|
const cacheSettings = settings.filter((setting) => cacheSettingKeys.has(setting.key))
|
|
|
|
|
const artworkSettings = settings.filter((setting) => artworkSettingKeys.has(setting.key))
|
|
|
|
|
const settingsSections = isCacheSection
|
|
|
|
|
const settingsSections: SettingsSectionGroup[] = isCacheSection
|
|
|
|
|
? [
|
|
|
|
|
{ key: 'cache', title: 'Cache control', items: cacheSettings },
|
|
|
|
|
{ key: 'artwork', title: 'Artwork cache', items: artworkSettings },
|
|
|
|
|
]
|
|
|
|
|
: visibleSections.map((sectionKey) => ({
|
|
|
|
|
key: sectionKey,
|
|
|
|
|
title: SECTION_LABELS[sectionKey] ?? sectionKey,
|
|
|
|
|
items: (() => {
|
|
|
|
|
const sectionItems = groupedSettings[sectionKey] ?? []
|
|
|
|
|
const filtered =
|
|
|
|
|
sectionKey === 'requests' || sectionKey === 'artwork'
|
|
|
|
|
? sectionItems.filter((setting) => !hiddenSettingKeys.has(setting.key))
|
|
|
|
|
: sectionItems
|
|
|
|
|
if (sectionKey === 'requests') {
|
|
|
|
|
return sortByOrder(filtered, requestSettingOrder)
|
|
|
|
|
: section === 'magent'
|
|
|
|
|
? (() => {
|
|
|
|
|
const magentItems = groupedSettings.magent ?? []
|
|
|
|
|
const byKey = new Map(magentItems.map((item) => [item.key, item]))
|
|
|
|
|
const used = new Set<string>()
|
|
|
|
|
const groups: SettingsSectionGroup[] = MAGENT_SECTION_GROUPS.map((group) => {
|
|
|
|
|
const items = group.keys
|
|
|
|
|
.map((key) => byKey.get(key))
|
|
|
|
|
.filter((item): item is AdminSetting => Boolean(item))
|
|
|
|
|
for (const item of items) {
|
|
|
|
|
used.add(item.key)
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
key: group.key,
|
|
|
|
|
title: group.title,
|
|
|
|
|
description: group.description,
|
|
|
|
|
items,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
const remaining = magentItems.filter((item) => !used.has(item.key))
|
|
|
|
|
if (remaining.length) {
|
|
|
|
|
groups.push({
|
|
|
|
|
key: 'magent-other',
|
|
|
|
|
title: 'Additional Magent settings',
|
|
|
|
|
description: 'Uncategorized Magent settings.',
|
|
|
|
|
items: remaining,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
return filtered
|
|
|
|
|
})(),
|
|
|
|
|
}))
|
|
|
|
|
return groups
|
|
|
|
|
})()
|
|
|
|
|
: visibleSections.map((sectionKey) => ({
|
|
|
|
|
key: sectionKey,
|
|
|
|
|
title: SECTION_LABELS[sectionKey] ?? sectionKey,
|
|
|
|
|
items: (() => {
|
|
|
|
|
const sectionItems = groupedSettings[sectionKey] ?? []
|
|
|
|
|
const filtered =
|
|
|
|
|
sectionKey === 'requests' || sectionKey === 'artwork'
|
|
|
|
|
? sectionItems.filter((setting) => !hiddenSettingKeys.has(setting.key))
|
|
|
|
|
: sectionItems
|
|
|
|
|
if (sectionKey === 'requests') {
|
|
|
|
|
return sortByOrder(filtered, requestSettingOrder)
|
|
|
|
|
}
|
|
|
|
|
return filtered
|
|
|
|
|
})(),
|
|
|
|
|
}))
|
|
|
|
|
const showLogs = section === 'logs'
|
|
|
|
|
const showMaintenance = section === 'maintenance'
|
|
|
|
|
const showRequestsExtras = section === 'requests'
|
|
|
|
|
@@ -350,6 +569,65 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
|
|
|
|
}, [artworkPrefetch])
|
|
|
|
|
|
|
|
|
|
const settingDescriptions: Record<string, string> = {
|
|
|
|
|
magent_application_url:
|
|
|
|
|
'Canonical public URL for the Magent web app (used for links and reverse-proxy-aware features).',
|
|
|
|
|
magent_application_port:
|
|
|
|
|
'Preferred frontend/UI port for local or direct-hosted deployments.',
|
|
|
|
|
magent_api_url:
|
|
|
|
|
'Canonical public URL for the Magent API when it differs from the app URL.',
|
|
|
|
|
magent_api_port: 'Preferred API port for local or direct-hosted deployments.',
|
|
|
|
|
magent_bind_host:
|
|
|
|
|
'Host/IP to bind the application services to when running without an external process manager.',
|
|
|
|
|
magent_proxy_enabled:
|
|
|
|
|
'Enable reverse-proxy-aware behavior and use proxy-specific URL settings.',
|
|
|
|
|
magent_proxy_base_url:
|
|
|
|
|
'Base URL Magent should use when it is published behind a proxy path or external proxy hostname.',
|
|
|
|
|
magent_proxy_trust_forwarded_headers:
|
|
|
|
|
'Trust X-Forwarded-* headers from your reverse proxy.',
|
|
|
|
|
magent_proxy_forwarded_prefix:
|
|
|
|
|
'Optional path prefix added by your proxy (example: /magent).',
|
|
|
|
|
magent_ssl_bind_enabled:
|
|
|
|
|
'Enable direct HTTPS binding in Magent (for environments not terminating TLS at a proxy).',
|
|
|
|
|
magent_ssl_certificate_path:
|
|
|
|
|
'Path to the TLS certificate file on disk (PEM).',
|
|
|
|
|
magent_ssl_private_key_path:
|
|
|
|
|
'Path to the TLS private key file on disk (PEM).',
|
|
|
|
|
magent_ssl_certificate_pem:
|
|
|
|
|
'Paste the TLS certificate PEM if you want Magent to store it directly.',
|
|
|
|
|
magent_ssl_private_key_pem:
|
|
|
|
|
'Paste the TLS private key PEM if you want Magent to store it directly.',
|
|
|
|
|
magent_notify_enabled:
|
|
|
|
|
'Master switch for Magent notifications. Individual provider toggles still apply.',
|
|
|
|
|
magent_notify_email_enabled: 'Enable SMTP email notifications.',
|
|
|
|
|
magent_notify_email_smtp_host: 'SMTP server hostname or IP.',
|
|
|
|
|
magent_notify_email_smtp_port: 'SMTP port (587 for STARTTLS, 465 for SSL).',
|
|
|
|
|
magent_notify_email_smtp_username: 'SMTP account username.',
|
|
|
|
|
magent_notify_email_smtp_password: 'SMTP account password or app password.',
|
|
|
|
|
magent_notify_email_from_address: 'Sender email address used by Magent.',
|
|
|
|
|
magent_notify_email_from_name: 'Sender display name shown to recipients.',
|
|
|
|
|
magent_notify_email_use_tls: 'Use STARTTLS after connecting to SMTP.',
|
|
|
|
|
magent_notify_email_use_ssl: 'Use implicit TLS/SSL for SMTP (usually port 465).',
|
|
|
|
|
magent_notify_discord_enabled: 'Enable Discord webhook notifications.',
|
|
|
|
|
magent_notify_discord_webhook_url:
|
|
|
|
|
'Discord channel webhook URL used for notifications and optional feedback routing.',
|
|
|
|
|
magent_notify_telegram_enabled: 'Enable Telegram notifications.',
|
|
|
|
|
magent_notify_telegram_bot_token: 'Bot token from BotFather.',
|
|
|
|
|
magent_notify_telegram_chat_id:
|
|
|
|
|
'Default Telegram chat/group/user ID for notifications.',
|
|
|
|
|
magent_notify_push_enabled: 'Enable generic push notifications.',
|
|
|
|
|
magent_notify_push_provider:
|
|
|
|
|
'Push backend to target (ntfy, gotify, pushover, webhook, etc.).',
|
|
|
|
|
magent_notify_push_base_url:
|
|
|
|
|
'Base URL for your push provider (for example ntfy/gotify server URL).',
|
|
|
|
|
magent_notify_push_topic: 'Topic/channel/room name used by the push provider.',
|
|
|
|
|
magent_notify_push_token: 'Provider token/API key/password.',
|
|
|
|
|
magent_notify_push_user_key:
|
|
|
|
|
'Provider recipient key/user key (for example Pushover user key).',
|
|
|
|
|
magent_notify_push_device:
|
|
|
|
|
'Optional device or target override, depending on provider.',
|
|
|
|
|
magent_notify_webhook_enabled: 'Enable generic webhook notifications.',
|
|
|
|
|
magent_notify_webhook_url:
|
|
|
|
|
'Generic webhook endpoint for custom integrations or automation flows.',
|
|
|
|
|
jellyseerr_base_url:
|
|
|
|
|
'Base URL for your Jellyseerr server (FQDN or IP). Scheme is optional.',
|
|
|
|
|
jellyseerr_api_key: 'API key used to read requests and status.',
|
|
|
|
|
@@ -397,6 +675,29 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const settingPlaceholders: Record<string, string> = {
|
|
|
|
|
magent_application_url: 'https://magent.example.com',
|
|
|
|
|
magent_application_port: '3000',
|
|
|
|
|
magent_api_url: 'https://api.example.com or https://magent.example.com/api',
|
|
|
|
|
magent_api_port: '8000',
|
|
|
|
|
magent_bind_host: '0.0.0.0',
|
|
|
|
|
magent_proxy_base_url: 'https://proxy.example.com/magent',
|
|
|
|
|
magent_proxy_forwarded_prefix: '/magent',
|
|
|
|
|
magent_ssl_certificate_path: '/certs/fullchain.pem',
|
|
|
|
|
magent_ssl_private_key_path: '/certs/privkey.pem',
|
|
|
|
|
magent_ssl_certificate_pem: '-----BEGIN CERTIFICATE-----',
|
|
|
|
|
magent_ssl_private_key_pem: '-----BEGIN PRIVATE KEY-----',
|
|
|
|
|
magent_notify_email_smtp_host: 'smtp.office365.com',
|
|
|
|
|
magent_notify_email_smtp_port: '587',
|
|
|
|
|
magent_notify_email_smtp_username: 'notifications@example.com',
|
|
|
|
|
magent_notify_email_from_address: 'notifications@example.com',
|
|
|
|
|
magent_notify_email_from_name: 'Magent',
|
|
|
|
|
magent_notify_discord_webhook_url: 'https://discord.com/api/webhooks/...',
|
|
|
|
|
magent_notify_telegram_bot_token: '123456789:AA...',
|
|
|
|
|
magent_notify_telegram_chat_id: '-1001234567890',
|
|
|
|
|
magent_notify_push_base_url: 'https://ntfy.example.com or https://gotify.example.com',
|
|
|
|
|
magent_notify_push_topic: 'magent-alerts',
|
|
|
|
|
magent_notify_push_device: 'iphone-zak',
|
|
|
|
|
magent_notify_webhook_url: 'https://automation.example.com/webhooks/magent',
|
|
|
|
|
jellyseerr_base_url: 'https://requests.example.com or 10.30.1.81:5055',
|
|
|
|
|
jellyfin_base_url: 'https://jelly.example.com or 10.40.0.80:8096',
|
|
|
|
|
jellyfin_public_url: 'https://jelly.example.com',
|
|
|
|
|
@@ -599,83 +900,101 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const baseUrl = getApiBase()
|
|
|
|
|
const params = new URLSearchParams()
|
|
|
|
|
params.set('access_token', token)
|
|
|
|
|
if (showLogs) {
|
|
|
|
|
params.set('include_logs', '1')
|
|
|
|
|
params.set('log_lines', String(logsCount))
|
|
|
|
|
}
|
|
|
|
|
const streamUrl = `${baseUrl}/admin/events/stream?${params.toString()}`
|
|
|
|
|
let closed = false
|
|
|
|
|
const source = new EventSource(streamUrl)
|
|
|
|
|
let source: EventSource | null = null
|
|
|
|
|
|
|
|
|
|
source.onopen = () => {
|
|
|
|
|
if (closed) return
|
|
|
|
|
setLiveStreamConnected(true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
source.onmessage = (event) => {
|
|
|
|
|
if (closed) return
|
|
|
|
|
setLiveStreamConnected(true)
|
|
|
|
|
const connect = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const payload = JSON.parse(event.data)
|
|
|
|
|
if (!payload || payload.type !== 'admin_live_state') {
|
|
|
|
|
return
|
|
|
|
|
const streamToken = await getEventStreamToken()
|
|
|
|
|
if (closed) return
|
|
|
|
|
const params = new URLSearchParams()
|
|
|
|
|
params.set('stream_token', streamToken)
|
|
|
|
|
if (showLogs) {
|
|
|
|
|
params.set('include_logs', '1')
|
|
|
|
|
params.set('log_lines', String(logsCount))
|
|
|
|
|
}
|
|
|
|
|
const streamUrl = `${baseUrl}/admin/events/stream?${params.toString()}`
|
|
|
|
|
source = new EventSource(streamUrl)
|
|
|
|
|
|
|
|
|
|
source.onopen = () => {
|
|
|
|
|
if (closed) return
|
|
|
|
|
setLiveStreamConnected(true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rawSync =
|
|
|
|
|
payload.requestsSync && typeof payload.requestsSync === 'object'
|
|
|
|
|
? payload.requestsSync
|
|
|
|
|
: null
|
|
|
|
|
const nextSync = rawSync?.status === 'idle' ? null : rawSync
|
|
|
|
|
const prevSync = requestsSyncRef.current
|
|
|
|
|
requestsSyncRef.current = nextSync
|
|
|
|
|
setRequestsSync(nextSync)
|
|
|
|
|
if (prevSync?.status === 'running' && nextSync?.status && nextSync.status !== 'running') {
|
|
|
|
|
setRequestsSyncStatus(nextSync.message || 'Sync complete.')
|
|
|
|
|
}
|
|
|
|
|
source.onmessage = (event) => {
|
|
|
|
|
if (closed) return
|
|
|
|
|
setLiveStreamConnected(true)
|
|
|
|
|
try {
|
|
|
|
|
const payload = JSON.parse(event.data)
|
|
|
|
|
if (!payload || payload.type !== 'admin_live_state') {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rawArtwork =
|
|
|
|
|
payload.artworkPrefetch && typeof payload.artworkPrefetch === 'object'
|
|
|
|
|
? payload.artworkPrefetch
|
|
|
|
|
: null
|
|
|
|
|
const nextArtwork = rawArtwork?.status === 'idle' ? null : rawArtwork
|
|
|
|
|
const prevArtwork = artworkPrefetchRef.current
|
|
|
|
|
artworkPrefetchRef.current = nextArtwork
|
|
|
|
|
setArtworkPrefetch(nextArtwork)
|
|
|
|
|
if (
|
|
|
|
|
prevArtwork?.status === 'running' &&
|
|
|
|
|
nextArtwork?.status &&
|
|
|
|
|
nextArtwork.status !== 'running'
|
|
|
|
|
) {
|
|
|
|
|
setArtworkPrefetchStatus(nextArtwork.message || 'Artwork caching complete.')
|
|
|
|
|
if (showArtworkExtras) {
|
|
|
|
|
void loadArtworkSummary()
|
|
|
|
|
const rawSync =
|
|
|
|
|
payload.requestsSync && typeof payload.requestsSync === 'object'
|
|
|
|
|
? payload.requestsSync
|
|
|
|
|
: null
|
|
|
|
|
const nextSync = rawSync?.status === 'idle' ? null : rawSync
|
|
|
|
|
const prevSync = requestsSyncRef.current
|
|
|
|
|
requestsSyncRef.current = nextSync
|
|
|
|
|
setRequestsSync(nextSync)
|
|
|
|
|
if (
|
|
|
|
|
prevSync?.status === 'running' &&
|
|
|
|
|
nextSync?.status &&
|
|
|
|
|
nextSync.status !== 'running'
|
|
|
|
|
) {
|
|
|
|
|
setRequestsSyncStatus(nextSync.message || 'Sync complete.')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rawArtwork =
|
|
|
|
|
payload.artworkPrefetch && typeof payload.artworkPrefetch === 'object'
|
|
|
|
|
? payload.artworkPrefetch
|
|
|
|
|
: null
|
|
|
|
|
const nextArtwork = rawArtwork?.status === 'idle' ? null : rawArtwork
|
|
|
|
|
const prevArtwork = artworkPrefetchRef.current
|
|
|
|
|
artworkPrefetchRef.current = nextArtwork
|
|
|
|
|
setArtworkPrefetch(nextArtwork)
|
|
|
|
|
if (
|
|
|
|
|
prevArtwork?.status === 'running' &&
|
|
|
|
|
nextArtwork?.status &&
|
|
|
|
|
nextArtwork.status !== 'running'
|
|
|
|
|
) {
|
|
|
|
|
setArtworkPrefetchStatus(nextArtwork.message || 'Artwork caching complete.')
|
|
|
|
|
if (showArtworkExtras) {
|
|
|
|
|
void loadArtworkSummary()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (payload.logs && typeof payload.logs === 'object') {
|
|
|
|
|
if (Array.isArray(payload.logs.lines)) {
|
|
|
|
|
setLogsLines(payload.logs.lines)
|
|
|
|
|
setLogsStatus(null)
|
|
|
|
|
} else if (typeof payload.logs.error === 'string' && payload.logs.error.trim()) {
|
|
|
|
|
setLogsStatus(payload.logs.error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (payload.logs && typeof payload.logs === 'object') {
|
|
|
|
|
if (Array.isArray(payload.logs.lines)) {
|
|
|
|
|
setLogsLines(payload.logs.lines)
|
|
|
|
|
setLogsStatus(null)
|
|
|
|
|
} else if (typeof payload.logs.error === 'string' && payload.logs.error.trim()) {
|
|
|
|
|
setLogsStatus(payload.logs.error)
|
|
|
|
|
}
|
|
|
|
|
source.onerror = () => {
|
|
|
|
|
if (closed) return
|
|
|
|
|
setLiveStreamConnected(false)
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (closed) return
|
|
|
|
|
console.error(err)
|
|
|
|
|
setLiveStreamConnected(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
source.onerror = () => {
|
|
|
|
|
if (closed) return
|
|
|
|
|
setLiveStreamConnected(false)
|
|
|
|
|
}
|
|
|
|
|
void connect()
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
closed = true
|
|
|
|
|
setLiveStreamConnected(false)
|
|
|
|
|
source.close()
|
|
|
|
|
source?.close()
|
|
|
|
|
}
|
|
|
|
|
}, [loadArtworkSummary, logsCount, showArtworkExtras, showLogs, showRequestsExtras])
|
|
|
|
|
|
|
|
|
|
@@ -1000,8 +1319,17 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
{SECTION_DESCRIPTIONS[sectionGroup.key] && !settingsSection && (
|
|
|
|
|
<p className="section-subtitle">{SECTION_DESCRIPTIONS[sectionGroup.key]}</p>
|
|
|
|
|
{(sectionGroup.description || SECTION_DESCRIPTIONS[sectionGroup.key]) &&
|
|
|
|
|
(!settingsSection || section === 'magent') && (
|
|
|
|
|
<p className="section-subtitle">
|
|
|
|
|
{sectionGroup.description || SECTION_DESCRIPTIONS[sectionGroup.key]}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
{section === 'magent' && sectionGroup.key === 'magent-runtime' && (
|
|
|
|
|
<div className="status-banner">
|
|
|
|
|
Runtime host/port and SSL values are configuration settings. Container/process
|
|
|
|
|
restarts may still be required before bind/port changes take effect.
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{sectionGroup.key === 'sonarr' && sonarrError && (
|
|
|
|
|
<div className="error-banner">{sonarrError}</div>
|
|
|
|
|
@@ -1339,6 +1667,35 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
|
|
|
|
</label>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
if (setting.key === 'magent_notify_push_provider') {
|
|
|
|
|
return (
|
|
|
|
|
<label key={setting.key} data-helper={helperText || undefined}>
|
|
|
|
|
<span className="label-row">
|
|
|
|
|
<span>{labelFromKey(setting.key)}</span>
|
|
|
|
|
<span className="meta">
|
|
|
|
|
{setting.isSet ? `Source: ${setting.source}` : 'Not set'}
|
|
|
|
|
</span>
|
|
|
|
|
</span>
|
|
|
|
|
<select
|
|
|
|
|
name={setting.key}
|
|
|
|
|
value={value || 'ntfy'}
|
|
|
|
|
onChange={(event) =>
|
|
|
|
|
setFormValues((current) => ({
|
|
|
|
|
...current,
|
|
|
|
|
[setting.key]: event.target.value,
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<option value="ntfy">ntfy</option>
|
|
|
|
|
<option value="gotify">Gotify</option>
|
|
|
|
|
<option value="pushover">Pushover</option>
|
|
|
|
|
<option value="webhook">Webhook</option>
|
|
|
|
|
<option value="telegram">Telegram relay</option>
|
|
|
|
|
<option value="discord">Discord relay</option>
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
setting.key === 'requests_full_sync_time' ||
|
|
|
|
|
setting.key === 'requests_cleanup_time'
|
|
|
|
|
@@ -1365,10 +1722,7 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
|
|
|
|
</label>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
setting.key === 'requests_delta_sync_interval_minutes' ||
|
|
|
|
|
setting.key === 'requests_cleanup_days'
|
|
|
|
|
) {
|
|
|
|
|
if (NUMBER_SETTINGS.has(setting.key)) {
|
|
|
|
|
return (
|
|
|
|
|
<label key={setting.key} data-helper={helperText || undefined}>
|
|
|
|
|
<span className="label-row">
|
|
|
|
|
@@ -1381,6 +1735,7 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
|
|
|
|
name={setting.key}
|
|
|
|
|
type="number"
|
|
|
|
|
min={1}
|
|
|
|
|
step={1}
|
|
|
|
|
value={value}
|
|
|
|
|
onChange={(event) =>
|
|
|
|
|
setFormValues((current) => ({
|
|
|
|
|
@@ -1420,8 +1775,15 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
if (TEXTAREA_SETTINGS.has(setting.key)) {
|
|
|
|
|
const isPemField =
|
|
|
|
|
setting.key === 'magent_ssl_certificate_pem' ||
|
|
|
|
|
setting.key === 'magent_ssl_private_key_pem'
|
|
|
|
|
return (
|
|
|
|
|
<label key={setting.key} data-helper={helperText || undefined}>
|
|
|
|
|
<label
|
|
|
|
|
key={setting.key}
|
|
|
|
|
data-helper={helperText || undefined}
|
|
|
|
|
className={isPemField ? 'field-span-full' : undefined}
|
|
|
|
|
>
|
|
|
|
|
<span className="label-row">
|
|
|
|
|
<span>{labelFromKey(setting.key)}</span>
|
|
|
|
|
<span className="meta">
|
|
|
|
|
@@ -1431,11 +1793,11 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
|
|
|
|
</span>
|
|
|
|
|
<textarea
|
|
|
|
|
name={setting.key}
|
|
|
|
|
rows={setting.key === 'site_changelog' ? 6 : 3}
|
|
|
|
|
rows={setting.key === 'site_changelog' ? 6 : isPemField ? 8 : 3}
|
|
|
|
|
placeholder={
|
|
|
|
|
setting.key === 'site_changelog'
|
|
|
|
|
? 'One update per line.'
|
|
|
|
|
: ''
|
|
|
|
|
: settingPlaceholders[setting.key] ?? ''
|
|
|
|
|
}
|
|
|
|
|
value={value}
|
|
|
|
|
onChange={(event) =>
|
|
|
|
|
|