Build 2602262030: add magent settings and hardening
This commit is contained in:
@@ -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) =>
|
||||
|
||||
Reference in New Issue
Block a user