Compare commits

...
2 Commits
Author SHA1 Message Date
legop3 c94e1a0508 change request to switch
Container image / image (push) Failing after 59s
2026-10-09 18:44:50 -04:00
legop3 abcb0871b2 allowed to leave is now used in the web UI to show and hide request / open buttons and trigger popup when leaving the page 2026-10-09 18:30:10 -04:00
4 changed files with 44 additions and 10 deletions
+32 -3
View File
@@ -24,8 +24,34 @@ export default function PageExitGuard() {
event.preventDefault();
event.returnValue = true;
}
function blockNavigation(event) {
event.preventDefault();
event.stopPropagation();
setWarning({ blocked: true, visible: true });
}
function handleLinkClick(event) {
if (event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
const link = event.target instanceof Element ? event.target.closest('a[href]') : null;
if (!link || link.hasAttribute('download')) return;
const target = (link.getAttribute('target') || document.querySelector('base[target]')?.getAttribute('target') || '_self').toLowerCase();
if (target !== '_self') return;
const destination = new URL(link.href, window.location.href);
if (destination.protocol !== 'http:' && destination.protocol !== 'https:') return;
// Hash links stay in this document and do not abandon the rover.
if (destination.origin === window.location.origin
&& destination.pathname === window.location.pathname
&& destination.search === window.location.search
&& link.getAttribute('href').includes('#')) return;
blockNavigation(event);
}
window.addEventListener('beforeunload', handleBeforeUnload);
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
document.addEventListener('click', handleLinkClick, true);
window.addEventListener('page:navigate', blockNavigation);
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
document.removeEventListener('click', handleLinkClick, true);
window.removeEventListener('page:navigate', blockNavigation);
};
}, [shouldBlock]);
useEffect(() => {
@@ -46,18 +72,21 @@ export default function PageExitGuard() {
role="dialog"
aria-modal="true"
aria-labelledby="page-exit-warning"
className="fixed inset-0 z-[2147483647] flex flex-col items-center justify-center gap-10 bg-neutral-950 p-6 text-center text-white"
className="fixed inset-0 z-[2147483647] flex flex-col items-center justify-between gap-4 bg-black p-6 text-center text-white"
onKeyDown={(event) => {
event.stopPropagation();
if (event.key === 'Escape') dismiss();
}}
>
<p id="page-exit-warning" className="text-[clamp(3rem,10vw,10rem)] font-bold leading-tight">
Please dock your rover
Please dock your rover!
</p>
<button type="button" autoFocus className="button-dark px-8 py-4 text-3xl" onClick={dismiss}>
Okay
</button>
<p aria-hidden="true" className="text-[clamp(3rem,10vw,10rem)] font-bold leading-tight">
Please dock your rover!
</p>
</div>,
document.body,
);
+4 -3
View File
@@ -21,6 +21,7 @@ export default function PtzQueueCard() {
const featureEnabled = useSessionSelector((state) => isFeatureEnabled(state, 'ptzCamera'));
const ptz = useSessionSelector((state) => state.session?.ptzCamera || null);
const selfId = useSessionSelector((state) => state.session?.socketId || null);
const canLeaveRover = useSessionSelector((state) => state.session?.canLeaveRover !== false);
const { setOperatingMode } = useSessionActions();
const navigate = useNavigate();
const now = useSharedClock(1000, Boolean(ptz?.turn?.deadline));
@@ -32,7 +33,7 @@ export default function PtzQueueCard() {
if (!featureEnabled) return null;
const handleRequest = async () => {
if (!canUse || pending) return;
if (!canUse || !canLeaveRover || pending) return;
if (isParticipant) {
navigate('/ptz');
return;
@@ -76,14 +77,14 @@ export default function PtzQueueCard() {
}}
turn={ptz?.turn}
selfId={selfId}
canClick={canUse && !pending}
canClick={canUse && canLeaveRover && !pending}
pending={pending}
buttonLabel={actionLabel}
batteryLabel={ptz?.turn?.ownsControl ? 'LIVE' : ptz?.turn?.turnsAhead ? `#${ptz.turn.turnsAhead}` : '--'}
batteryClassName={ptz?.turn?.ownsControl ? 'text-emerald-300' : ptz?.turn?.turnsAhead ? 'text-sky-300' : 'text-slate-400'}
timerLabel={timerLabel}
onRequest={handleRequest}
showAction={canUse}
showAction={canUse && canLeaveRover}
/>
</ul>
{isParticipant ? (
@@ -101,6 +101,7 @@ export default function RoverQueuesPanel({
fillHeight = false,
}) {
const role = useSessionSelector((state) => state.session?.role || null);
const canLeaveRover = useSessionSelector((state) => state.session?.canLeaveRover !== false);
const localRoster = useSessionSelector((state) => state.session?.roster ?? []);
const interInstanceEnabled = useSessionSelector((state) => isFeatureEnabled(state, 'interInstance'));
const { value: pageSettings } = useSettingsNamespace('page', { interInstanceTransferSettings: true });
@@ -122,8 +123,8 @@ export default function RoverQueuesPanel({
const roster = Array.isArray(rosterOverride) ? rosterOverride : localRoster;
const canRequest = useMemo(
() => (externalMode ? !externalBlocked : role && role !== 'spectator'),
[externalBlocked, externalMode, role],
() => canLeaveRover && (externalMode ? !externalBlocked : role && role !== 'spectator'),
[canLeaveRover, externalBlocked, externalMode, role],
);
const adminCapable = useMemo(
() => role === 'admin' || role === 'lockdown',
@@ -142,7 +143,7 @@ export default function RoverQueuesPanel({
async function handleRequest(targetRoverId) {
if (!targetRoverId) return;
if (externalBlocked) return;
if (!canRequest) return;
if (externalMode) {
/*
External queue cards deliberately reuse the local row layout, but their
@@ -252,7 +253,7 @@ export default function RoverQueuesPanel({
? lockLabel
: externalMode
? 'Open'
: 'request';
: 'Switch';
const canClickRow = canRequest && !lockedBlocked && !pending[roverId];
return (
<QueueTargetRow
+3
View File
@@ -40,5 +40,8 @@ export function buildExternalRoverUrl(instance, roverId, { includeSettings = fal
export function openExternalRover(instance, roverId, { includeSettings = true } = {}) {
const targetUrl = buildExternalRoverUrl(instance, roverId, { includeSettings });
if (!targetUrl) return;
// These queue actions are buttons, so let the page guard veto navigation
// before beforeunload would pause painting behind the native browser dialog.
if (!window.dispatchEvent(new Event('page:navigate', { cancelable: true }))) return;
window.location.href = targetUrl;
}