Add device-admin iframe for reboot/shutdown management in SystemSettings#245
Add device-admin iframe for reboot/shutdown management in SystemSettings#245
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates the SystemSettings admin panel to replace non-functional Raspberry Pi reboot/shutdown buttons with a device-admin embedded iframe (and a direct-link fallback) for system power management.
Changes:
- Adds a device-admin “System Management” section with an embedded iframe for reboot/shutdown.
- Adds a fallback UI that links out to device-admin in a new tab when the iframe is unavailable.
- Removes the previous Raspberry Pi reboot/shutdown toggle + buttons.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const [deviceAdminUrl] = useState( | ||
| () => `http://${hostIP}/admin/panel/boot/?mode=minimal&nav=hidden`, | ||
| ); |
There was a problem hiding this comment.
hostIP from ConnectionSettingsSlice already includes the protocol prefix (e.g. http://hostname). Building deviceAdminUrl as http://${hostIP}/... produces an invalid URL like http://http://hostname/.... Build this URL from hostIP directly (or via new URL('/admin/panel/boot/', hostIP)) and avoid hardcoding the scheme so https setups work too.
| const [deviceAdminUrl] = useState( | |
| () => `http://${hostIP}/admin/panel/boot/?mode=minimal&nav=hidden`, | |
| ); | |
| const [deviceAdminUrl] = useState(() => { | |
| const url = new URL("/admin/panel/boot/", hostIP); | |
| url.searchParams.set("mode", "minimal"); | |
| url.searchParams.set("nav", "hidden"); | |
| return url.toString(); | |
| }); |
| const [deviceAdminUrl] = useState( | ||
| () => `http://${hostIP}/admin/panel/boot/?mode=minimal&nav=hidden`, | ||
| ); | ||
| const [deviceAdminLoaded, setDeviceAdminLoaded] = useState(false); |
There was a problem hiding this comment.
deviceAdminUrl is stored in state with an initializer, so it will not update if the connection settings (hostIP) change after first render. Since this value is derived from Redux state, compute it from hostIP on each render (or use useMemo with [hostIP]) instead of useState.
| {deviceAdminLoaded ? ( | ||
| <Box | ||
| sx={{ | ||
| border: "1px solid #e0e0e0", | ||
| borderRadius: 1, | ||
| overflow: "hidden", | ||
| mb: 2, | ||
| }} | ||
| > | ||
| <iframe | ||
| src={deviceAdminUrl} | ||
| style={{ | ||
| width: "100%", | ||
| height: "300px", | ||
| border: "none", | ||
| borderRadius: "4px", | ||
| }} | ||
| title="Device Admin Panel - Reboot/Shutdown" | ||
| onLoad={() => setDeviceAdminLoaded(true)} | ||
| onError={() => setDeviceAdminLoaded(false)} | ||
| sandbox="allow-same-origin allow-scripts allow-forms allow-popups" | ||
| /> | ||
| </Box> | ||
| ) : ( |
There was a problem hiding this comment.
The iframe is only rendered when deviceAdminLoaded is already true, but deviceAdminLoaded starts as false and is only set to true by the iframe’s onLoad. As a result, the iframe can never load and the UI will stay stuck on the fallback link. Render the iframe initially (e.g., always render it and show a loading/fallback overlay, or use a separate deviceAdminError flag).
| {deviceAdminLoaded ? ( | |
| <Box | |
| sx={{ | |
| border: "1px solid #e0e0e0", | |
| borderRadius: 1, | |
| overflow: "hidden", | |
| mb: 2, | |
| }} | |
| > | |
| <iframe | |
| src={deviceAdminUrl} | |
| style={{ | |
| width: "100%", | |
| height: "300px", | |
| border: "none", | |
| borderRadius: "4px", | |
| }} | |
| title="Device Admin Panel - Reboot/Shutdown" | |
| onLoad={() => setDeviceAdminLoaded(true)} | |
| onError={() => setDeviceAdminLoaded(false)} | |
| sandbox="allow-same-origin allow-scripts allow-forms allow-popups" | |
| /> | |
| </Box> | |
| ) : ( | |
| <Box | |
| sx={{ | |
| border: "1px solid #e0e0e0", | |
| borderRadius: 1, | |
| overflow: "hidden", | |
| mb: 2, | |
| }} | |
| > | |
| <iframe | |
| src={deviceAdminUrl} | |
| style={{ | |
| width: "100%", | |
| height: "300px", | |
| border: "none", | |
| borderRadius: "4px", | |
| }} | |
| title="Device Admin Panel - Reboot/Shutdown" | |
| onLoad={() => setDeviceAdminLoaded(true)} | |
| onError={() => setDeviceAdminLoaded(false)} | |
| sandbox="allow-same-origin allow-scripts allow-forms allow-popups" | |
| /> | |
| </Box> | |
| {!deviceAdminLoaded && ( |
| title="Device Admin Panel - Reboot/Shutdown" | ||
| onLoad={() => setDeviceAdminLoaded(true)} | ||
| onError={() => setDeviceAdminLoaded(false)} | ||
| sandbox="allow-same-origin allow-scripts allow-forms allow-popups" | ||
| /> | ||
| </Box> | ||
| ) : ( | ||
| <Box | ||
| sx={{ | ||
| border: "1px solid #424242", | ||
| borderRadius: 1, | ||
| p: 2, | ||
| mb: 2, | ||
| backgroundColor: "#2a2a2a", | ||
| display: "flex", | ||
| flexDirection: "column", | ||
| gap: 1, | ||
| }} | ||
| > | ||
| <Typography variant="body2" sx={{ color: "#b0b0b0", mb: 1 }}> | ||
| Device admin panel not available. Please use the direct link: | ||
| </Typography> | ||
| <Link | ||
| href={deviceAdminUrl} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| sx={{ | ||
| display: "flex", | ||
| alignItems: "center", | ||
| gap: 0.5, | ||
| color: "#64b5f6", | ||
| "&:hover": { color: "#90caf9" }, | ||
| }} | ||
| > | ||
| Reboot/Shutdown in device-admin <OpenInNew fontSize="small" /> | ||
| </Link> | ||
| </Box> | ||
| )} |
There was a problem hiding this comment.
Because the conditional unmounts the iframe when deviceAdminLoaded flips to false, any transient load error (or a later navigation error) will permanently remove the iframe and there’s no retry path besides a full rerender. Consider keeping the iframe mounted and toggling visibility, and provide an explicit “Retry” action that resets the error/loading state.
Add an iframe instead of the not working reboot buttons to the admin panel.