From ebbc31f1ab1e063cee143e1fff8c8a727f9d4201 Mon Sep 17 00:00:00 2001 From: Deluan Date: Sun, 1 Feb 2026 16:16:58 +0100 Subject: [PATCH] fix(scanner): store scan errors in the database and update UI error handling Signed-off-by: Deluan --- scanner/controller.go | 4 +++ scanner/phase_1_folders.go | 2 +- scanner/scanner_multilibrary_test.go | 8 +++++- ui/src/layout/ActivityPanel.jsx | 36 ++++++++++++++++---------- ui/src/layout/ActivityPanel.test.jsx | 38 ++++++++++++++++++++++++---- 5 files changed, 67 insertions(+), 21 deletions(-) diff --git a/scanner/controller.go b/scanner/controller.go index b42246a50..635011840 100644 --- a/scanner/controller.go +++ b/scanner/controller.go @@ -224,6 +224,10 @@ func (s *controller) ScanFolders(requestCtx context.Context, fullScan bool, targ for _, w := range scanWarnings { log.Warn(ctx, fmt.Sprintf("Scan warning: %s", w)) } + // Store scan error in database so it can be displayed in the UI + if scanError != nil { + _ = s.ds.Property(ctx).Put(consts.LastScanErrorKey, scanError.Error()) + } // If changes were detected, send a refresh event to all clients if s.changesDetected { log.Debug(ctx, "Library changes imported. Sending refresh event") diff --git a/scanner/phase_1_folders.go b/scanner/phase_1_folders.go index b493a94d4..38967832c 100644 --- a/scanner/phase_1_folders.go +++ b/scanner/phase_1_folders.go @@ -40,7 +40,7 @@ func createPhaseFolders(ctx context.Context, state *scanState, ds model.DataStor job, err := newScanJob(ctx, ds, cw, lib, state.fullScan, targetFolders) if err != nil { log.Error(ctx, "Scanner: Error creating scan context", "lib", lib.Name, err) - state.sendWarning(err.Error()) + state.sendError(err) continue } jobs = append(jobs, job) diff --git a/scanner/scanner_multilibrary_test.go b/scanner/scanner_multilibrary_test.go index 66db62edf..107e66a99 100644 --- a/scanner/scanner_multilibrary_test.go +++ b/scanner/scanner_multilibrary_test.go @@ -51,8 +51,14 @@ var _ = Describe("Scanner - Multi-Library", Ordered, func() { BeforeEach(func() { DeferCleanup(configtest.SetupConfig()) + conf.Server.MusicFolder = "default:///music" // Use a distinct schema for the default library conf.Server.DevExternalScanner = false + // Register an empty fake storage for the default library + emptyFS := storagetest.FakeFS{} + emptyFS.SetFiles(fstest.MapFS{}) + storagetest.Register("default", &emptyFS) + db.Init(ctx) DeferCleanup(func() { Expect(tests.ClearDB()).To(Succeed()) @@ -770,7 +776,7 @@ var _ = Describe("Scanner - Multi-Library", Ordered, func() { // Second scan should recover and import all rock content warnings, err = s.ScanAll(ctx, true) Expect(err).ToNot(HaveOccurred()) - Expect(warnings).ToNot(BeEmpty(), "Should have warnings for temporary disk error") + Expect(warnings).To(BeEmpty(), "Should have no warnings after error recovery") // Verify both libraries now have content (at least jazz should work) rockFiles, err := ds.MediaFile(ctx).GetAll(model.QueryOptions{ diff --git a/ui/src/layout/ActivityPanel.jsx b/ui/src/layout/ActivityPanel.jsx index 6d5d32d31..085911ed7 100644 --- a/ui/src/layout/ActivityPanel.jsx +++ b/ui/src/layout/ActivityPanel.jsx @@ -15,7 +15,7 @@ import { Typography, } from '@material-ui/core' import { FiActivity } from 'react-icons/fi' -import { BiError } from 'react-icons/bi' +import { BiError, BiMessageError } from 'react-icons/bi' import { VscSync } from 'react-icons/vsc' import { GiMagnifyingGlass } from 'react-icons/gi' import subsonic from '../subsonic' @@ -28,7 +28,12 @@ import config from '../config' const useStyles = makeStyles((theme) => ({ wrapper: { position: 'relative', - color: (props) => (props.up ? null : 'orange'), + color: (props) => + props.serverDown + ? theme.palette.error.main + : props.hasWarning + ? theme.palette.warning.main + : null, }, progress: { color: theme.palette.primary.light, @@ -75,12 +80,10 @@ const ActivityPanel = () => { scanStatus.scanning, scanStatus.elapsedTime, ) - const [acknowledgedError, setAcknowledgedError] = useState(null) - const isErrorVisible = - scanStatus.error && scanStatus.error !== acknowledgedError - const classes = useStyles({ - up: up && (!scanStatus.error || !isErrorVisible), - }) + // Determine icon state: error (server down), warning (scan error), or normal + const serverDown = !up + const hasWarning = Boolean(scanStatus.error) + const classes = useStyles({ serverDown, hasWarning }) const translate = useTranslate() const notify = useNotify() const [anchorEl, setAnchorEl] = useState(null) @@ -88,13 +91,12 @@ const ActivityPanel = () => { useInitialScanStatus() const handleMenuOpen = (event) => { - if (scanStatus.error) { - setAcknowledgedError(scanStatus.error) - } setAnchorEl(event.currentTarget) } - const handleMenuClose = () => setAnchorEl(null) + const handleMenuClose = () => { + setAnchorEl(null) + } const triggerScan = (full) => () => subsonic.startScan({ fullScan: full }) useEffect(() => { @@ -125,8 +127,10 @@ const ActivityPanel = () => {
- {!up || isErrorVisible ? ( + {serverDown ? ( + ) : hasWarning ? ( + ) : ( )} @@ -155,7 +159,11 @@ const ActivityPanel = () => { {translate('activity.serverUptime')}: - + {up ? : translate('activity.serverDown')} diff --git a/ui/src/layout/ActivityPanel.test.jsx b/ui/src/layout/ActivityPanel.test.jsx index c506fd08b..3a951df5d 100644 --- a/ui/src/layout/ActivityPanel.test.jsx +++ b/ui/src/layout/ActivityPanel.test.jsx @@ -43,19 +43,47 @@ describe('', () => { }) }) - it('clears the error icon after opening the panel', () => { + it('shows warning icon when server reports a scan error', () => { render( , ) + // Warning icon should be visible when there's a scan error + expect(screen.getByTestId('activity-warning-icon')).toBeInTheDocument() + + // Open the panel - warning icon should still be visible const button = screen.getByRole('button') - expect(screen.getByTestId('activity-error-icon')).toBeInTheDocument() - fireEvent.click(button) - - expect(screen.getByTestId('activity-ok-icon')).toBeInTheDocument() + expect(screen.getByTestId('activity-warning-icon')).toBeInTheDocument() expect(screen.getByText('Scan failed')).toBeInTheDocument() }) + + it('shows error icon when server is down', () => { + const downStore = createStore( + combineReducers({ activity: activityReducer }), + { + activity: { + scanStatus: { + scanning: false, + folderCount: 0, + count: 0, + error: '', + elapsedTime: 0, + }, + serverStart: { version: config.version, startTime: null }, // null startTime = server down + }, + }, + ) + + render( + + + , + ) + + // Error icon should be visible when server is down + expect(screen.getByTestId('activity-error-icon')).toBeInTheDocument() + }) })