mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
adding added ad slot support for adddss
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
+2
-2
File diff suppressed because one or more lines are too long
+1
-1
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
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user