From da732c7eae2fe07b259d7090893282a90723a538 Mon Sep 17 00:00:00 2001
From: Lee Kelly
Date: Mon, 17 Aug 2026 02:24:44 +0000
Subject: [PATCH 1/2] refactor(settings): align settings copy and status
feedback
- Standardize settings labels and helper text
- Add inline save and integration test statuses
- Remove obsolete sponsor section
---
.../SettingsMetadataSponsorSection.jsx | 28 ----
frontend/src/index.css | 53 ++-----
frontend/src/pages/ProfilePage.jsx | 9 +-
frontend/src/pages/Settings/SettingsPage.jsx | 29 ++--
.../components/CommunityGuideModal.jsx | 9 +-
.../components/LidarrSettingsModalContent.jsx | 60 ++++----
.../Settings/components/PathMappingModal.jsx | 6 +-
.../components/PlexSelfLinkSection.jsx | 5 +-
.../components/QualityProfileModal.jsx | 2 +-
.../components/SettingsAccountTab.jsx | 39 ++---
.../components/SettingsConnectTab.jsx | 27 ++--
.../components/SettingsDiscoverTab.jsx | 33 ++---
.../SettingsDownloadClientsSection.jsx | 94 ++++++------
.../components/SettingsIndexersSection.jsx | 46 +++---
.../components/SettingsIntegrationCards.jsx | 40 +++---
.../components/SettingsMetadataTab.jsx | 2 +-
.../components/SettingsPlaybackSection.jsx | 55 +++----
.../components/SettingsPlaybackTab.jsx | 4 +-
.../components/SettingsRssNewsTab.jsx | 34 ++---
.../components/SettingsStorageSection.jsx | 61 ++++----
.../Settings/components/SettingsTasksTab.jsx | 8 +-
.../Settings/components/SettingsUsersTab.jsx | 6 +-
.../components/arr/SettingsArrLayout.jsx | 12 +-
frontend/src/pages/Settings/settingsArr.css | 134 ++++++++++++++++--
.../src/pages/Settings/settingsTabsConfig.js | 36 ++---
25 files changed, 450 insertions(+), 382 deletions(-)
delete mode 100644 frontend/src/components/SettingsMetadataSponsorSection.jsx
diff --git a/frontend/src/components/SettingsMetadataSponsorSection.jsx b/frontend/src/components/SettingsMetadataSponsorSection.jsx
deleted file mode 100644
index ed959c45b..000000000
--- a/frontend/src/components/SettingsMetadataSponsorSection.jsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { ExternalLink, Heart } from "lucide-react";
-
-function SettingsMetadataSponsorSection() {
- return (
-
-
-
Support hosted services
-
-
-
- Aurral's metadata and search backends currently run on personal infrastructure and may
- experience occasional downtime. Sponsorship goes directly toward maintaining Aurral, keeping
- those services online, and funding cloud capacity when needed.
-
-
-
- Become a sponsor
-
-
- );
-}
-
-export default SettingsMetadataSponsorSection;
diff --git a/frontend/src/index.css b/frontend/src/index.css
index fc75a3b30..d15db93dc 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -9145,16 +9145,6 @@ button.native-library-genre-row:focus-visible {
line-height: 1.5;
}
-.settings-page__modal-reminder {
- margin: 0;
- padding: 0.5rem 0.75rem;
- color: var(--aurral-text-muted);
- font-size: 0.75rem;
- line-height: 1.45;
- background: color-mix(in srgb, var(--aurral-text) 4%, transparent);
- border-radius: var(--aurral-radius-sm);
-}
-
.settings-page__advanced-toggle-row {
display: flex;
justify-content: flex-start;
@@ -9605,34 +9595,6 @@ button.native-library-genre-row:focus-visible {
height: 0.9rem;
}
-.settings-page__section--sponsor {
- padding: 1rem 1.125rem;
- margin-bottom: 2rem;
- border-radius: var(--aurral-radius-sm);
- background: var(--aurral-surface-raised);
-}
-
-.settings-page__section--sponsor .settings-page__section-header {
- margin-bottom: 0.5rem;
-}
-
-.settings-page__sponsor-icon {
- width: 1rem;
- height: 1rem;
- color: var(--aurral-text-muted);
-}
-
-.settings-page__sponsor-copy {
- margin: 0 0 0.875rem;
- color: var(--aurral-text-muted);
- font-size: 0.875rem;
- line-height: 1.55;
-}
-
-.settings-page__sponsor-cta {
- width: fit-content;
-}
-
.settings-page__link {
color: var(--aurral-text-muted);
text-decoration: underline;
@@ -9685,11 +9647,6 @@ button.native-library-genre-row:focus-visible {
padding: 1rem 1.125rem 0;
}
-.settings-page__modal--integration .settings-page__modal-reminder {
- flex-shrink: 0;
- margin: 0.625rem 1.125rem 0;
-}
-
.settings-page__modal-body {
flex: 1;
min-height: 0;
@@ -12475,6 +12432,7 @@ button.native-library-genre-row:focus-visible {
}
.profile-page__header {
+ position: relative;
display: flex;
align-items: flex-start;
justify-content: space-between;
@@ -12489,10 +12447,17 @@ button.native-library-genre-row:focus-visible {
}
.profile-page__save-state {
+ position: absolute;
+ top: 0.25rem;
+ right: 0;
flex-shrink: 0;
- padding-top: 0.25rem;
color: var(--aurral-text-subtle);
font-size: 0.75rem;
+ visibility: hidden;
+}
+
+.profile-page__save-state.is-saving {
+ visibility: visible;
}
.profile-settings {
diff --git a/frontend/src/pages/ProfilePage.jsx b/frontend/src/pages/ProfilePage.jsx
index 51dcec6c1..252a0be6d 100644
--- a/frontend/src/pages/ProfilePage.jsx
+++ b/frontend/src/pages/ProfilePage.jsx
@@ -30,8 +30,13 @@ function ProfilePage() {
Profile
Personal listening history and library defaults
-
- {account.saving ? "Saving…" : "Saved automatically"}
+
+ {account.saving ? "Saving…" : null}
diff --git a/frontend/src/pages/Settings/SettingsPage.jsx b/frontend/src/pages/Settings/SettingsPage.jsx
index 19ef53eae..374bab7f6 100644
--- a/frontend/src/pages/Settings/SettingsPage.jsx
+++ b/frontend/src/pages/Settings/SettingsPage.jsx
@@ -3,7 +3,6 @@ import { Navigate, useParams } from "react-router-dom";
import { useToast } from "../../contexts/ToastContext";
import { useAuth } from "../../contexts/AuthContext";
import { useDocumentTitle } from "../../hooks/useDocumentTitle";
-import SettingsMetadataSponsorSection from "../../components/SettingsMetadataSponsorSection";
import { useSettingsData } from "./hooks/useSettingsData";
import { useSettingsTabs } from "./hooks/useSettingsTabs";
import { useSettingsUsers } from "./hooks/useSettingsUsers";
@@ -207,16 +206,13 @@ function SettingsPage() {
);
case "metadata":
return (
- <>
-
-
- >
+
);
case "users":
return (
@@ -303,6 +299,17 @@ function SettingsPage() {
activeTab={tabs.activeTab}
onSelectTab={handleTabSelect}
/>
+ {tabs.activeTab !== "tasks" ? (
+
+ {data.saving ? "Saving…" : null}
+
+ ) : null}
{renderTabContent()}
diff --git a/frontend/src/pages/Settings/components/CommunityGuideModal.jsx b/frontend/src/pages/Settings/components/CommunityGuideModal.jsx
index 7a990488d..d3d8ed71f 100644
--- a/frontend/src/pages/Settings/components/CommunityGuideModal.jsx
+++ b/frontend/src/pages/Settings/components/CommunityGuideModal.jsx
@@ -20,10 +20,10 @@ export function CommunityGuideModal({ show, onClose, onApply }) {
tabIndex={-1}
>
- Apply Davo's Recommended Settings
+ Apply recommended settings
- This will apply Davo's Community Lidarr Guide settings to your Lidarr instance:
+ Apply Davo's Community Lidarr Guide settings to your Lidarr instance:
Update quality definitions for FLAC and FLAC 24bit
@@ -41,15 +41,14 @@ export function CommunityGuideModal({ show, onClose, onApply }) {
className="settings-page__link"
>
Read the full guide
- {" "}
- for more details on these settings.
+
Cancel
- Apply Settings
+ Apply settings
diff --git a/frontend/src/pages/Settings/components/LidarrSettingsModalContent.jsx b/frontend/src/pages/Settings/components/LidarrSettingsModalContent.jsx
index 1ad0aaefe..f74e6545f 100644
--- a/frontend/src/pages/Settings/components/LidarrSettingsModalContent.jsx
+++ b/frontend/src/pages/Settings/components/LidarrSettingsModalContent.jsx
@@ -41,6 +41,7 @@ export function LidarrSettingsSection({
showInfo,
}) {
const [lidarrTestLatencyMs, setLidarrTestLatencyMs] = useState(null);
+ const [lidarrTestStatus, setLidarrTestStatus] = useState(null);
const safeLidarrRootFolders = Array.isArray(lidarrRootFolders) ? lidarrRootFolders : [];
const safeLidarrProfiles = Array.isArray(lidarrProfiles) ? lidarrProfiles : [];
@@ -65,16 +66,19 @@ export function LidarrSettingsSection({
const url = settings.integrations?.lidarr?.url;
const apiKey = settings.integrations?.lidarr?.apiKey;
if (!url || !apiKey) {
+ setLidarrTestStatus({ tone: "error", message: "Enter the URL and API key." });
showError("Please enter both URL and API key");
return;
}
setTestingLidarr(true);
setLidarrTestLatencyMs(null);
+ setLidarrTestStatus(null);
const startTime = performance.now();
try {
const result = await testLidarrConnection(url, apiKey);
setLidarrTestLatencyMs(Math.round(performance.now() - startTime));
if (result.success) {
+ setLidarrTestStatus({ tone: "success", message: "Connected." });
showSuccess(`Lidarr connection successful! (${result.instanceName || "Lidarr"})`);
setLoadingLidarrRootFolders(true);
setLoadingLidarrProfiles(true);
@@ -115,12 +119,14 @@ export function LidarrSettingsSection({
setLoadingLidarrTags(false);
}
} else {
+ setLidarrTestStatus({ tone: "error", message: "Connection failed. Check the URL and API key, then retry." });
showError(
`Connection failed: ${result.message || result.error}${result.details ? `\n${result.details}` : ""}`,
);
}
} catch (err) {
setLidarrTestLatencyMs(Math.round(performance.now() - startTime));
+ setLidarrTestStatus({ tone: "error", message: "Connection failed. Check the URL and API key, then retry." });
const errorMsg = err.response?.data?.message || err.response?.data?.error || err.message;
showError(`Connection failed: ${errorMsg}`);
} finally {
@@ -192,9 +198,6 @@ export function LidarrSettingsSection({
Lidarr
-
- Connect Aurral to your Lidarr instance and configure library defaults.
-
@@ -230,11 +233,19 @@ export function LidarrSettingsSection({
>
{testingLidarr ? "Testing..." : "Test connection"}
+ {lidarrTestStatus ? (
+
+ {lidarrTestStatus.message}
+
+ ) : null}
>
}
>
- Music library manager. File access, mounts, and path mappings are checked in{" "}
+ Music library manager. Path access and mappings are checked in{" "}
System
@@ -250,13 +261,14 @@ export function LidarrSettingsSection({
value={settings.integrations?.lidarr?.url || ""}
onChange={(e) => {
setLidarrTestLatencyMs(null);
+ setLidarrTestStatus(null);
updateLidarr({ url: e.target.value });
}}
/>
@@ -275,6 +287,7 @@ export function LidarrSettingsSection({
value={settings.integrations?.lidarr?.apiKey || ""}
onChange={(e) => {
setLidarrTestLatencyMs(null);
+ setLidarrTestStatus(null);
updateLidarr({ apiKey: e.target.value });
}}
/>
@@ -297,7 +310,10 @@ export function LidarrSettingsSection({
-
+
+ Users can override the root folder and quality profile in Profile.
+
+
))}
-
- Users can set their own default in Profile → Library Defaults, which overrides
- this instance-wide setting.
-
-
+
))}
-
- Users can set their own default in Profile → Library Defaults, which overrides
- this instance-wide setting.
-
-
+
-
+
updateLidarr({ defaultMonitorOption: e.target.value })}
>
- None (Artist Only)
- Existing Albums
- All Albums
- Future Albums
- Missing Albums
- Latest Album
- First Album
+ None (artist only)
+ Existing albums
+ All albums
+ Future albums
+ Missing albums
+ Latest album
+ First album
-
+
- {applyingCommunityGuide ? "Applying..." : "Apply Davo's Recommended Settings"}
+ {applyingCommunityGuide ? "Applying..." : "Apply recommended settings"}
Creates quality profile, updates quality definitions, adds custom formats, and updates
diff --git a/frontend/src/pages/Settings/components/PathMappingModal.jsx b/frontend/src/pages/Settings/components/PathMappingModal.jsx
index 17d7d3d6e..536e92458 100644
--- a/frontend/src/pages/Settings/components/PathMappingModal.jsx
+++ b/frontend/src/pages/Settings/components/PathMappingModal.jsx
@@ -73,7 +73,7 @@ export function PathMappingModal({ title, initialValue, onClose, onSave }) {
diff --git a/frontend/src/pages/Settings/components/PlexSelfLinkSection.jsx b/frontend/src/pages/Settings/components/PlexSelfLinkSection.jsx
index c40fe43f0..845d1e183 100644
--- a/frontend/src/pages/Settings/components/PlexSelfLinkSection.jsx
+++ b/frontend/src/pages/Settings/components/PlexSelfLinkSection.jsx
@@ -104,10 +104,9 @@ export function PlexSelfLinkSection({ showSuccess, showError, className = "" })
return (
-
Plex Account
+
Plex account
- Link your own Plex.tv account (e.g. if an admin invited you as a friend to their Plex
- server) so your flow and shared playlists are created under your own Plex login.
+ Link your Plex account so flows and shared playlists use your login.
diff --git a/frontend/src/pages/Settings/components/QualityProfileModal.jsx b/frontend/src/pages/Settings/components/QualityProfileModal.jsx
index 399f57a97..b0c2ab82d 100644
--- a/frontend/src/pages/Settings/components/QualityProfileModal.jsx
+++ b/frontend/src/pages/Settings/components/QualityProfileModal.jsx
@@ -110,7 +110,7 @@ export function QualityProfileModal({ profile, onChange, onClose }) {
};
return (
-
+
Rank qualities from best to worst. Aurral accepts allowed qualities and upgrades tracks until they reach the cutoff.
diff --git a/frontend/src/pages/Settings/components/SettingsAccountTab.jsx b/frontend/src/pages/Settings/components/SettingsAccountTab.jsx
index 967b918ce..7de4cb8df 100644
--- a/frontend/src/pages/Settings/components/SettingsAccountTab.jsx
+++ b/frontend/src/pages/Settings/components/SettingsAccountTab.jsx
@@ -90,9 +90,6 @@ export function SettingsAccountTab({
Appearance
-
- Choose how Aurral looks on this device.
-
@@ -134,9 +131,9 @@ export function SettingsAccountTab({
-
Listening History
+
Listening history
- Connect a listening service to personalize discovery recommendations.
+ Connect a service to personalize discovery.
{profileSummary ? (
@@ -172,8 +169,7 @@ export function SettingsAccountTab({
{listenHistoryProvider === "local" ? (
- Use Aurral play events for personalized recommendations. Aurral will not read
- listening history from an external service.
+ Uses Aurral play events only.
) : listenHistoryProvider === "koito" ? (
@@ -191,8 +187,7 @@ export function SettingsAccountTab({
onChange={(e) => setListenHistoryUrl(e.target.value)}
/>
- Your self-hosted Koito instance URL. Aurral reads top artists from Koito's
- chart API to power personalized discovery.
+ Aurral reads top artists from Koito for personalized discovery.
) : (
@@ -213,8 +208,7 @@ export function SettingsAccountTab({
onChange={(e) => setListenHistoryUsername(e.target.value)}
/>
- Aurral uses the profile selected above for personalized discovery recommendations.
- Configure the Last.fm API key in{" "}
+ Aurral uses this profile for personalized discovery. Configure API credentials in{" "}
Settings → Connect
@@ -233,10 +227,9 @@ export function SettingsAccountTab({
-
Library Defaults
+
Library defaults
- These defaults apply to one-click artist adds unless you override them from the
- Customize action on the artist page.
+ Defaults for one-click artist adds. Profile values override them.
@@ -246,7 +239,7 @@ export function SettingsAccountTab({
>
- Default Root Folder
+ Default root folder
))}
-
- Choose where one-click artist adds should be stored by default.
-
- Default Quality Profile
+ Default quality profile
))}
-
- Choose the Lidarr quality profile for one-click artist adds.
-
@@ -300,10 +287,10 @@ export function SettingsAccountTab({
-
Discovery Tastes
+
Discovery tastes
- Clear your More like this and Less like this feedback so recommendations start fresh.
- Manage hard exclusions on the Blocked Artists page.
+ Reset your recommendation feedback. Manage blocked artists separately.
+ {" "} Blocked artists
@@ -314,7 +301,7 @@ export function SettingsAccountTab({
className="btn btn-secondary btn-sm"
>
- {resettingTastes ? "Resetting…" : "Reset Discovery Tastes"}
+ {resettingTastes ? "Resetting…" : "Reset discovery tastes"}
diff --git a/frontend/src/pages/Settings/components/SettingsConnectTab.jsx b/frontend/src/pages/Settings/components/SettingsConnectTab.jsx
index e72c8312d..ae4b6456a 100644
--- a/frontend/src/pages/Settings/components/SettingsConnectTab.jsx
+++ b/frontend/src/pages/Settings/components/SettingsConnectTab.jsx
@@ -1,4 +1,4 @@
-import { useState, useRef } from "react";
+import { useEffect, useState, useRef } from "react";
import { testGotifyConnection } from "../../../utils/api/endpoints/settings.js";
import { Plus, Trash2, GripVertical } from "lucide-react";
@@ -30,6 +30,7 @@ export function SettingsConnectTab({
showError,
}) {
const [activeModal, setActiveModal] = useState(null);
+ const [testStatus, setTestStatus] = useState(null);
const gotify = settings.integrations?.gotify || {};
const lastfm = settings.integrations?.lastfm || {};
const ticketmaster = settings.integrations?.ticketmaster || {};
@@ -97,18 +98,25 @@ export function SettingsConnectTab({
const [dragIdx, setDragIdx] = useState(null);
const allowDragRef = useRef(null);
+ useEffect(() => {
+ setTestStatus(null);
+ }, [activeModal]);
+
const handleTestGotify = async () => {
const url = gotify.url;
const token = gotify.token;
if (!url || !token) {
- showError("Enter Gotify URL and token first");
+ setTestStatus({ tone: "error", message: "Enter the Gotify URL and token." });
+ showError("Enter the Gotify URL and token first");
return;
}
setTestingGotify(true);
try {
await testGotifyConnection(url, token);
+ setTestStatus({ tone: "success", message: "Test notification sent." });
showSuccess("Test notification sent.");
} catch (err) {
+ setTestStatus({ tone: "error", message: "Test failed. Check the URL and token, then retry." });
const msg = err.response?.data?.message || err.response?.data?.error || err.message;
showError(`Gotify test failed: ${msg}`);
} finally {
@@ -177,9 +185,6 @@ export function SettingsConnectTab({
)}
-
+
- Controls how often Aurral refreshes recommendations and flows. Listening history API
- keys are configured in{" "}
+ Use{" "}
Connect
- ; per-user accounts are in{" "}
+ {" "}for API keys and{" "}
Profile
- .
+ {" "}for personal accounts.
@@ -154,9 +152,8 @@ export function SettingsDiscoverTab({
labelFor="discover-mode"
help={
<>
- Safer favors more obvious recommendations.{" "}
- Balanced mixes familiar artists with exploration.{" "}
- Deeper pushes further beyond obvious similar artists.
+ Safer favors familiar recommendations. Balanced mixes familiarity and exploration.
+ Deeper goes further beyond similar artists.
>
}
>
@@ -179,7 +176,7 @@ export function SettingsDiscoverTab({
- {refreshingDiscovery ? "Refreshing…" : "Refresh Discovery"}
+ {refreshingDiscovery ? "Refreshing…" : "Refresh discovery"}
- {clearingCache ? "Clearing…" : "Clear Image Cache"}
+ {clearingCache ? "Clearing…" : "Clear image cache"}
>
}
diff --git a/frontend/src/pages/Settings/components/SettingsDownloadClientsSection.jsx b/frontend/src/pages/Settings/components/SettingsDownloadClientsSection.jsx
index 06499a6d3..31c2b5d4c 100644
--- a/frontend/src/pages/Settings/components/SettingsDownloadClientsSection.jsx
+++ b/frontend/src/pages/Settings/components/SettingsDownloadClientsSection.jsx
@@ -1,4 +1,4 @@
-import { useState } from "react";
+import { useEffect, useState } from "react";
import { testNzbgetConnection, testSabnzbdConnection, testSlskdConnection, testYtdlpConnection } from "../../../utils/api/endpoints/settings.js";
import { Plus, RefreshCw, Trash2, Wrench } from "lucide-react";
@@ -65,9 +65,14 @@ export function SettingsDownloadClientsSection({
const [testingYtdlp, setTestingYtdlp] = useState(false);
const [testingNzbget, setTestingNzbget] = useState(false);
const [testingSabnzbd, setTestingSabnzbd] = useState(false);
+ const [testStatus, setTestStatus] = useState(null);
const [showAdvanced, setShowAdvanced] = useState(false);
const [mappingModal, setMappingModal] = useState(null);
+ useEffect(() => {
+ setTestStatus(null);
+ }, [activeModal]);
+
const integrations = settings.integrations || {};
const slskd = integrations.slskd || {};
const ytdlp = integrations.ytdlp || {};
@@ -148,6 +153,7 @@ export function SettingsDownloadClientsSection({
const handleTestNzbget = async () => {
if (!nzbgetEnabled || !nzbget.url) {
+ setTestStatus({ tone: "error", message: "Enable NZBGet and enter the server URL." });
showError("Enable NZBGet and enter the server URL first");
return;
}
@@ -155,8 +161,10 @@ export function SettingsDownloadClientsSection({
try {
await handleSaveSettings();
const result = await testNzbgetConnection();
+ setTestStatus({ tone: "success", message: "Connected." });
showSuccess(result.message || "NZBGet connection OK");
} catch (error) {
+ setTestStatus({ tone: "error", message: "Connection failed. Check the settings and retry." });
showError(
error.response?.data?.message ||
error.response?.data?.error ||
@@ -170,6 +178,7 @@ export function SettingsDownloadClientsSection({
const handleTestSabnzbd = async () => {
if (!sabnzbdEnabled || !sabnzbd.url || !sabnzbd.apiKey) {
+ setTestStatus({ tone: "error", message: "Enable SABnzbd and enter the URL and API key." });
showError("Enable SABnzbd and enter the server URL and API key first");
return;
}
@@ -177,8 +186,10 @@ export function SettingsDownloadClientsSection({
try {
await handleSaveSettings();
const result = await testSabnzbdConnection();
+ setTestStatus({ tone: "success", message: "Connected." });
showSuccess(result.message || "SABnzbd connection OK");
} catch (error) {
+ setTestStatus({ tone: "error", message: "Connection failed. Check the settings and retry." });
showError(
error.response?.data?.message ||
error.response?.data?.error ||
@@ -192,6 +203,7 @@ export function SettingsDownloadClientsSection({
const handleTestSlskd = async () => {
if (!slskd.url || !slskd.apiKey) {
+ setTestStatus({ tone: "error", message: "Enter the slskd URL and API key." });
showError("Enter slskd URL and API key first");
return;
}
@@ -201,14 +213,18 @@ export function SettingsDownloadClientsSection({
const result = await testSlskdConnection();
if (result.success || result.ok) {
if (result.warning || result.soulseekConnected === false) {
+ setTestStatus({ tone: "warning", message: "API reachable, but Soulseek is not connected." });
showInfo(result.message || "slskd API is reachable, but Soulseek is not connected");
} else {
+ setTestStatus({ tone: "success", message: "Connected." });
showSuccess(result.message || "slskd connection OK");
}
} else {
+ setTestStatus({ tone: "error", message: "Connection failed. Check the settings and retry." });
showError(result.message || "slskd connection failed");
}
} catch (error) {
+ setTestStatus({ tone: "error", message: "Connection failed. Check the settings and retry." });
showError(
error.response?.data?.message ||
error.response?.data?.error ||
@@ -225,8 +241,10 @@ export function SettingsDownloadClientsSection({
try {
await handleSaveSettings();
const result = await testYtdlpConnection();
+ setTestStatus({ tone: "success", message: "Connected." });
showSuccess(result.message || "yt-dlp OK");
} catch (error) {
+ setTestStatus({ tone: "error", message: "Connection failed. Check the settings and retry." });
showError(
error.response?.data?.message ||
error.response?.data?.error ||
@@ -244,9 +262,6 @@ export function SettingsDownloadClientsSection({
Download clients
-
- Clients Aurral sends playlist and flow downloads to.
-
@@ -281,9 +296,9 @@ export function SettingsDownloadClientsSection({
-
+
- Choose acceptable formats, rank them by preference, and set the upgrade cutoff.
+ Choose formats, order, and upgrade cutoff.
-
+
-
+
- Remote path mappings are rarely required. If Aurral and your download clients share the
- same container mounts, match paths instead of adding mappings here.
+ Only needed when client and Aurral paths differ. Shared mounts need no mapping.
@@ -325,8 +339,8 @@ export function SettingsDownloadClientsSection({
Source
- Remote Path
- Local Path
+ Remote path
+ Local path
Actions
@@ -389,7 +403,7 @@ export function SettingsDownloadClientsSection({
{mappingModal ? (
setActiveModal(null)}
+ testStatus={testStatus}
footerActions={
}
>
-
- updateIntegration("slskd", { enabled: event.target.checked })}
- />
-
+ updateIntegration("slskd", { enabled: event.target.checked })}
+ />
@@ -484,6 +497,7 @@ export function SettingsDownloadClientsSection({
setActiveModal(null)}
+ testStatus={testStatus}
footerActions={
}
>
-
- updateIntegration("ytdlp", { enabled: event.target.checked })}
- />
-
+ updateIntegration("ytdlp", { enabled: event.target.checked })}
+ />
@@ -524,7 +536,7 @@ export function SettingsDownloadClientsSection({
setActiveModal(null)}
+ testStatus={testStatus}
footerActions={
}
>
-
- updateIntegration("nzbget", { enabled: event.target.checked })}
- />
-
+ updateIntegration("nzbget", { enabled: event.target.checked })}
+ />
@@ -668,6 +679,7 @@ export function SettingsDownloadClientsSection({
setActiveModal(null)}
+ testStatus={testStatus}
footerActions={
}
>
-
- updateIntegration("sabnzbd", { enabled: event.target.checked })}
- />
-
+ updateIntegration("sabnzbd", { enabled: event.target.checked })}
+ />
diff --git a/frontend/src/pages/Settings/components/SettingsIndexersSection.jsx b/frontend/src/pages/Settings/components/SettingsIndexersSection.jsx
index 79bae2182..6399591b0 100644
--- a/frontend/src/pages/Settings/components/SettingsIndexersSection.jsx
+++ b/frontend/src/pages/Settings/components/SettingsIndexersSection.jsx
@@ -27,6 +27,7 @@ export function SettingsIndexersSection({
}) {
const [activeModal, setActiveModal] = useState(null);
const [testingProwlarr, setTestingProwlarr] = useState(false);
+ const [testStatus, setTestStatus] = useState(null);
const [loadingProwlarrIndexers, setLoadingProwlarrIndexers] = useState(false);
const [prowlarrIndexers, setProwlarrIndexers] = useState([]);
@@ -34,6 +35,10 @@ export function SettingsIndexersSection({
const prowlarrConfigured = Boolean(prowlarr.url && prowlarr.apiKey);
const prowlarrEnabled = prowlarr.enabled === true;
+ useEffect(() => {
+ setTestStatus(null);
+ }, [activeModal]);
+
const updateIntegration = (key, patch) =>
updateSettings({
...settings,
@@ -126,6 +131,7 @@ export function SettingsIndexersSection({
const handleTestProwlarr = async () => {
if (!prowlarrEnabled || !prowlarr.url || !prowlarr.apiKey) {
+ setTestStatus({ tone: "error", message: "Enable Prowlarr and enter the URL and API key." });
showError("Enable Prowlarr and enter URL and API key first");
return;
}
@@ -134,8 +140,10 @@ export function SettingsIndexersSection({
await handleSaveSettings();
const result = await testProwlarrConnection();
setProwlarrIndexers(Array.isArray(result.indexers) ? result.indexers : []);
+ setTestStatus({ tone: "success", message: "Connected." });
showSuccess(result.message || "Prowlarr connection OK");
} catch (error) {
+ setTestStatus({ tone: "error", message: "Connection failed. Check the URL and API key, then retry." });
showError(
error.response?.data?.message ||
error.response?.data?.error ||
@@ -159,9 +167,6 @@ export function SettingsIndexersSection({
Indexers
-
- Configure Prowlarr and choose which Usenet indexers Aurral uses.
-
{loadingProwlarrIndexers && (
@@ -210,6 +215,7 @@ export function SettingsIndexersSection({
setActiveModal(null)}
+ testStatus={testStatus}
footerActions={
}
>
-
- updateIntegration("prowlarr", { enabled: event.target.checked })}
- />
-
+ updateIntegration("prowlarr", { enabled: event.target.checked })}
+ />
@@ -301,18 +305,16 @@ export function SettingsIndexersSection({
const state = getIndexerState(activeIndexer);
return (
<>
-
-
- updateProwlarrIndexer(activeIndexer.id, {
- enabled: event.target.checked,
- priority: state.priority,
- })
- }
- />
-
+
+ updateProwlarrIndexer(activeIndexer.id, {
+ enabled: event.target.checked,
+ priority: state.priority,
+ })
+ }
+ />
{meta} : null}
-
+
+
+ {status.label}
+
@@ -34,10 +35,9 @@ export function SettingsIntegrationModal({
title,
children,
onClose,
- saveReminder = true,
wide = true,
footerActions = null,
- showDone = true,
+ testStatus = null,
}) {
const titleId = useId();
const { dialogRef, handleBackdropClick } = useModalDialog({
@@ -67,21 +67,25 @@ export function SettingsIntegrationModal({
onClick={onClose}
aria-label="Close"
>
-
+
- {saveReminder ? (
-
- Changes save automatically.
-
- ) : null}
-
- {footerActions}
- {showDone ? Done : null}
-
+ {footerActions || testStatus ? (
+
+ {testStatus ? (
+
+ {testStatus.message}
+
+ ) : null}
+ {footerActions}
+
+ ) : null}
);
diff --git a/frontend/src/pages/Settings/components/SettingsMetadataTab.jsx b/frontend/src/pages/Settings/components/SettingsMetadataTab.jsx
index 7b54c2e92..486c3ffe9 100644
--- a/frontend/src/pages/Settings/components/SettingsMetadataTab.jsx
+++ b/frontend/src/pages/Settings/components/SettingsMetadataTab.jsx
@@ -13,7 +13,7 @@ export function SettingsMetadataTab({
@@ -91,24 +91,22 @@ function DiskSpaceTable({ entries = [] }) {
);
}
-function SystemSection({ title, description, children }) {
+function SystemSection({ title, children }) {
return (
{title}
- {description ?
{description}
: null}
{children}
);
}
-function SystemRow({ label, description, children }) {
+function SystemRow({ label, children }) {
return (
{label}
- {description ?
{description}
: null}
{children ?? "—"}
@@ -189,16 +187,8 @@ export function SettingsStorageHealthSection({
return (
<>
-
-
-
-
-
-
-
-
- {checkingHealth ? "Checking…" : "Run Checks"}
+ {checkingHealth ? "Checking…" : "Run checks"}
}
>
-
- Verifies that Aurral can access configured library paths, transfer completed downloads,
- and emit paths that playback servers can scan. Matching container paths are simplest,
- but narrower mounts and remote path mappings are supported.
-
+
+ Checks configured library, download, and playback paths.
+
+
+
+
>
);
}
@@ -231,7 +222,6 @@ export function SettingsSystemSection({ health }) {
System
-
A quick overview of the running Aurral app.
@@ -239,45 +229,42 @@ export function SettingsSystemSection({ health }) {
-
-
+
+
{system.version || health?.appVersion}
-
+
{formatUptime(system.uptimeSeconds)}
-
+
{system.mode && system.hostname ? `${system.mode} · ${system.hostname}` : null}
-
+
{system.nodeVersion && system.platform && system.arch
? `Node ${system.nodeVersion} · ${system.platform} ${system.arch}`
: null}
-
+
{system.docker == null ? null : system.docker ? "Docker" : "Host process"}
-
-
+
+
{system.database?.label}
-
+
{system.dataDir}
-
+
{system.databasePath}
-
+
{system.startupDirectory}
-
+
{(system.links || []).map((link) => (
diff --git a/frontend/src/pages/Settings/components/SettingsTasksTab.jsx b/frontend/src/pages/Settings/components/SettingsTasksTab.jsx
index bc2aa98bc..67f7a8322 100644
--- a/frontend/src/pages/Settings/components/SettingsTasksTab.jsx
+++ b/frontend/src/pages/Settings/components/SettingsTasksTab.jsx
@@ -213,9 +213,9 @@ function ScheduledTable({ scheduled = [], loading = false }) {
Name
Interval
- Last Execution
- Last Duration
- Next Execution
+ Last run
+ Duration
+ Next run
Result
@@ -281,7 +281,7 @@ function WorkersTable({ workers = [], loading = false, showAllWorkers = false })
Queued
Scheduled
Failed
- Last Run
+ Last run
diff --git a/frontend/src/pages/Settings/components/SettingsUsersTab.jsx b/frontend/src/pages/Settings/components/SettingsUsersTab.jsx
index 20e1a7993..7d6ba5ed7 100644
--- a/frontend/src/pages/Settings/components/SettingsUsersTab.jsx
+++ b/frontend/src/pages/Settings/components/SettingsUsersTab.jsx
@@ -197,7 +197,7 @@ export function SettingsUsersTab({
return (
{authUser?.role !== "admin" ? (
-
+
{
@@ -275,7 +275,7 @@ export function SettingsUsersTab({
) : (
<>
-
+
+
-
{legend}
+
+ {legend}
+
{actions ?
{actions}
: null}
{children}
-
+
);
}
diff --git a/frontend/src/pages/Settings/settingsArr.css b/frontend/src/pages/Settings/settingsArr.css
index dca7fc439..f6509e9b9 100644
--- a/frontend/src/pages/Settings/settingsArr.css
+++ b/frontend/src/pages/Settings/settingsArr.css
@@ -174,6 +174,20 @@
flex-shrink: 0;
}
+.arr-test-result {
+ color: var(--arr-text-muted);
+ font-size: 0.75rem;
+ line-height: 1.4;
+}
+
+.arr-test-result--success {
+ color: var(--aurral-success);
+}
+
+.arr-test-result--error {
+ color: var(--aurral-danger);
+}
+
.arr-fieldset__body {
display: block;
}
@@ -892,7 +906,6 @@
font-weight: 400;
}
-.settings-arr__modal .settings-page__modal-reminder,
.settings-arr__modal .settings-modal__hint,
.settings-arr__modal .settings-modal__intro {
color: var(--arr-text-muted);
@@ -1588,9 +1601,28 @@
}
.settings-arr__content {
+ position: relative;
padding: 1rem 0 2.5rem;
}
+.settings-arr__save-state {
+ position: absolute;
+ top: 0;
+ right: 0.75rem;
+ z-index: 1;
+ color: var(--aurral-text-subtle);
+ font-size: 0.75rem;
+ line-height: 1.25;
+ text-align: right;
+ visibility: hidden;
+ pointer-events: none;
+}
+
+.settings-arr__save-state.is-saving {
+ color: var(--aurral-text-muted);
+ visibility: visible;
+}
+
.settings-arr__toolbar {
justify-content: flex-end;
min-height: 2rem;
@@ -1815,13 +1847,6 @@
letter-spacing: -0.02em;
}
-.settings-arr .settings-page__section--sponsor {
- padding: 0.875rem 1rem;
- border: 1px solid var(--aurral-border);
- border-radius: 0.75rem;
- background: color-mix(in srgb, var(--aurral-text) 3%, transparent);
-}
-
.settings-arr :is(.arr-input, .arr-select, .arr-textarea, .artist-input) {
height: 2rem;
padding: 0 0.625rem;
@@ -1973,17 +1998,26 @@
.settings-arr .arr-card__main,
.settings-arr .arr-card__side,
-.settings-arr .arr-card__status {
+.settings-arr .arr-card__status-wrap,
+.settings-arr .arr-card__status-label {
min-width: 0;
}
.settings-arr .arr-card__title,
-.settings-arr .arr-card__status {
+.settings-arr .arr-card__status-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
+.settings-arr .arr-card__status-wrap {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.375rem;
+ color: var(--arr-text-muted);
+ font-size: 0.6875rem;
+}
+
.settings-arr .arr-card__title {
font-size: 0.875rem;
font-weight: 600;
@@ -2103,11 +2137,91 @@
border-color: var(--aurral-border);
}
+.settings-arr .settings-page__modal-result {
+ flex: 1 1 auto;
+ min-width: 0;
+ margin: 0 auto 0 0;
+ color: var(--arr-text-muted);
+ font-size: 0.75rem;
+ line-height: 1.4;
+}
+
+.settings-arr .settings-page__modal-result--success {
+ color: var(--aurral-success);
+}
+
+.settings-arr .settings-page__modal-result--error {
+ color: var(--aurral-danger);
+}
+
+.settings-arr .settings-page__modal-result--warning {
+ color: var(--aurral-warning);
+}
+
+@media (pointer: coarse) {
+ .settings-arr .arr-btn--icon,
+ .arr-portal .arr-btn--icon {
+ width: 2.75rem;
+ min-width: 2.75rem;
+ height: 2.75rem;
+ min-height: 2.75rem;
+ }
+
+ .settings-arr .pill-toggle {
+ --w: 2rem;
+ --h: calc(var(--w) * 0.4865);
+ width: 2.75rem;
+ height: 2.75rem;
+ background: transparent;
+ box-shadow: none;
+ }
+
+ .settings-arr .pill-toggle::after {
+ position: absolute;
+ top: 0.875rem;
+ left: 0.375rem;
+ width: var(--w);
+ height: var(--h);
+ border-radius: var(--aurral-radius-round);
+ background: var(--aurral-surface-raised);
+ box-shadow: inset 0 0 0 1px var(--aurral-border);
+ content: "";
+ }
+
+ .settings-arr .pill-toggle::before {
+ top: calc(0.875rem + var(--thumb-offset));
+ bottom: auto;
+ left: calc(0.375rem + var(--thumb-offset));
+ z-index: 1;
+ }
+
+ .settings-arr .pill-toggle[aria-checked="true"] {
+ background: transparent;
+ box-shadow: none;
+ }
+
+ .settings-arr .pill-toggle[aria-checked="true"]::after {
+ background: var(--aurral-control-on);
+ box-shadow:
+ inset 0 0 0 1px color-mix(in srgb, var(--aurral-control-on) 72%, var(--aurral-text)),
+ 0 0 0 1px color-mix(in srgb, var(--aurral-control-on) 38%, transparent);
+ }
+
+ .settings-arr .pill-toggle:focus-visible,
+ .settings-arr .pill-toggle[aria-checked="true"]:focus-visible {
+ box-shadow: 0 0 0 3px var(--aurral-ring);
+ }
+}
+
@media (max-width: 48rem) {
.settings-arr__content {
padding: 0.5rem 0 2rem;
}
+ .settings-arr__save-state {
+ top: 4rem;
+ }
+
.settings-arr__toolbar {
padding: 0 0.5rem;
}
diff --git a/frontend/src/pages/Settings/settingsTabsConfig.js b/frontend/src/pages/Settings/settingsTabsConfig.js
index ef6e81b9a..b3b5182ec 100644
--- a/frontend/src/pages/Settings/settingsTabsConfig.js
+++ b/frontend/src/pages/Settings/settingsTabsConfig.js
@@ -15,14 +15,14 @@ import {
export const SETTINGS_TABS = [
{ id: "system", label: "System", icon: Monitor },
- { id: "storage-health", label: "Storage Health", icon: HardDrive },
+ { id: "storage-health", label: "Storage health", icon: HardDrive },
{ id: "tasks", label: "Tasks", icon: ListChecks },
{ id: "lidarr", label: "Lidarr", icon: Server },
{ id: "indexers", label: "Indexers", icon: DatabaseSearch },
- { id: "download-clients", label: "Download Clients", icon: Download },
+ { id: "download-clients", label: "Download clients", icon: Download },
{ id: "playback", label: "Playback", icon: Music },
{ id: "connect", label: "Connect", icon: Bell },
- { id: "rss-news", label: "RSS News", icon: Rss },
+ { id: "rss-news", label: "RSS news", icon: Rss },
{ id: "discover", label: "Discover", icon: Compass },
{ id: "metadata", label: "Metadata", icon: Database, hidden: true },
{ id: "users", label: "Users", icon: Users },
@@ -32,7 +32,7 @@ export const SETTINGS_NAV_TABS = SETTINGS_TABS.filter((tab) => !tab.hidden);
const SETTINGS_SEARCH_METADATA = {
system: {
- sections: ["Runtime", "Data", "Display", "API Key", "More Info"],
+ sections: ["Runtime", "Data", "Display", "API key", "More info"],
services: {
Runtime: "version uptime host platform",
Database: "sqlite data runtime",
@@ -51,7 +51,7 @@ const SETTINGS_SEARCH_METADATA = {
},
},
"storage-health": {
- sections: ["Health", "Disk Space", "Storage Health"],
+ sections: ["Health", "Disk space", "Storage health"],
services: {
Storage: "disk filesystem paths data directory",
Downloads: "download paths free space",
@@ -98,7 +98,7 @@ const SETTINGS_SEARCH_METADATA = {
},
},
indexers: {
- sections: ["General", "Connection", "Indexing", "Priority", "Details"],
+ sections: ["Connection", "Indexing", "Priority", "Details"],
services: {
Prowlarr: "indexer manager search",
Usenet: "audio indexers",
@@ -116,7 +116,7 @@ const SETTINGS_SEARCH_METADATA = {
},
},
"download-clients": {
- sections: ["Quality Profile", "Downloads Folder", "Remote Path Mappings", "General", "Connection", "Behavior", "Downloads", "Advanced"],
+ sections: ["Quality profile", "Downloads folder", "Remote path mappings", "Connection", "Behavior", "Downloads", "Advanced"],
services: {
slskd: "Soulseek download client",
"yt-dlp": "YouTube web download client",
@@ -124,7 +124,7 @@ const SETTINGS_SEARCH_METADATA = {
SABnzbd: "Usenet download client",
},
fields: {
- "Quality Profile": "default acceptable allowed formats rank preference cutoff upgrades",
+ "Quality profile": "default acceptable allowed formats rank preference cutoff upgrades",
Qualities: "FLAC MP3 M4A 128 192 256 320 bitrate hi-res standard allowed cutoff drag rank",
"Automatic upgrades": "upgrade searches Flow Static tracks",
"Upgrade interval": "days between checks",
@@ -150,7 +150,7 @@ const SETTINGS_SEARCH_METADATA = {
},
},
playback: {
- sections: ["Playback Servers", "Scrobbling", "Cover Art", "Connection", "Account", "Aurral Library Path", "Main library (optional)", "Sync"],
+ sections: ["Playback servers", "Scrobbling", "Cover art", "Connection", "Account", "Aurral library path", "Main library (optional)", "Sync"],
services: {
Navidrome: "Subsonic music server",
Plex: "Plexamp music server",
@@ -173,7 +173,7 @@ const SETTINGS_SEARCH_METADATA = {
},
},
connect: {
- sections: ["Connections", "Webhooks", "Notification Events", "Inbox"],
+ sections: ["Connections", "Webhooks", "Notification events", "Inbox"],
services: {
Gotify: "push notifications mobile alerts",
"Last.fm": "recommendations API key secret scrobbling",
@@ -198,22 +198,22 @@ const SETTINGS_SEARCH_METADATA = {
"Weekly flow finished": "notification event webhook Gotify",
"Request made": "notification event webhook Gotify",
"Request available": "notification event webhook Gotify",
- "Enable Inbox": "inbox on off",
+ "Enable inbox": "inbox on off",
"Upcoming releases": "inbox albums",
"Upcoming shows": "inbox concerts events",
- "Library Artist news": "inbox RSS",
- "Recommended Artist news": "inbox RSS",
+ "Library artist news": "inbox RSS",
+ "Recommended artist news": "inbox RSS",
Discoveries: "inbox recommendations",
},
},
"rss-news": {
- sections: ["RSS News", "Custom feeds", "Feed groups"],
+ sections: ["RSS news", "Custom feeds", "Feed groups"],
services: {
News: "RSS articles artists library recommendations",
Feeds: "music news sources custom feeds",
},
fields: {
- "Enable RSS News": "news on off",
+ "Enable RSS news": "news on off",
"Add RSS feed": "custom source",
"Feed name": "custom RSS source",
"Feed URL": "custom RSS address",
@@ -221,7 +221,7 @@ const SETTINGS_SEARCH_METADATA = {
},
},
discover: {
- sections: ["Discovery Behavior", "Cache Status"],
+ sections: ["Discovery behavior", "Cache status"],
services: {
"Last.fm": "recommendations listening history",
ListenBrainz: "recommendations discovery fallback",
@@ -239,7 +239,7 @@ const SETTINGS_SEARCH_METADATA = {
},
},
metadata: {
- sections: ["Metadata Server"],
+ sections: ["Metadata server"],
services: {
BrainzMash: "MusicBrainz metadata provider",
},
@@ -248,7 +248,7 @@ const SETTINGS_SEARCH_METADATA = {
},
},
users: {
- sections: ["Change Password", "Local Network Auto-login", "Users"],
+ sections: ["Change password", "Local network auto-login", "Users"],
services: {
Authentication: "login password security",
Permissions: "roles access control",
From 5bcdc3f490209e2e369858f2e5362021b67248de Mon Sep 17 00:00:00 2001
From: Lee Kelly
Date: Mon, 17 Aug 2026 02:46:07 +0000
Subject: [PATCH 2/2] fix(settings): address review feedback
---
.../components/SettingsConnectTab.jsx | 5 ++-
.../SettingsDownloadClientsSection.jsx | 8 +++-
.../components/SettingsIndexersSection.jsx | 5 ++-
.../components/SettingsPlaybackSection.jsx | 12 ++++--
.../components/arr/SettingsArrLayout.jsx | 14 +++----
frontend/src/pages/Settings/settingsArr.css | 40 +++++++++++--------
6 files changed, 54 insertions(+), 30 deletions(-)
diff --git a/frontend/src/pages/Settings/components/SettingsConnectTab.jsx b/frontend/src/pages/Settings/components/SettingsConnectTab.jsx
index ae4b6456a..b1635a3a5 100644
--- a/frontend/src/pages/Settings/components/SettingsConnectTab.jsx
+++ b/frontend/src/pages/Settings/components/SettingsConnectTab.jsx
@@ -62,7 +62,8 @@ export function SettingsConnectTab({
});
};
- const updateGotify = (patch) =>
+ const updateGotify = (patch) => {
+ setTestStatus(null);
updateSettings({
...settings,
integrations: {
@@ -70,6 +71,7 @@ export function SettingsConnectTab({
gotify: { ...gotify, ...patch },
},
});
+ };
const updateLastfm = (patch) =>
updateSettings({
@@ -103,6 +105,7 @@ export function SettingsConnectTab({
}, [activeModal]);
const handleTestGotify = async () => {
+ setTestStatus(null);
const url = gotify.url;
const token = gotify.token;
if (!url || !token) {
diff --git a/frontend/src/pages/Settings/components/SettingsDownloadClientsSection.jsx b/frontend/src/pages/Settings/components/SettingsDownloadClientsSection.jsx
index 31c2b5d4c..c0a32b419 100644
--- a/frontend/src/pages/Settings/components/SettingsDownloadClientsSection.jsx
+++ b/frontend/src/pages/Settings/components/SettingsDownloadClientsSection.jsx
@@ -98,7 +98,8 @@ export function SettingsDownloadClientsSection({
const nzbgetEnabled = nzbget.enabled === true;
const sabnzbdEnabled = sabnzbd.enabled === true;
- const updateIntegration = (key, patch) =>
+ const updateIntegration = (key, patch) => {
+ setTestStatus(null);
updateSettings({
...settings,
integrations: {
@@ -109,6 +110,7 @@ export function SettingsDownloadClientsSection({
},
},
});
+ };
const updatePathMappings = (nextMappings) => {
updateSettings({
@@ -152,6 +154,7 @@ export function SettingsDownloadClientsSection({
};
const handleTestNzbget = async () => {
+ setTestStatus(null);
if (!nzbgetEnabled || !nzbget.url) {
setTestStatus({ tone: "error", message: "Enable NZBGet and enter the server URL." });
showError("Enable NZBGet and enter the server URL first");
@@ -177,6 +180,7 @@ export function SettingsDownloadClientsSection({
};
const handleTestSabnzbd = async () => {
+ setTestStatus(null);
if (!sabnzbdEnabled || !sabnzbd.url || !sabnzbd.apiKey) {
setTestStatus({ tone: "error", message: "Enable SABnzbd and enter the URL and API key." });
showError("Enable SABnzbd and enter the server URL and API key first");
@@ -202,6 +206,7 @@ export function SettingsDownloadClientsSection({
};
const handleTestSlskd = async () => {
+ setTestStatus(null);
if (!slskd.url || !slskd.apiKey) {
setTestStatus({ tone: "error", message: "Enter the slskd URL and API key." });
showError("Enter slskd URL and API key first");
@@ -237,6 +242,7 @@ export function SettingsDownloadClientsSection({
};
const handleTestYtdlp = async () => {
+ setTestStatus(null);
setTestingYtdlp(true);
try {
await handleSaveSettings();
diff --git a/frontend/src/pages/Settings/components/SettingsIndexersSection.jsx b/frontend/src/pages/Settings/components/SettingsIndexersSection.jsx
index 6399591b0..21cdd6e6a 100644
--- a/frontend/src/pages/Settings/components/SettingsIndexersSection.jsx
+++ b/frontend/src/pages/Settings/components/SettingsIndexersSection.jsx
@@ -39,7 +39,8 @@ export function SettingsIndexersSection({
setTestStatus(null);
}, [activeModal]);
- const updateIntegration = (key, patch) =>
+ const updateIntegration = (key, patch) => {
+ setTestStatus(null);
updateSettings({
...settings,
integrations: {
@@ -50,6 +51,7 @@ export function SettingsIndexersSection({
},
},
});
+ };
const loadProwlarrIndexers = async ({ quiet = false } = {}) => {
if (!health?.prowlarrConfigured) {
@@ -130,6 +132,7 @@ export function SettingsIndexersSection({
};
const handleTestProwlarr = async () => {
+ setTestStatus(null);
if (!prowlarrEnabled || !prowlarr.url || !prowlarr.apiKey) {
setTestStatus({ tone: "error", message: "Enable Prowlarr and enter the URL and API key." });
showError("Enable Prowlarr and enter URL and API key first");
diff --git a/frontend/src/pages/Settings/components/SettingsPlaybackSection.jsx b/frontend/src/pages/Settings/components/SettingsPlaybackSection.jsx
index efb9cf596..a1adefc59 100644
--- a/frontend/src/pages/Settings/components/SettingsPlaybackSection.jsx
+++ b/frontend/src/pages/Settings/components/SettingsPlaybackSection.jsx
@@ -99,7 +99,8 @@ export function SettingsPlaybackSection({
setPlexPathPickerOpen(false);
};
- const updateNavidrome = (patch) =>
+ const updateNavidrome = (patch) => {
+ setTestStatus(null);
updateSettings({
...settings,
integrations: {
@@ -107,8 +108,10 @@ export function SettingsPlaybackSection({
navidrome: { ...navidrome, ...patch },
},
});
+ };
- const updatePlex = (patch) =>
+ const updatePlex = (patch) => {
+ setTestStatus(null);
updateSettings({
...settings,
integrations: {
@@ -116,6 +119,7 @@ export function SettingsPlaybackSection({
plex: { ...plex, ...patch },
},
});
+ };
const updatePlexLibraryLocalPath = (localPath) => {
const trimmed = String(localPath || "").trim();
@@ -261,6 +265,7 @@ export function SettingsPlaybackSection({
};
const handleTestPlex = async () => {
+ setTestStatus(null);
if (!plex.url || !plex.token) {
setTestStatus({ tone: "error", message: "Connect to Plex and select a server first." });
showError("Connect to Plex and select a server first");
@@ -270,11 +275,11 @@ export function SettingsPlaybackSection({
try {
const result = await testPlaybackConnection("plex", plex);
if (result.success) {
- setTestStatus({ tone: "success", message: "Connected." });
showSuccess(`Plex connection successful!${result.version ? ` (v${result.version})` : ""}`);
if (result.machineIdentifier) {
updatePlex({ machineIdentifier: result.machineIdentifier });
}
+ setTestStatus({ tone: "success", message: "Connected." });
} else {
setTestStatus({ tone: "error", message: "Connection failed. Check Plex settings and retry." });
showError(`Connection failed: ${result.message || result.error}`);
@@ -289,6 +294,7 @@ export function SettingsPlaybackSection({
};
const handleTestNavidrome = async () => {
+ setTestStatus(null);
if (!navidrome.url || !navidrome.username || !navidrome.password) {
setTestStatus({ tone: "error", message: "Enter the URL and credentials first." });
showError("Enter Navidrome URL, username, and password first");
diff --git a/frontend/src/pages/Settings/components/arr/SettingsArrLayout.jsx b/frontend/src/pages/Settings/components/arr/SettingsArrLayout.jsx
index cf1c1eb70..fd65f7bca 100644
--- a/frontend/src/pages/Settings/components/arr/SettingsArrLayout.jsx
+++ b/frontend/src/pages/Settings/components/arr/SettingsArrLayout.jsx
@@ -4,15 +4,13 @@ export function SettingsArrFieldSet({ legend, actions = null, children }) {
const headingId = useId();
return (
-
-
-
- {legend}
-
- {actions ?
{actions}
: null}
-
+
+
+ {legend}
+
+ {actions ? {actions}
: null}
{children}
-
+
);
}
diff --git a/frontend/src/pages/Settings/settingsArr.css b/frontend/src/pages/Settings/settingsArr.css
index f6509e9b9..5f6547955 100644
--- a/frontend/src/pages/Settings/settingsArr.css
+++ b/frontend/src/pages/Settings/settingsArr.css
@@ -140,27 +140,20 @@
}
.arr-fieldset {
+ position: relative;
margin: 0 0 1.25rem;
padding: 0;
min-width: 0;
border: 0;
}
-.arr-fieldset__head {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 0.75rem;
- margin-bottom: 1.3125rem;
- padding-bottom: 0.625rem;
- border-bottom: 0.0625rem solid var(--aurral-border-strong);
-}
-
.arr-fieldset__legend {
display: block;
- margin: 0;
- padding: 0;
+ width: 100%;
+ margin: 0 0 1.3125rem;
+ padding: 0 0 0.625rem;
border: 0;
+ border-bottom: 0.0625rem solid var(--aurral-border-strong);
color: var(--aurral-text-muted);
font-size: 1.3125rem;
font-weight: 400;
@@ -168,6 +161,9 @@
}
.arr-fieldset__actions {
+ position: absolute;
+ top: 0;
+ right: 0;
display: flex;
align-items: center;
gap: 0.5rem;
@@ -1685,7 +1681,6 @@
background: transparent;
}
-.settings-arr .arr-fieldset__head,
.settings-arr .settings-page__section-header {
min-height: 2rem;
margin: 0 0 0.625rem;
@@ -1695,8 +1690,10 @@
.settings-arr .arr-fieldset__legend,
.settings-arr .settings-page__section-title {
- margin: 0;
- padding: 0;
+ min-height: 2rem;
+ margin: 0 0 0.625rem;
+ padding: 0 0.75rem;
+ border: 0;
color: var(--aurral-text);
font-size: 1.125rem;
font-weight: 650;
@@ -1704,7 +1701,11 @@
line-height: 1.25;
}
-.settings-arr .arr-fieldset__actions,
+.settings-arr .arr-fieldset__actions {
+ top: 0;
+ right: 0.75rem;
+}
+
.settings-arr .settings-page__inline-row {
gap: 0.375rem;
}
@@ -2170,6 +2171,7 @@
.settings-arr .pill-toggle {
--w: 2rem;
--h: calc(var(--w) * 0.4865);
+
width: 2.75rem;
height: 2.75rem;
background: transparent;
@@ -2222,6 +2224,12 @@
top: 4rem;
}
+ .settings-arr .arr-fieldset__actions {
+ position: static;
+ flex-wrap: wrap;
+ margin: -0.25rem 0.75rem 0.625rem;
+ }
+
.settings-arr__toolbar {
padding: 0 0.5rem;
}