appoint spectator access when you login from external, actually...

This commit is contained in:
legop3
2026-07-14 20:42:37 -04:00
parent 51fbee400c
commit ad7de34d6d
6 changed files with 74 additions and 66 deletions
+8 -38
View File
@@ -14,64 +14,35 @@ const PRIVILEGED_ROLES = new Set(['admin', 'lockdown']);
const LOCKDOWN_ROLES = new Set(['lockdown']);
const RESTRICTED_MODES = new Set(['admin', 'lockdown']);
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:',
};
}
function getModeDetails(mode = 'admin') {
if (mode === 'lockdown') {
return {
title: 'Lockdown mode active',
description:
'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 {
title: 'Admin mode active',
// description:
// '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({ includeSpectatorAccessGate = false }) {
export default function ModeGateOverlay() {
const mode = useSessionSelector((state) => state.session?.mode || null);
const role = useSessionSelector((state) => state.session?.role || null);
const reason = useSessionSelector((state) => state.session?.adminReason?.text || '');
const timezone = useSessionSelector((state) => state.session?.timezone || 'UTC');
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 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
only visible while access is actually blocked. Gating the shared clock here
prevents the hidden overlay from registering a permanent interval.
*/
const nowMs = useSharedClock(1000, blocked);
const nowMs = useSharedClock(1000, restricted && !privileged);
const serverTime = useMemo(() => {
const now = new Date(nowMs);
@@ -87,12 +58,11 @@ export default function ModeGateOverlay({ includeSpectatorAccessGate = false })
}
}, [nowMs, timezone]);
if (!blocked) {
if (!restricted || privileged) {
return null;
}
const details = getModeDetails({ mode, spectatorAccessBlocked, spectatorAccessMode });
const displayedReason = spectatorAccessBlocked ? details.reason : reason || 'No reason set.';
const details = getModeDetails(mode);
return (
<div className="pointer-events-auto fixed inset-0 z-50 overflow-y-auto bg-black px-0.5 py-0.5">
@@ -103,9 +73,9 @@ export default function ModeGateOverlay({ includeSpectatorAccessGate = false })
<p className="text-sm text-slate-300">{details.description}</p>
</div>
<div className="surface-muted space-y-0.5">
<p className="text-[0.7rem] tracking-wide text-slate-400">{details.reasonLabel}</p>
<p className="text-[0.7rem] tracking-wide text-slate-400">Reason for locking:</p>
<p className="text-lg font-semibold text-slate-100">
{displayedReason}
{reason ? reason : 'No reason set.'}
</p>
<p className="text-center text-sm text-slate-300">Server time: {serverTime}</p>
</div>
@@ -113,7 +83,7 @@ export default function ModeGateOverlay({ includeSpectatorAccessGate = false })
<AuthPanel />
</div>
<SocialButton id="discord" label="Join our Discord server for updates!" />
{details.chatIntro}
You can still use the chat while the server is locked:
{/* set max height of this box */}
<div className='max-h-80 overflow-y-auto'>
<ChatPanel nicknameLayout="stacked" />
@@ -1,21 +1,8 @@
// Spectator App Root
// 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.
import ModeGateOverlay from '../../components/ModeGateOverlay/index.jsx';
import SpectatorContent from './SpectatorContent.jsx';
export default function SpectatorAppRoot() {
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 />
</>
);
return <SpectatorContent />;
}