Skip to content

Commit dd1b63c

Browse files
committed
update docs, fix deluge login failure loop, check health every 2 min, fix health check false positives, update explanation for SECRET in docker compose
1 parent 536b857 commit dd1b63c

File tree

11 files changed

+89
-71
lines changed

11 files changed

+89
-71
lines changed

README.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,22 @@ This is an open-source user interface designed to be your internally/externally
55

66
# Features
77
Lab Dash features a customizable grid layout where you can add various widgets:
8-
- Links to your tools/services
8+
- Shortcuts to your tools/services
99
- System information
1010
- Service health checks
1111
- Custom widgets and more
1212

1313
### Customization
1414
You can easily customize your dashboard by:
15-
- Dragging and reordering widgets
15+
- Dragging and reordering
1616
- Changing the background image
1717
- Adding custom search providers
18-
- Importing/exporting configurations
18+
- Custom title and tab name
1919

2020
### Privacy & Data Control
2121
You have complete control over your data and dashboard configuration.
22-
- All data is stored locally on your own server
22+
- All data is stored & used on your own device
23+
- Sensitive data is encrypted locally using [AES-256-CBC](https://docs.anchormydata.com/docs/what-is-aes-256-cbc)
2324
- Only administrator accounts can make changes
2425
- Configurations can be easily backed up and restored
2526

@@ -47,9 +48,12 @@ services:
4748
```
4849
4950
# Usage
50-
Lab Dash can aslo be accessed from any web browser via `http://localhost:2022` or `192.168.x.x:2022` which should be your servers local IP address or yout hosted url `www.your-homepage.com`.
51+
Lab Dash can aslo be accessed from any web browser via
52+
- `http://localhost:2022` on the device running the container
53+
- `192.168.x.x:2022` on local network
54+
- `www.your-homepage.com` using your custom domain name
5155

52-
Lab Dash can aslo be installed as an app on your computer/phone as a PWA (Progressive Web App):
56+
Lab Dash can also be installed as an app on your computer/phone as a PWA (Progressive Web App):
5357
- Using Google Chrome on Mac/Windows/Android/Linux
5458
- Using Safari on iOS/iPad OS via the share menu > add to homscreen
5559

backend/src/routes/deluge.route.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,6 @@ process.on('SIGINT', async () => {
153153

154154
delugeRoute.post('/login', async (req: Request, res: Response) => {
155155
try {
156-
const { username } = req.body;
157156
let { password } = req.body;
158157
const baseUrl = getBaseUrl(req);
159158
const host = req.query.host as string;
@@ -285,8 +284,8 @@ delugeRoute.get('/stats', async (req: Request, res: Response) => {
285284
ssl
286285
};
287286
}
288-
} catch (loginError) {
289-
console.error('Deluge login attempt failed:', loginError);
287+
} catch (loginError: any) {
288+
console.error('Deluge login attempt failed:', loginError.message);
290289
// Continue without cookie - will return default stats
291290
}
292291
}
@@ -457,8 +456,8 @@ delugeRoute.get('/torrents', async (req: Request, res: Response) => {
457456
ssl
458457
};
459458
}
460-
} catch (loginError) {
461-
console.error('Deluge login attempt failed:', loginError);
459+
} catch (loginError: any) {
460+
console.error('Deluge login attempt failed:', loginError.message);
462461
// Continue without cookie - will return empty array
463462
}
464463
}
@@ -600,8 +599,8 @@ delugeRoute.post('/torrents/resume', authenticateToken, async (req: Request, res
600599
ssl
601600
};
602601
}
603-
} catch (loginError) {
604-
console.error('Deluge login attempt failed:', loginError);
602+
} catch (loginError: any) {
603+
console.error('Deluge login attempt failed:', loginError.message);
605604
// Continue with existing cookie if available, otherwise will return an error below
606605
}
607606
}
@@ -694,8 +693,8 @@ delugeRoute.post('/torrents/pause', authenticateToken, async (req: Request, res:
694693
ssl
695694
};
696695
}
697-
} catch (loginError) {
698-
console.error('Deluge login attempt failed:', loginError);
696+
} catch (loginError: any) {
697+
console.error('Deluge login attempt failed:', loginError.message);
699698
// Continue with existing cookie if available, otherwise will return an error below
700699
}
701700
}
@@ -788,8 +787,8 @@ delugeRoute.post('/torrents/delete', authenticateToken, async (req: Request, res
788787
ssl
789788
};
790789
}
791-
} catch (loginError) {
792-
console.error('Deluge login attempt failed:', loginError);
790+
} catch (loginError: any) {
791+
console.error('Deluge login attempt failed:', loginError.message);
793792
// Continue with existing cookie if available, otherwise will return an error below
794793
}
795794
}

backend/src/routes/health.route.ts

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -46,23 +46,16 @@ healthRoute.get('/', async (req: Request, res: Response): Promise<void> => {
4646
}
4747

4848
// If axios doesn't return valid status, try ping as fallback
49-
try {
50-
const parsedUrl = new URL(url);
51-
const isReachable = await pingHost(parsedUrl.hostname);
52-
53-
res.json({ status: isReachable ? 'online' : 'offline' });
54-
} catch (pingError) {
55-
res.json({ status: 'offline' });
56-
}
49+
// try {
50+
// const parsedUrl = new URL(url);
51+
// const isReachable = await pingHost(parsedUrl.hostname);
52+
53+
// res.json({ status: isReachable ? 'online' : 'offline' });
54+
// } catch (pingError) {
55+
// res.json({ status: 'offline' });
56+
// }
5757
} catch (error) {
58-
// If axios completely fails, try ping as fallback
59-
try {
60-
const parsedUrl = new URL(url);
61-
const isReachable = await pingHost(parsedUrl.hostname);
62-
63-
res.json({ status: isReachable ? 'online' : 'offline' });
64-
} catch (pingError) {
65-
res.json({ status: 'offline' });
66-
}
58+
console.log('service is offline', req.query.url);
59+
res.json({ status: 'offline' });
6760
}
6861
});

backend/src/utils/crypto.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,6 @@ export function decrypt(encryptedText: string): string {
5959
} catch (error) {
6060
console.error('Decryption error:', error);
6161
console.warn('Decryption failed - possibly encrypted with a different key');
62-
63-
// Return empty string instead of encrypted text to avoid sending invalid credentials
6462
return '';
6563
}
6664
}

docker-compose.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ services:
44
container_name: lab-dash
55
image: ghcr.io/anthonygress/lab-dash:latest
66
privileged: true
7-
network_mode: host
7+
network_mode: host # for monitoring network usage stats
88
ports:
99
- 2022:2022
1010
environment:
11-
- SECRET=YOUR_SECRET_KEY # any random string for jwt encryption
11+
- SECRET=YOUR_SECRET_KEY # any random string for used for encryption.
12+
# You can run `openssl rand -base64 32` to generate a key
1213
volumes:
1314
- /sys:/sys:ro
1415
- /docker/lab-dash/config:/config

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

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const DelugeWidget = (props: { config?: DelugeWidgetConfig }) => {
2020
const [authError, setAuthError] = useState('');
2121
const [stats, setStats] = useState<any>(null);
2222
const [torrents, setTorrents] = useState<any[]>([]);
23+
const [loginAttemptFailed, setLoginAttemptFailed] = useState(false);
2324
const [loginCredentials, setLoginCredentials] = useState({
2425
host: config?.host || 'localhost',
2526
port: config?.port || '8112',
@@ -38,6 +39,8 @@ export const DelugeWidget = (props: { config?: DelugeWidgetConfig }) => {
3839
username: config.username || '',
3940
password: config.password || ''
4041
});
42+
// Reset failed flag when credentials are updated
43+
setLoginAttemptFailed(false);
4144
}
4245
}, [config]);
4346

@@ -49,6 +52,9 @@ export const DelugeWidget = (props: { config?: DelugeWidgetConfig }) => {
4952
setIsAuthenticated(success);
5053
if (!success) {
5154
setAuthError('Login failed. Check your credentials and connection.');
55+
setLoginAttemptFailed(true);
56+
} else {
57+
setLoginAttemptFailed(false);
5258
}
5359
} catch (error: any) {
5460
console.error('Login error:', error);
@@ -59,12 +65,15 @@ export const DelugeWidget = (props: { config?: DelugeWidgetConfig }) => {
5965
setAuthError('Connection error. Check your Deluge WebUI settings.');
6066
}
6167
setIsAuthenticated(false);
68+
setLoginAttemptFailed(true);
6269
} finally {
6370
setIsLoading(false);
6471
}
6572
}, [loginCredentials]);
6673

6774
const fetchStats = useCallback(async () => {
75+
if (loginAttemptFailed || !isAuthenticated) return;
76+
6877
try {
6978
const connectionInfo = {
7079
host: loginCredentials.host,
@@ -91,9 +100,11 @@ export const DelugeWidget = (props: { config?: DelugeWidgetConfig }) => {
91100
setAuthError('Session expired. Please login again.');
92101
}
93102
}
94-
}, [loginCredentials]);
103+
}, [loginCredentials, isAuthenticated, loginAttemptFailed]);
95104

96105
const fetchTorrents = useCallback(async () => {
106+
if (loginAttemptFailed || !isAuthenticated) return;
107+
97108
try {
98109
const connectionInfo = {
99110
host: loginCredentials.host,
@@ -126,7 +137,7 @@ export const DelugeWidget = (props: { config?: DelugeWidgetConfig }) => {
126137
setAuthError('Session expired. Please login again.');
127138
}
128139
}
129-
}, [loginCredentials, config?.maxDisplayedTorrents]);
140+
}, [loginCredentials, config?.maxDisplayedTorrents, isAuthenticated, loginAttemptFailed]);
130141

131142
// Handle input changes
132143
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -135,29 +146,33 @@ export const DelugeWidget = (props: { config?: DelugeWidgetConfig }) => {
135146
...prev,
136147
[name]: type === 'checkbox' ? checked : value
137148
}));
149+
// Reset failed flag when credentials are changed manually
150+
setLoginAttemptFailed(false);
138151
};
139152

140-
// Auto-login when username and password are available
153+
// Auto-login when username and password are available and no previous login attempt failed
141154
useEffect(() => {
142-
if (config?.username && config?.password) {
155+
if (config?.username && config?.password && !loginAttemptFailed && !isAuthenticated) {
143156
handleLogin();
144157
}
145-
}, [config, handleLogin]);
158+
}, [config, handleLogin, loginAttemptFailed, isAuthenticated]);
146159

147160
// Refresh stats and torrents periodically
148161
useEffect(() => {
149-
// Refresh both stats and torrents when the component mounts
150-
fetchStats();
151-
fetchTorrents();
152-
153-
// Set up periodic refresh
154-
const interval = setInterval(() => {
162+
// Only fetch data if authenticated
163+
if (isAuthenticated) {
155164
fetchStats();
156165
fetchTorrents();
157-
}, 5000); // Fixed interval of 5000ms as specified
158166

159-
return () => clearInterval(interval);
160-
}, [fetchStats, fetchTorrents]);
167+
// Set up periodic refresh
168+
const interval = setInterval(() => {
169+
fetchStats();
170+
fetchTorrents();
171+
}, 5000); // Fixed interval of 5000ms as specified
172+
173+
return () => clearInterval(interval);
174+
}
175+
}, [fetchStats, fetchTorrents, isAuthenticated]);
161176

162177
// Torrent actions
163178
const handleResumeTorrent = useCallback(async (hash: string) => {

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

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ export const QBittorrentWidget = (props: { config?: QBittorrentWidgetConfig }) =
6666
}, [loginCredentials]);
6767

6868
const fetchStats = useCallback(async () => {
69+
if (!isAuthenticated) return;
70+
6971
try {
7072
const connectionInfo = {
7173
host: loginCredentials.host,
@@ -92,9 +94,11 @@ export const QBittorrentWidget = (props: { config?: QBittorrentWidgetConfig }) =
9294
setAuthError('Session expired. Please login again.');
9395
}
9496
}
95-
}, [loginCredentials]);
97+
}, [loginCredentials, isAuthenticated]);
9698

9799
const fetchTorrents = useCallback(async () => {
100+
if (!isAuthenticated) return;
101+
98102
try {
99103
const connectionInfo = {
100104
host: loginCredentials.host,
@@ -136,7 +140,7 @@ export const QBittorrentWidget = (props: { config?: QBittorrentWidgetConfig }) =
136140
setAuthError('Session expired. Please login again.');
137141
}
138142
}
139-
}, [loginCredentials, config?.maxDisplayedTorrents]);
143+
}, [loginCredentials, config?.maxDisplayedTorrents, isAuthenticated]);
140144

141145
// Handle input changes
142146
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -147,27 +151,29 @@ export const QBittorrentWidget = (props: { config?: QBittorrentWidgetConfig }) =
147151
}));
148152
};
149153

150-
// Auto-login when username and password are available
154+
// Auto-login when username and password are available and not authenticated
151155
useEffect(() => {
152-
if (config?.username && config?.password) {
156+
if (config?.username && config?.password && !isAuthenticated) {
153157
handleLogin();
154158
}
155-
}, [config, handleLogin]);
159+
}, [config, handleLogin, isAuthenticated]);
156160

157161
// Refresh stats and torrents periodically
158162
useEffect(() => {
159-
// Refresh both stats and torrents when the component mounts
160-
fetchStats();
161-
fetchTorrents();
162-
163-
// Set up periodic refresh
164-
const interval = setInterval(() => {
163+
// Only fetch data if authenticated
164+
if (isAuthenticated) {
165165
fetchStats();
166166
fetchTorrents();
167-
}, 5000); // Fixed interval of 5000ms as specified
168167

169-
return () => clearInterval(interval);
170-
}, [fetchStats, fetchTorrents]);
168+
// Set up periodic refresh
169+
const interval = setInterval(() => {
170+
fetchStats();
171+
fetchTorrents();
172+
}, 5000); // Fixed interval of 5000ms as specified
173+
174+
return () => clearInterval(interval);
175+
}
176+
}, [fetchStats, fetchTorrents, isAuthenticated]);
171177

172178
// Torrent actions
173179
const handleStartTorrent = useCallback(async (hash: string) => {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -630,7 +630,7 @@ export const TorrentClientWidget: React.FC<TorrentClientWidgetProps> = ({
630630
bottom: 0,
631631
zIndex: 1
632632
}}>
633-
No active torrents
633+
No active items
634634
</Box>
635635
{/* Hidden sample torrent item to maintain width - but not visible */}
636636
<Box sx={{
@@ -660,7 +660,7 @@ export const TorrentClientWidget: React.FC<TorrentClientWidgetProps> = ({
660660
fontSize: isMobile ? '0.7rem' : '.8rem'
661661
}}
662662
>
663-
Sample Torrent Name.mkv
663+
Sample File Name.mkv
664664
</Typography>
665665
<Typography
666666
variant='caption'

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ export const WidgetContainer: React.FC<Props> = ({ children, editMode, onEdit, o
2323
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
2424
const open = Boolean(anchorEl);
2525

26-
const isOnline = useServiceStatus(url);
26+
const isUrl = url && isValidHttpUrl(url);
27+
const isOnline = useServiceStatus(isUrl ? url : null);
2728

2829
let dotColor = 'gray';
2930
let tooltipText = 'Unknown';

frontend/src/constants/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ export const initialItems = [
1414
export const BACKEND_URL = import.meta.env.PROD ? '' : 'http://localhost:5000';
1515

1616
export const FIFTEEN_MIN_IN_MS = 900000;
17+
export const TWO_MIN_IN_MS = 120000;
1718
// export const BACKEND_URL = 'http://localhost:5000';

0 commit comments

Comments
 (0)