Skip to content

Commit 94cc7d6

Browse files
committed
show error messages on widgets
1 parent af72b70 commit 94cc7d6

File tree

4 files changed

+226
-120
lines changed

4 files changed

+226
-120
lines changed

backend/src/middleware/rate-limiter.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const logRateLimitHit = (req: Request, limiterName: string) => {
88
const method = req.method;
99
const path = req.originalUrl || req.url;
1010

11-
console.error(`🚫 Rate limit hit [${limiterName}] at ${timestamp} - IP: ${ip}, Method: ${method}, Path: ${path}`);
11+
console.error(`Rate limit hit [${limiterName}] at ${timestamp}, Method: ${method}, Path: ${path}`);
1212
};
1313

1414
// General API rate limiter - used as the default
@@ -21,22 +21,24 @@ export const generalLimiter = rateLimit({
2121
logRateLimitHit(req, 'GENERAL');
2222
res.status(429).json({
2323
success: false,
24-
message: 'Too many requests from this IP, please try again after 5 minutes'
24+
message: 'Too many requests from this general IP, please try again after 5 minutes',
25+
error_source: 'labdash_api'
2526
});
2627
}
2728
});
2829

2930
// Auth endpoints rate limiter - more restrictive for security
3031
export const authLimiter = rateLimit({
3132
windowMs: 5 * 60 * 1000, // 5 minutes
32-
max: 150, // Limit each IP to 50 auth requests per window
33+
max: 150,
3334
standardHeaders: true,
3435
legacyHeaders: false,
3536
handler: (req: Request, res: Response) => {
3637
logRateLimitHit(req, 'AUTH');
3738
res.status(429).json({
3839
success: false,
39-
message: 'Too many authentication attempts, please try again after 5 minutes'
40+
message: 'Too many authentication attempts, please try again after 5 minutes',
41+
error_source: 'labdash_api'
4042
});
4143
}
4244
});
@@ -51,7 +53,8 @@ export const apiLimiter = rateLimit({
5153
logRateLimitHit(req, 'API');
5254
res.status(429).json({
5355
success: false,
54-
message: 'Too many API requests, please try again after 5 minutes'
56+
message: 'Too many API requests, please try again after 5 minutes',
57+
error_source: 'labdash_api'
5558
});
5659
}
5760
});
@@ -66,7 +69,8 @@ export const healthLimiter = rateLimit({
6669
logRateLimitHit(req, 'HEALTH');
6770
res.status(429).json({
6871
success: false,
69-
message: 'Health check rate limit exceeded, please try again later'
72+
message: 'Health check rate limit exceeded, please try again later',
73+
error_source: 'labdash_api'
7074
});
7175
}
7276
});
@@ -81,7 +85,8 @@ export const weatherApiLimiter = rateLimit({
8185
logRateLimitHit(req, 'WEATHER');
8286
res.status(429).json({
8387
success: false,
84-
message: 'Weather API rate limit exceeded, please try again later'
88+
message: 'Weather API rate limit exceeded, please try again later',
89+
error_source: 'labdash_api'
8590
});
8691
}
8792
});
@@ -96,7 +101,8 @@ export const torrentApiLimiter = rateLimit({
96101
logRateLimitHit(req, 'TORRENT');
97102
res.status(429).json({
98103
success: false,
99-
message: 'Torrent client API rate limit exceeded, please try again later'
104+
message: 'Torrent client API rate limit exceeded, please try again later',
105+
error_source: 'labdash_api'
100106
});
101107
}
102108
});
@@ -111,7 +117,8 @@ export const systemMonitorLimiter = rateLimit({
111117
logRateLimitHit(req, 'SYSTEM');
112118
res.status(429).json({
113119
success: false,
114-
message: 'System monitor API rate limit exceeded, please try again later'
120+
message: 'System monitor API rate limit exceeded, please try again later',
121+
error_source: 'labdash_api'
115122
});
116123
}
117124
});

frontend/src/components/dashboard/base-items/widgets/PiholeWidget/PiholeWidget.tsx

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -272,16 +272,23 @@ export const PiholeWidget = (props: { config?: PiholeWidgetConfig }) => {
272272
if (err.pihole?.requiresReauth) {
273273
setAuthFailed(true);
274274
setError('Authentication failed. Please check your Pi-hole credentials in widget settings.');
275-
} else if (err.response?.status === 401) {
275+
} else if (err.response?.status === 401 || err.response?.status === 403) {
276276
setAuthFailed(true);
277277
setError('Authentication failed. Please check your Pi-hole credentials in widget settings.');
278-
} else if (err.response?.status === 403) {
279-
setAuthFailed(true);
280-
setError('Access forbidden. Please check your Pi-hole credentials in widget settings.');
281278
} else if (err.response?.status === 400) {
282279
setAuthFailed(true);
283280
setError('Bad Request: Invalid configuration or authentication data');
284-
} else if (err.response?.status === 429 || err.pihole?.code === 'TOO_MANY_REQUESTS') {
281+
} else if (err.response?.status === 429) {
282+
// Check if this is a rate limit from our backend API
283+
if (err.response?.data?.error_source === 'labdash_api') {
284+
setAuthFailed(true);
285+
setError(`Lab-Dash API rate limit exceeded: ${err.response?.data?.message}`);
286+
} else {
287+
// This is a rate limit from Pi-hole itself
288+
setAuthFailed(true);
289+
setError('Too many requests to Pi-hole API. The default session expiration is 30 minutes. You can manually clear unused sessions or increase the max_sessions setting in Pi-hole.');
290+
}
291+
} else if (err.pihole?.code === 'TOO_MANY_REQUESTS') {
285292
setAuthFailed(true);
286293
setError('Too many requests to Pi-hole API. The default session expiration is 30 minutes. You can manually clear unused sessions or increase the max_sessions setting in Pi-hole.');
287294
} else if (err.message?.includes('Network Error') || err.message?.includes('timeout')) {
@@ -654,7 +661,17 @@ export const PiholeWidget = (props: { config?: PiholeWidgetConfig }) => {
654661
} else if (err.response?.status === 401 || err.response?.status === 403) {
655662
setAuthFailed(true);
656663
setError('Authentication failed. Please check your Pi-hole credentials in widget settings.');
657-
} else if (err.response?.status === 429 || err.pihole?.code === 'TOO_MANY_REQUESTS') {
664+
} else if (err.response?.status === 429) {
665+
// Check if this is a rate limit from our backend API
666+
if (err.response?.data?.error_source === 'labdash_api') {
667+
setAuthFailed(true);
668+
setError(`Lab-Dash API rate limit exceeded: ${err.response?.data?.message}`);
669+
} else {
670+
// This is a rate limit from Pi-hole itself
671+
setAuthFailed(true);
672+
setError('Too many requests to Pi-hole API. The default session expiration is 30 minutes. You can manually clear unused sessions or increase the max_sessions setting in Pi-hole.');
673+
}
674+
} else if (err.pihole?.code === 'TOO_MANY_REQUESTS') {
658675
setAuthFailed(true);
659676
setError('Too many requests to Pi-hole API. The default session expiration is 30 minutes. You can manually clear unused sessions or increase the max_sessions setting in Pi-hole.');
660677
} else if (err.message?.includes('Network Error') || err.message?.includes('timeout')) {
@@ -713,7 +730,17 @@ export const PiholeWidget = (props: { config?: PiholeWidgetConfig }) => {
713730
} else if (err.response?.status === 401 || err.response?.status === 403) {
714731
setAuthFailed(true);
715732
setError('Authentication failed. Please check your Pi-hole credentials in widget settings.');
716-
} else if (err.response?.status === 429 || err.pihole?.code === 'TOO_MANY_REQUESTS') {
733+
} else if (err.response?.status === 429) {
734+
// Check if this is a rate limit from our backend API
735+
if (err.response?.data?.error_source === 'labdash_api') {
736+
setAuthFailed(true);
737+
setError(`Lab-Dash API rate limit exceeded: ${err.response?.data?.message}`);
738+
} else {
739+
// This is a rate limit from Pi-hole itself
740+
setAuthFailed(true);
741+
setError('Too many requests to Pi-hole API. The default session expiration is 30 minutes. You can manually clear unused sessions or increase the max_sessions setting in Pi-hole.');
742+
}
743+
} else if (err.pihole?.code === 'TOO_MANY_REQUESTS') {
717744
setAuthFailed(true);
718745
setError('Too many requests to Pi-hole API. The default session expiration is 30 minutes. You can manually clear unused sessions or increase the max_sessions setting in Pi-hole.');
719746
} else if (err.message?.includes('Network Error') || err.message?.includes('timeout')) {

frontend/src/components/dashboard/base-items/widgets/SystemMonitorWidget/SystemMonitorWidget.tsx

Lines changed: 88 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { ArrowDownward, ArrowUpward } from '@mui/icons-material';
2-
import { Box, Grid2 as Grid, IconButton, Paper, Typography } from '@mui/material';
1+
import { ArrowDownward, ArrowUpward, ErrorOutline } from '@mui/icons-material';
2+
import { Box, Button, CircularProgress, Grid2 as Grid, IconButton, Paper, Typography } from '@mui/material';
33
import { useEffect, useState } from 'react';
44
import { IoInformationCircleOutline } from 'react-icons/io5';
55

@@ -39,6 +39,8 @@ export const SystemMonitorWidget = ({ config }: SystemMonitorWidgetProps) => {
3939
});
4040
const [openSystemModal, setOpenSystemModal] = useState(false);
4141
const [isFahrenheit, setIsFahrenheit] = useState(config?.temperatureUnit !== 'celsius');
42+
const [errorMessage, setErrorMessage] = useState<string | null>('null');
43+
const [isLoading, setIsLoading] = useState(false);
4244

4345
// Default gauges if not specified in config
4446
const selectedGauges = config?.gauges || ['cpu', 'temp', 'ram'];
@@ -194,8 +196,8 @@ export const SystemMonitorWidget = ({ config }: SystemMonitorWidgetProps) => {
194196
totalSpace: totalSpaceGB,
195197
usedSpace: usedSpaceGB,
196198
};
197-
} catch (error) {
198-
console.error('Error getting disk info:', error);
199+
} catch (err) {
200+
console.error('Error getting disk info:', err);
199201
return null;
200202
}
201203
};
@@ -361,25 +363,52 @@ export const SystemMonitorWidget = ({ config }: SystemMonitorWidgetProps) => {
361363
}
362364
};
363365

364-
useEffect(() => {
365-
// Update temperature unit preference from config
366-
setIsFahrenheit(config?.temperatureUnit !== 'celsius');
367-
368-
// Define the data fetching function inside the effect to avoid recreating it on every render
369-
const fetchSystemInfo = async () => {
366+
// Function to fetch system information
367+
const fetchSystemInfo = async () => {
368+
try {
369+
setIsLoading(true);
370370
const res = await DashApi.getSystemInformation(config?.networkInterface);
371371
setSystemInformation(res);
372372
getRamPercentage(res);
373373
getNetworkInformation(res);
374374
getMainDiskInfo(res);
375-
};
375+
// Clear any previous errors on successful data fetch
376+
setErrorMessage(null);
377+
setIsLoading(false);
378+
} catch (err: any) {
379+
setIsLoading(false);
380+
// Handle API rate limit errors
381+
if (err?.response?.status === 429 && err?.response?.data?.error_source === 'labdash_api') {
382+
console.error(`Lab-Dash API rate limit: ${err.response?.data?.message}`);
383+
setErrorMessage(`API Rate limit: ${err.response?.data?.message}`);
384+
} else if (err?.response?.status >= 400) {
385+
// Handle other API errors
386+
const message = err?.response?.data?.message || 'Error fetching system data';
387+
console.error(`API error: ${message}`);
388+
setErrorMessage(`API error: ${message}`);
389+
} else if (err?.message) {
390+
// Handle network or other errors
391+
console.error(`Error: ${err.message}`);
392+
setErrorMessage(`Error: ${err.message}`);
393+
} else {
394+
setErrorMessage('An unknown error occurred');
395+
}
396+
}
397+
};
398+
399+
useEffect(() => {
400+
// Update temperature unit preference from config
401+
setIsFahrenheit(config?.temperatureUnit !== 'celsius');
376402

377403
// Immediately fetch data with the current settings
378404
fetchSystemInfo();
379405

380406
// Fetch system info every 5 seconds
381407
const interval = setInterval(() => {
382-
fetchSystemInfo();
408+
// Only fetch if there's no error
409+
if (!errorMessage) {
410+
fetchSystemInfo();
411+
}
383412
}, 5000); // 5000 ms = 5 seconds
384413

385414
// Clean up the interval when component unmounts or dependencies change
@@ -400,6 +429,52 @@ export const SystemMonitorWidget = ({ config }: SystemMonitorWidgetProps) => {
400429
? (isMobile ? 0.5 : 1) // Smaller gap in dual widget, especially on mobile
401430
: 2; // Normal gap otherwise
402431

432+
// If there's an error, show full-screen error message
433+
if (errorMessage) {
434+
return (
435+
<Box sx={{
436+
height: '100%',
437+
width: '100%',
438+
display: 'flex',
439+
flexDirection: 'column',
440+
justifyContent: 'center',
441+
alignItems: 'center',
442+
p: 2
443+
}}>
444+
<Typography variant='subtitle1' align='center' sx={{ mb: 1 }}>
445+
{errorMessage}
446+
</Typography>
447+
<Button
448+
variant='contained'
449+
color='primary'
450+
onClick={fetchSystemInfo}
451+
disabled={isLoading}
452+
sx={{ mt: 2 }}
453+
>
454+
{isLoading ? 'Retrying...' : 'Retry'}
455+
</Button>
456+
</Box>
457+
);
458+
}
459+
460+
// Loading state
461+
if (isLoading && !systemInformation) {
462+
return (
463+
<Box sx={{
464+
height: '100%',
465+
width: '100%',
466+
display: 'flex',
467+
justifyContent: 'center',
468+
alignItems: 'center'
469+
}}>
470+
<Box sx={{ textAlign: 'center' }}>
471+
<Typography variant='body2' sx={{ mb: 2 }}>Loading system data...</Typography>
472+
<CircularProgress size={30} />
473+
</Box>
474+
</Box>
475+
);
476+
}
477+
403478
return (
404479
<Grid container gap={0} sx={{ display: 'flex', width: '100%', justifyContent: 'center' }}>
405480
<div
@@ -413,6 +488,7 @@ export const SystemMonitorWidget = ({ config }: SystemMonitorWidgetProps) => {
413488
<IoInformationCircleOutline style={{ color: theme.palette.text.primary, fontSize: '1.5rem' }}/>
414489
</IconButton>
415490
</div>
491+
416492
<Grid container
417493
gap={gapSize}
418494
sx={containerStyles}

0 commit comments

Comments
 (0)