adding added ad slot support for adddss

This commit is contained in:
legop3
2026-08-01 01:58:34 -04:00
parent 7fdcb53041
commit 3ebd1c7f9c
16 changed files with 198 additions and 73 deletions
+3
View File
@@ -34,3 +34,6 @@ server/data/barcode-games.json
server/data/identity.sqlite-shm
server/data/identity.sqlite-wal
server/src/services/balanceBoardService/native/balance_board_worker
server/data/fleet-reports.sqlite
server/data/fleet-reports.sqlite-shm
server/data/fleet-reports.sqlite-wal
+12
View File
@@ -232,6 +232,18 @@ socials:
url: "https://ko-fi.com/your-handle"
icon: "FaCoffee"
color: "#29ABE0"
# Optional trusted HTML card shown at the bottom of the desktop driver page's
# left column. Leave html empty (or omit this section) to hide the card. This
# content is sent to driver browsers without sanitization, so only place markup
# here that is controlled by the server operator.
driverAd:
title: "Advertisement"
html: |
<a href="https://example.com" target="_blank" rel="noopener noreferrer">
<img src="https://example.com/ad.png" alt="Advertisement" style="display:block;width:100%;height:auto;">
</a>
# Optional passive fleet telemetry, history, and daily reporting. The collector
# observes existing server events and rover sensor frames but never participates
# in command, assignment, docking, or safety decisions.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -12,8 +12,8 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<!-- site-metadata:inject -->
<!-- analytics:inject -->
<script type="module" crossorigin src="/assets/index-B0pLdqo3.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CoK6vO1j.css">
<script type="module" crossorigin src="/assets/index-CqhbbOso.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-5vzB2lhE.css">
</head>
<body>
<div id="root"></div>
@@ -9,6 +9,20 @@ const discordInvite = config.discord?.invite || null;
const kofiLink = config.kofi?.link || null;
const serverTimezone = config.timezone || null;
const configuredSocials = getConfiguredSocials(config);
/*
The driver ad is trusted deployment content supplied by the server operator.
Normalize both values at the server boundary so every browser receives a
predictable string-only contract, even when the YAML keys are absent or were
accidentally configured with another scalar type.
Keep the title and markup together because they describe one optional card.
An empty HTML string disables the card; the title alone must never leave an
empty panel at the bottom of the driver layout.
*/
const driverAd = {
title: typeof config.driverAd?.title === 'string' ? config.driverAd.title.trim() : '',
html: typeof config.driverAd?.html === 'string' ? config.driverAd.html.trim() : '',
};
const ACTIVITY_SYNC_COOLDOWN_MS = 3000;
const GPIO_TOGGLE_SYNC_COOLDOWN_MS = 1000;
@@ -19,6 +33,7 @@ module.exports = {
kofiLink,
serverTimezone,
configuredSocials,
driverAd,
ACTIVITY_SYNC_COOLDOWN_MS,
GPIO_TOGGLE_SYNC_COOLDOWN_MS,
PERIODIC_SYNC_MS,
@@ -61,6 +61,7 @@ const {
kofiLink,
serverTimezone,
configuredSocials,
driverAd,
ACTIVITY_SYNC_COOLDOWN_MS,
GPIO_TOGGLE_SYNC_COOLDOWN_MS,
PERIODIC_SYNC_MS,
@@ -209,6 +210,13 @@ function buildSession(socket) {
adminReason: getAdminReason(),
users,
socials,
/*
This is intentionally raw, operator-authored HTML. The browser only
mounts it on the desktop driver page, but keeping it in the ordinary
session payload makes the server configuration the single source of
truth and avoids a separate endpoint for one small optional card.
*/
driverAd,
discord: {
invite: discordInvite,
},
+8
View File
@@ -52,6 +52,7 @@ import NeatoCard from './components/NeatoCard/index.jsx';
import RewardRunOverlay from './components/RewardRunOverlay/index.jsx';
import SocketConnectionPill from './components/SocketConnectionPill/index.jsx';
import DuplicateIdentityOverlay from './components/DuplicateIdentityOverlay/index.jsx';
import DriverAdCard from './components/DriverAdCard/index.jsx';
import {
DEFAULT_PAGE_THEME_KEY,
getPageThemeClass,
@@ -66,6 +67,13 @@ function DesktopLayout({ layout, onOpenHelpOverlay }) {
<div className={`flex min-w-0 flex-[1.22] flex-col ${themeGapClass} overflow-y-auto pr-0`}>
<DriverVideo />
<PiHostStatsCard />
{/*
The ad owns its empty-state gate and uses auto top margin to consume
any spare column height. Keeping it as the final desktop-only child
pins it to the bottom without introducing layout measurement code or
exposing it on mobile and alternate application routes.
*/}
<DriverAdCard className="mt-auto" />
{/* <TelemetryPanel /> */}
</div>
<div className={`flex min-w-0 flex-1 flex-col ${themeGapClass} overflow-y-auto`}>
@@ -0,0 +1,49 @@
// Driver Ad Card
// Purpose: Renders the optional server-configured advertisement in the desktop
// driver column. Scope: This component owns presentation and self-gating only;
// the server remains the source of both the title and trusted HTML markup.
import CardFrame from '../CardFrame/index.jsx';
import { useSessionSelector } from '../../context/SessionContext.jsx';
export default function DriverAdCard({ className = '' }) {
const driverAd = useSessionSelector((state) => state.session?.driverAd || null);
const title = typeof driverAd?.title === 'string' ? driverAd.title : '';
const html = typeof driverAd?.html === 'string' ? driverAd.html.trim() : '';
/*
The HTML value is the feature gate. A configured title without content
should not reserve desktop space or display an empty CardFrame.
*/
if (!html) return null;
return (
<CardFrame
title={title}
/*
The card owns its full-width media surface, while each layout owns
placement such as desktop auto-spacing or mode-gate centering. This
keeps one ad renderer reusable without baking either location into it.
*/
className={`w-full shrink-0 ${className}`}
bodyClassName="w-full overflow-hidden p-0"
>
{/*
This markup is deliberately not sanitized: the requested contract is
trusted HTML authored by the server operator. Anyone allowed to edit
server configuration must therefore be treated as having control over
content rendered in connected driver browsers.
React's inner-HTML insertion renders markup such as links, images, and
iframes, but browsers do not normally execute script elements inserted
this way. Providers that require a script loader need a separate,
explicit integration rather than silently changing this contract.
*/}
{/*
The slot deliberately imposes no rules on the operator-authored child
markup. Images, iframes, or more complex embeds keep their own sizing
contract instead of inheriting assumptions from one ad provider.
*/}
<div className="w-full" dangerouslySetInnerHTML={{ __html: html }} />
</CardFrame>
);
}
@@ -195,10 +195,15 @@ export function InterInstancePopup({ onClose }) {
);
}
function InterInstanceCards({ instances, centered = false }) {
function InterInstanceCards({ instances, centered = false, singleColumn = false }) {
return (
<div className={classNames(
'flex flex-wrap justify-center gap-0.5',
/*
The full browser wraps cards to use available popup space. Restricted
mode instead requests one stable vertical column so queue overflow can
only add vertical scrolling and can never widen the gate overlay.
*/
singleColumn ? 'flex flex-col items-center gap-0.5' : 'flex flex-wrap justify-center gap-0.5',
centered && 'mx-auto w-full max-w-3xl',
)}>
{instances.map((remote) => (
@@ -235,6 +240,7 @@ export function InterInstanceBrowserFrame({
bodyClassName = 'p-0.5',
centered = false,
scaledOverlay = false,
singleColumn = false,
}) {
const enabled = useInterInstanceEnabled();
const instances = useRemoteInstances();
@@ -259,7 +265,7 @@ export function InterInstanceBrowserFrame({
clipOverflow={false}
>
{instances.length ? (
<InterInstanceCards instances={instances} centered={centered} />
<InterInstanceCards instances={instances} centered={centered} singleColumn={singleColumn} />
) : (
<p className="text-sm text-slate-500">No external instances discovered.</p>
)}
+77 -53
View File
@@ -9,6 +9,7 @@ import SocialButton from '../SocialButton/index.jsx';
import ChatPanel from '../ChatPanel/index.jsx';
import { InterInstanceBrowserFrame } from '../InterInstancePanel/index.jsx';
import { isFeatureEnabled } from '../../lib/features.js';
import DriverAdCard from '../DriverAdCard/index.jsx';
const PRIVILEGED_ROLES = new Set(['admin', 'lockdown']);
const LOCKDOWN_ROLES = new Set(['lockdown']);
@@ -29,22 +30,19 @@ function getModeDetails(mode = 'admin') {
};
}
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 restricted = RESTRICTED_MODES.has(mode);
const privileged = mode === 'lockdown' ? LOCKDOWN_ROLES.has(role) : PRIVILEGED_ROLES.has(role);
function ServerTime({ timezone }) {
/*
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, restricted && !privileged);
Keep the one-second clock subscription in this leaf component. If the mode
gate itself owns the changing timestamp, React rerenders the authentication
card, chat, external-instance browser, and raw ad iframe every second even
though only this short label changed.
const serverTime = useMemo(() => {
ServerTime is only mounted while the gate is visible, so useSharedClock
automatically removes its listener when access is restored and no hidden
overlay interval remains active.
*/
const nowMs = useSharedClock(1000);
const formattedTime = useMemo(() => {
const now = new Date(nowMs);
try {
return new Intl.DateTimeFormat('en-US', {
@@ -58,6 +56,18 @@ export default function ModeGateOverlay() {
}
}, [nowMs, timezone]);
return <p className="text-center text-sm text-slate-300">Server time: {formattedTime}</p>;
}
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 restricted = RESTRICTED_MODES.has(mode);
const privileged = mode === 'lockdown' ? LOCKDOWN_ROLES.has(role) : PRIVILEGED_ROLES.has(role);
if (!restricted || privileged) {
return null;
}
@@ -66,47 +76,61 @@ export default function ModeGateOverlay() {
return (
<div className="pointer-events-auto fixed inset-0 z-50 overflow-y-auto bg-black px-0.5 py-0.5">
<div className="mx-auto flex min-h-full w-full max-w-7xl flex-col items-center justify-center gap-0.5 lg:flex-row lg:items-center">
<div className="surface w-full max-w-md shrink-0 space-y-0.5 text-slate-100 shadow-2xl">
<div className="space-y-0.5">
<p className="text-lg font-semibold">{details.title}</p>
<p className="text-sm text-slate-300">{details.description}</p>
<div className="mx-auto flex min-h-full w-full max-w-7xl flex-col gap-0.5">
{/*
The access and external-instance cards live in a flexible center
region. The ad remains a separate final row, so it stays centered at
the bottom of the overlay rather than becoming a third column or
shifting the access controls away from the visual center.
*/}
<div className="flex min-h-0 flex-1 items-center justify-center">
<div className="flex w-full min-w-0 flex-col items-center justify-center gap-0.5 lg:flex-row lg:items-center">
<div className="surface w-full max-w-md shrink-0 space-y-0.5 text-slate-100 shadow-2xl">
<div className="space-y-0.5">
<p className="text-lg font-semibold">{details.title}</p>
<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">Reason for locking:</p>
<p className="text-lg font-semibold text-slate-100">
{reason ? reason : 'No reason set.'}
</p>
<ServerTime timezone={timezone} />
</div>
<div className="surface-muted">
<AuthPanel />
</div>
<SocialButton id="discord" label="Join our Discord server for updates!" />
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" />
</div>
</div>
{interInstanceEnabled ? (
/*
Give the mode-gate copy of the shared browser a stable width
and allow it to shrink below that width. Scrollable queue
descendants must not expand this flex item to their intrinsic
content width when the body gains a vertical scrollbar.
*/
<InterInstanceBrowserFrame
hideWhenEmpty
scaledOverlay
singleColumn
className="inter-instance-overlay-frame w-[20.5rem] min-w-0 max-w-full"
bodyClassName="inter-instance-overlay-body min-w-0 overflow-x-hidden overflow-y-auto p-0.5"
/>
) : null}
</div>
<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-lg font-semibold text-slate-100">
{reason ? reason : 'No reason set.'}
</p>
<p className="text-center text-sm text-slate-300">Server time: {serverTime}</p>
</div>
<div className="surface-muted">
<AuthPanel />
</div>
<SocialButton id="discord" label="Join our Discord server for updates!" />
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" />
</div>
{/* <p className="text-xs text-slate-500">
Your controls are paused until access is granted. You will automatically regain the interface once the mode
changes or after a successful login.
</p> */}
</div>
{interInstanceEnabled ? (
/*
The external browser is a sibling of the login card, not content
inside it. hideWhenEmpty lets the login card remain centered when
the directory has no other servers to offer.
*/
<InterInstanceBrowserFrame
hideWhenEmpty
scaledOverlay
className="inter-instance-overlay-frame"
bodyClassName="inter-instance-overlay-body overflow-y-auto p-0.5"
/>
) : null}
{/*
DriverAdCard self-gates on configured HTML. When disabled it returns
null and consumes no footer space; when enabled this responsive width
keeps the card centered without tying the overlay to one provider's
creative dimensions.
*/}
<DriverAdCard className="mx-auto max-w-3xl" />
</div>
</div>
);