Compare commits

..

No commits in common. "0551b779feb1d12426f9d1d218cf00a709dbdf03" and "f3bcf10c29a687cf976970a2e4e60717dc093d79" have entirely different histories.

5 changed files with 21 additions and 67 deletions

View File

@ -224,10 +224,6 @@ func (s *controller) ScanFolders(requestCtx context.Context, fullScan bool, targ
for _, w := range scanWarnings { for _, w := range scanWarnings {
log.Warn(ctx, fmt.Sprintf("Scan warning: %s", w)) 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 changes were detected, send a refresh event to all clients
if s.changesDetected { if s.changesDetected {
log.Debug(ctx, "Library changes imported. Sending refresh event") log.Debug(ctx, "Library changes imported. Sending refresh event")

View File

@ -40,7 +40,7 @@ func createPhaseFolders(ctx context.Context, state *scanState, ds model.DataStor
job, err := newScanJob(ctx, ds, cw, lib, state.fullScan, targetFolders) job, err := newScanJob(ctx, ds, cw, lib, state.fullScan, targetFolders)
if err != nil { if err != nil {
log.Error(ctx, "Scanner: Error creating scan context", "lib", lib.Name, err) log.Error(ctx, "Scanner: Error creating scan context", "lib", lib.Name, err)
state.sendError(err) state.sendWarning(err.Error())
continue continue
} }
jobs = append(jobs, job) jobs = append(jobs, job)

View File

@ -51,14 +51,8 @@ var _ = Describe("Scanner - Multi-Library", Ordered, func() {
BeforeEach(func() { BeforeEach(func() {
DeferCleanup(configtest.SetupConfig()) DeferCleanup(configtest.SetupConfig())
conf.Server.MusicFolder = "default:///music" // Use a distinct schema for the default library
conf.Server.DevExternalScanner = false 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) db.Init(ctx)
DeferCleanup(func() { DeferCleanup(func() {
Expect(tests.ClearDB()).To(Succeed()) Expect(tests.ClearDB()).To(Succeed())
@ -776,7 +770,7 @@ var _ = Describe("Scanner - Multi-Library", Ordered, func() {
// Second scan should recover and import all rock content // Second scan should recover and import all rock content
warnings, err = s.ScanAll(ctx, true) warnings, err = s.ScanAll(ctx, true)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect(warnings).To(BeEmpty(), "Should have no warnings after error recovery") Expect(warnings).ToNot(BeEmpty(), "Should have warnings for temporary disk error")
// Verify both libraries now have content (at least jazz should work) // Verify both libraries now have content (at least jazz should work)
rockFiles, err := ds.MediaFile(ctx).GetAll(model.QueryOptions{ rockFiles, err := ds.MediaFile(ctx).GetAll(model.QueryOptions{

View File

@ -15,7 +15,7 @@ import {
Typography, Typography,
} from '@material-ui/core' } from '@material-ui/core'
import { FiActivity } from 'react-icons/fi' import { FiActivity } from 'react-icons/fi'
import { BiError, BiMessageError } from 'react-icons/bi' import { BiError } from 'react-icons/bi'
import { VscSync } from 'react-icons/vsc' import { VscSync } from 'react-icons/vsc'
import { GiMagnifyingGlass } from 'react-icons/gi' import { GiMagnifyingGlass } from 'react-icons/gi'
import subsonic from '../subsonic' import subsonic from '../subsonic'
@ -28,12 +28,7 @@ import config from '../config'
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
wrapper: { wrapper: {
position: 'relative', position: 'relative',
color: (props) => color: (props) => (props.up ? null : 'orange'),
props.serverDown
? theme.palette.error.main
: props.hasWarning
? theme.palette.warning.main
: null,
}, },
progress: { progress: {
color: theme.palette.primary.light, color: theme.palette.primary.light,
@ -80,10 +75,12 @@ const ActivityPanel = () => {
scanStatus.scanning, scanStatus.scanning,
scanStatus.elapsedTime, scanStatus.elapsedTime,
) )
// Determine icon state: error (server down), warning (scan error), or normal const [acknowledgedError, setAcknowledgedError] = useState(null)
const serverDown = !up const isErrorVisible =
const hasWarning = Boolean(scanStatus.error) scanStatus.error && scanStatus.error !== acknowledgedError
const classes = useStyles({ serverDown, hasWarning }) const classes = useStyles({
up: up && (!scanStatus.error || !isErrorVisible),
})
const translate = useTranslate() const translate = useTranslate()
const notify = useNotify() const notify = useNotify()
const [anchorEl, setAnchorEl] = useState(null) const [anchorEl, setAnchorEl] = useState(null)
@ -91,12 +88,13 @@ const ActivityPanel = () => {
useInitialScanStatus() useInitialScanStatus()
const handleMenuOpen = (event) => { const handleMenuOpen = (event) => {
if (scanStatus.error) {
setAcknowledgedError(scanStatus.error)
}
setAnchorEl(event.currentTarget) setAnchorEl(event.currentTarget)
} }
const handleMenuClose = () => { const handleMenuClose = () => setAnchorEl(null)
setAnchorEl(null)
}
const triggerScan = (full) => () => subsonic.startScan({ fullScan: full }) const triggerScan = (full) => () => subsonic.startScan({ fullScan: full })
useEffect(() => { useEffect(() => {
@ -127,10 +125,8 @@ const ActivityPanel = () => {
<div className={classes.wrapper}> <div className={classes.wrapper}>
<Tooltip title={tooltipTitle}> <Tooltip title={tooltipTitle}>
<IconButton className={classes.button} onClick={handleMenuOpen}> <IconButton className={classes.button} onClick={handleMenuOpen}>
{serverDown ? ( {!up || isErrorVisible ? (
<BiError data-testid="activity-error-icon" size={'20'} /> <BiError data-testid="activity-error-icon" size={'20'} />
) : hasWarning ? (
<BiMessageError data-testid="activity-warning-icon" size={'20'} />
) : ( ) : (
<FiActivity data-testid="activity-ok-icon" size={'20'} /> <FiActivity data-testid="activity-ok-icon" size={'20'} />
)} )}
@ -159,11 +155,7 @@ const ActivityPanel = () => {
<Box component="span" flex={2}> <Box component="span" flex={2}>
{translate('activity.serverUptime')}: {translate('activity.serverUptime')}:
</Box> </Box>
<Box <Box component="span" flex={1}>
component="span"
flex={1}
className={!up ? classes.error : null}
>
{up ? <Uptime /> : translate('activity.serverDown')} {up ? <Uptime /> : translate('activity.serverDown')}
</Box> </Box>
</Box> </Box>

View File

@ -43,47 +43,19 @@ describe('<ActivityPanel />', () => {
}) })
}) })
it('shows warning icon when server reports a scan error', () => { it('clears the error icon after opening the panel', () => {
render( render(
<Provider store={store}> <Provider store={store}>
<ActivityPanel /> <ActivityPanel />
</Provider>, </Provider>,
) )
// 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') const button = screen.getByRole('button')
expect(screen.getByTestId('activity-error-icon')).toBeInTheDocument()
fireEvent.click(button) fireEvent.click(button)
expect(screen.getByTestId('activity-warning-icon')).toBeInTheDocument()
expect(screen.getByTestId('activity-ok-icon')).toBeInTheDocument()
expect(screen.getByText('Scan failed')).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(
<Provider store={downStore}>
<ActivityPanel />
</Provider>,
)
// Error icon should be visible when server is down
expect(screen.getByTestId('activity-error-icon')).toBeInTheDocument()
})
}) })