spectator login for exties

This commit is contained in:
legop3
2026-07-14 20:32:25 -04:00
parent 7fe5730953
commit 51fbee400c
5 changed files with 66 additions and 23 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -78,7 +78,7 @@
<script defer src="https://analytics.otter.land/script.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land"></script> <script defer src="https://analytics.otter.land/script.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land"></script>
<script defer src="https://analytics.otter.land/recorder.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land" data-sample-rate="0.15" data-mask-level="moderate" data-max-duration="300000"></script> <script defer src="https://analytics.otter.land/recorder.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land" data-sample-rate="0.15" data-mask-level="moderate" data-max-duration="300000"></script>
<title>Roomba Rover</title> <title>Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-CmNL6XEv.js"></script> <script type="module" crossorigin src="/assets/index-D5SKLi1e.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-sm_EKOxJ.css"> <link rel="stylesheet" crossorigin href="/assets/index-sm_EKOxJ.css">
</head> </head>
<body> <body>
+38 -8
View File
@@ -14,35 +14,64 @@ const PRIVILEGED_ROLES = new Set(['admin', 'lockdown']);
const LOCKDOWN_ROLES = new Set(['lockdown']); const LOCKDOWN_ROLES = new Set(['lockdown']);
const RESTRICTED_MODES = new Set(['admin', 'lockdown']); const RESTRICTED_MODES = new Set(['admin', 'lockdown']);
function getModeDetails(mode = 'admin') { function getModeDetails({ mode = 'admin', spectatorAccessBlocked = false, spectatorAccessMode = 'on' } = {}) {
if (spectatorAccessBlocked) {
/*
External spectator access is not a server mode like admin/lockdown; it is
a route-specific bandwidth/access gate. It still needs the same overlay
because the AuthPanel is the only browser-side way for an admin to prove
they should bypass that gate from /spectate.
*/
return {
title: 'Spectate access required',
description: spectatorAccessMode === 'admin'
? 'External spectators need admin approval or an admin login before viewing this page.'
: 'External spectator access is disabled. Admins can log in to continue.',
reasonLabel: 'Access state:',
reason: spectatorAccessMode === 'admin' ? 'Waiting for approved spectator identity or admin login.' : 'External spectating is off.',
chatIntro: 'You can still use the chat while access is blocked:',
};
}
if (mode === 'lockdown') { if (mode === 'lockdown') {
return { return {
title: 'Lockdown mode active', title: 'Lockdown mode active',
description: description:
'Only the server owners can access the interface at this time.', 'Only the server owners can access the interface at this time.',
reasonLabel: 'Reason for locking:',
chatIntro: 'You can still use the chat while the server is locked:',
}; };
} }
return { return {
title: 'Admin mode active', title: 'Admin mode active',
// description: // description:
// 'The server is currently in admin mode. Only admins can access the interface.', // 'The server is currently in admin mode. Only admins can access the interface.',
reasonLabel: 'Reason for locking:',
chatIntro: 'You can still use the chat while the server is locked:',
}; };
} }
export default function ModeGateOverlay() { export default function ModeGateOverlay({ includeSpectatorAccessGate = false }) {
const mode = useSessionSelector((state) => state.session?.mode || null); const mode = useSessionSelector((state) => state.session?.mode || null);
const role = useSessionSelector((state) => state.session?.role || null); const role = useSessionSelector((state) => state.session?.role || null);
const reason = useSessionSelector((state) => state.session?.adminReason?.text || ''); const reason = useSessionSelector((state) => state.session?.adminReason?.text || '');
const timezone = useSessionSelector((state) => state.session?.timezone || 'UTC'); const timezone = useSessionSelector((state) => state.session?.timezone || 'UTC');
const interInstanceEnabled = useSessionSelector((state) => isFeatureEnabled(state, 'interInstance')); const interInstanceEnabled = useSessionSelector((state) => isFeatureEnabled(state, 'interInstance'));
const spectatorAccessAllowed = useSessionSelector(
(state) => state.session?.bandwidthSavings?.canUseExternalSpectatorAccess,
);
const spectatorAccessMode = useSessionSelector(
(state) => state.session?.bandwidthSavings?.externalSpectatorAccess || 'on',
);
const restricted = RESTRICTED_MODES.has(mode); const restricted = RESTRICTED_MODES.has(mode);
const privileged = mode === 'lockdown' ? LOCKDOWN_ROLES.has(role) : PRIVILEGED_ROLES.has(role); const privileged = mode === 'lockdown' ? LOCKDOWN_ROLES.has(role) : PRIVILEGED_ROLES.has(role);
const spectatorAccessBlocked = Boolean(includeSpectatorAccessGate && spectatorAccessAllowed === false);
const blocked = (restricted && !privileged) || spectatorAccessBlocked;
/* /*
The overlay is mounted for the whole app, but the server-time display is The overlay is mounted for the whole app, but the server-time display is
only visible while access is actually blocked. Gating the shared clock here only visible while access is actually blocked. Gating the shared clock here
prevents the hidden overlay from registering a permanent interval. prevents the hidden overlay from registering a permanent interval.
*/ */
const nowMs = useSharedClock(1000, restricted && !privileged); const nowMs = useSharedClock(1000, blocked);
const serverTime = useMemo(() => { const serverTime = useMemo(() => {
const now = new Date(nowMs); const now = new Date(nowMs);
@@ -58,11 +87,12 @@ export default function ModeGateOverlay() {
} }
}, [nowMs, timezone]); }, [nowMs, timezone]);
if (!restricted || privileged) { if (!blocked) {
return null; return null;
} }
const details = getModeDetails(mode); const details = getModeDetails({ mode, spectatorAccessBlocked, spectatorAccessMode });
const displayedReason = spectatorAccessBlocked ? details.reason : reason || 'No reason set.';
return ( return (
<div className="pointer-events-auto fixed inset-0 z-50 overflow-y-auto bg-black px-0.5 py-0.5"> <div className="pointer-events-auto fixed inset-0 z-50 overflow-y-auto bg-black px-0.5 py-0.5">
@@ -73,9 +103,9 @@ export default function ModeGateOverlay() {
<p className="text-sm text-slate-300">{details.description}</p> <p className="text-sm text-slate-300">{details.description}</p>
</div> </div>
<div className="surface-muted space-y-0.5"> <div className="surface-muted space-y-0.5">
<p className="text-[0.7rem] tracking-wide text-slate-400">Reason for locking:</p> <p className="text-[0.7rem] tracking-wide text-slate-400">{details.reasonLabel}</p>
<p className="text-lg font-semibold text-slate-100"> <p className="text-lg font-semibold text-slate-100">
{reason ? reason : 'No reason set.'} {displayedReason}
</p> </p>
<p className="text-center text-sm text-slate-300">Server time: {serverTime}</p> <p className="text-center text-sm text-slate-300">Server time: {serverTime}</p>
</div> </div>
@@ -83,7 +113,7 @@ export default function ModeGateOverlay() {
<AuthPanel /> <AuthPanel />
</div> </div>
<SocialButton id="discord" label="Join our Discord server for updates!" /> <SocialButton id="discord" label="Join our Discord server for updates!" />
You can still use the chat while the server is locked: {details.chatIntro}
{/* set max height of this box */} {/* set max height of this box */}
<div className='max-h-80 overflow-y-auto'> <div className='max-h-80 overflow-y-auto'>
<ChatPanel nicknameLayout="stacked" /> <ChatPanel nicknameLayout="stacked" />
@@ -1,8 +1,21 @@
// Spectator App Root // Spectator App Root
// Purpose: Defines the Spectator App Root module and the local helpers/components used in this file. // Purpose: Defines the Spectator App Root module and the local helpers/components used in this file.
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit. // Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
import ModeGateOverlay from '../../components/ModeGateOverlay/index.jsx';
import SpectatorContent from './SpectatorContent.jsx'; import SpectatorContent from './SpectatorContent.jsx';
export default function SpectatorAppRoot() { export default function SpectatorAppRoot() {
return <SpectatorContent />; return (
<>
<SpectatorContent />
{/*
The spectator route does not render App.jsx, so it must mount the gate
overlay itself. This keeps admin/lockdown login behavior available on
/spectate and, more importantly, gives external spectators a real
AuthPanel when the bandwidth policy requires admin approval or admin
login before spectating.
*/}
<ModeGateOverlay includeSpectatorAccessGate />
</>
);
} }