mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
ui adjustments
This commit is contained in:
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
@@ -78,8 +78,8 @@
|
||||
<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>
|
||||
<title>Roomba Rover</title>
|
||||
<script type="module" crossorigin src="/assets/index-CAXEdE-L.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Brq9029h.css">
|
||||
<script type="module" crossorigin src="/assets/index-CDPuE5o4.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DfNhcl9P.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -152,11 +152,48 @@ function getPublicRoster() {
|
||||
.filter((rover) => !isClosedPrivateRover(rover));
|
||||
}
|
||||
|
||||
function publicRoverIdSet(roster = []) {
|
||||
return new Set(roster.map((rover) => String(rover?.id || '')).filter(Boolean));
|
||||
}
|
||||
|
||||
function filterPublicTurnQueues(turnQueues = {}, publicIds) {
|
||||
const visible = publicIds instanceof Set ? publicIds : new Set();
|
||||
const next = {};
|
||||
/*
|
||||
RoverQueuesPanel creates fallback rows for queue ids that are not present in
|
||||
the roster, so the public payload must filter queues with the exact same
|
||||
privacy boundary as the roster. Otherwise a closed-private rover can leak as
|
||||
an orphan queue row after a private access grant assigns someone to it.
|
||||
*/
|
||||
Object.entries(turnQueues || {}).forEach(([roverId, info]) => {
|
||||
if (!visible.has(String(roverId))) return;
|
||||
next[roverId] = info;
|
||||
});
|
||||
return next;
|
||||
}
|
||||
|
||||
function filterPublicUsers(users = [], publicIds) {
|
||||
const visible = publicIds instanceof Set ? publicIds : new Set();
|
||||
/*
|
||||
A user's current rover id is also part of the public inter-instance surface.
|
||||
If that rover is not in the public roster, scrub only that association while
|
||||
leaving the rest of the public user entry intact for normal queue display.
|
||||
*/
|
||||
return users.map((user) => {
|
||||
const roverId = user?.roverId ? String(user.roverId) : '';
|
||||
if (!roverId || visible.has(roverId)) return user;
|
||||
return { ...user, roverId: null };
|
||||
});
|
||||
}
|
||||
|
||||
function buildLocalInfo() {
|
||||
const mode = getMode();
|
||||
const lockdown = isLockdownMode();
|
||||
const features = getFeatureFlags();
|
||||
const roster = getPublicRoster().map((rover) => (lockdown ? rover : addRoverSnapshotLinks(rover)));
|
||||
const publicRoster = getPublicRoster();
|
||||
const publicIds = publicRoverIdSet(publicRoster);
|
||||
const roster = publicRoster.map((rover) => (lockdown ? rover : addRoverSnapshotLinks(rover)));
|
||||
const users = filterPublicUsers(Array.from(io.sockets.sockets.values()).map(buildUserEntry), publicIds);
|
||||
const roomCameras = lockdown || !features.roomCameras ? [] : getRoomCameras().map(buildRoomCameraInfo);
|
||||
return {
|
||||
instance: {
|
||||
@@ -167,8 +204,8 @@ function buildLocalInfo() {
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
roster,
|
||||
turnQueues: getTurnQueues(),
|
||||
users: Array.from(io.sockets.sockets.values()).map(buildUserEntry),
|
||||
turnQueues: filterPublicTurnQueues(getTurnQueues(), publicIds),
|
||||
users,
|
||||
roomCameras,
|
||||
socials: features.socials ? getConfiguredSocials(config) : [],
|
||||
};
|
||||
|
||||
@@ -30,8 +30,8 @@ function getRemoteAvailability(remote) {
|
||||
const mode = remote?.instance?.mode || 'unknown';
|
||||
if (!remote?.online) return { blocked: true, label: 'Offline', overlay: 'This server is offline', tone: 'red' };
|
||||
if (mode === 'lockdown') return { blocked: true, label: 'Lockdown', overlay: 'This server is in lockdown', tone: 'red' };
|
||||
if (mode === 'admin') return { blocked: true, label: 'Admin only', overlay: 'This server is admin only', tone: 'amber' };
|
||||
if (mode === 'turns') return { blocked: false, label: 'Turns', tone: 'sky' };
|
||||
if (mode === 'admin') return { blocked: true, label: 'Admin only', overlay: 'This server is admin only', tone: 'red' };
|
||||
if (mode === 'turns') return { blocked: false, label: 'Turns', tone: 'emerald' };
|
||||
if (mode === 'open') return { blocked: false, label: 'Open', tone: 'emerald' };
|
||||
return { blocked: false, label: mode, tone: 'slate' };
|
||||
}
|
||||
@@ -54,17 +54,19 @@ function statusClass(tone) {
|
||||
function InstanceStatus({ remote }) {
|
||||
const availability = getRemoteAvailability(remote);
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-0.5 text-[0.7rem]">
|
||||
<div className="flex shrink-0 flex-wrap items-center gap-0.5 text-[0.7rem]">
|
||||
<span className={classNames('rounded px-1.5 py-0.5 text-xs font-semibold', statusClass(availability.tone))}>
|
||||
{availability.label}
|
||||
</span>
|
||||
{remote?.latencyMs != null ? (
|
||||
<span className="rounded bg-slate-800 px-1 text-slate-300">{remote.latencyMs}ms</span>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InstanceLatency({ remote }) {
|
||||
if (remote?.latencyMs == null) return null;
|
||||
return <span className="text-[0.7rem] font-normal text-slate-500">{remote.latencyMs}ms</span>;
|
||||
}
|
||||
|
||||
function InstancePanel({ remote, children = null }) {
|
||||
const instance = remote?.instance || {};
|
||||
const features = featureEntries(instance.features);
|
||||
@@ -74,23 +76,23 @@ function InstancePanel({ remote, children = null }) {
|
||||
return (
|
||||
<CardFrame
|
||||
title={instance.name || remote.url || 'External server'}
|
||||
meta={<InstanceLatency remote={remote} />}
|
||||
bodyClassName="space-y-0.5 p-0.5 text-sm"
|
||||
>
|
||||
<div className="h-1 w-full" style={{ backgroundColor: color }} title={color} />
|
||||
|
||||
<div className="space-y-1 text-center">
|
||||
<div className="space-y-1">
|
||||
{/*
|
||||
The status is the main operational signal for a remote instance, so it
|
||||
is placed in the body as a primary row instead of competing with the
|
||||
card title. Offline cards stop after this small header/action area so
|
||||
the user only sees one offline message and one way to visit the server.
|
||||
stays beside the human-facing description where users naturally scan
|
||||
the server summary. Latency is intentionally moved to the title bar as
|
||||
quiet metadata because it is useful detail, not the primary decision.
|
||||
*/}
|
||||
<div className="flex justify-center">
|
||||
<div className="flex flex-wrap items-center justify-center gap-1 text-center">
|
||||
<InstanceStatus remote={remote} />
|
||||
{online && instance.description ? <p className="min-w-0 text-slate-200">{instance.description}</p> : null}
|
||||
</div>
|
||||
|
||||
{online && instance.description ? <p className="text-slate-200">{instance.description}</p> : null}
|
||||
|
||||
<div className="flex min-w-0 flex-col items-center justify-center gap-0.5 sm:flex-row sm:gap-2">
|
||||
{publicUrl ? (
|
||||
<span className="max-w-full truncate text-slate-400" title={publicUrl}>
|
||||
@@ -174,46 +176,25 @@ export function ExternalInstancesCompact() {
|
||||
}
|
||||
|
||||
export function InterInstancePopup({ onClose }) {
|
||||
const enabled = useInterInstanceEnabled();
|
||||
if (!enabled) return null;
|
||||
return (
|
||||
<div className="fixed inset-0 z-[70] flex items-center justify-center bg-black/80 p-0.5">
|
||||
<CardFrame
|
||||
title="External instances"
|
||||
actions={
|
||||
<button type="button" className="button-dark" onClick={onClose}>
|
||||
Close
|
||||
</button>
|
||||
}
|
||||
className="w-full max-w-6xl"
|
||||
<InterInstanceBrowserFrame
|
||||
onClose={onClose}
|
||||
className="max-w-[calc(100vw-0.5rem)]"
|
||||
bodyClassName="max-h-[82vh] overflow-y-auto p-0.5"
|
||||
clipOverflow={false}
|
||||
>
|
||||
<InterInstancePanel />
|
||||
</CardFrame>
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function InterInstancePanel({ compact = false, centered = false }) {
|
||||
const enabled = useInterInstanceEnabled();
|
||||
const instances = useRemoteInstances();
|
||||
if (!enabled) return null;
|
||||
if (compact) return <ExternalInstancesCompact />;
|
||||
if (!instances.length) {
|
||||
return (
|
||||
<CardFrame title="External instances" bodyClassName="p-0.5 text-sm">
|
||||
<p className="text-slate-500">No external instances discovered.</p>
|
||||
</CardFrame>
|
||||
);
|
||||
}
|
||||
function InterInstanceCards({ instances, centered = false }) {
|
||||
return (
|
||||
<div className={classNames(
|
||||
'flex flex-wrap justify-center gap-0.5',
|
||||
centered && 'mx-auto w-full max-w-3xl',
|
||||
)}>
|
||||
{instances.map((remote) => (
|
||||
<div key={remote.url} className="w-full max-w-md flex-1 basis-80 space-y-0.5">
|
||||
<div key={remote.url} className="w-80 max-w-full shrink-0 space-y-0.5">
|
||||
<InstancePanel remote={remote}>
|
||||
{/*
|
||||
The large browser should read as one card per external server:
|
||||
@@ -238,3 +219,56 @@ export default function InterInstancePanel({ compact = false, centered = false }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function InterInstanceBrowserFrame({
|
||||
onClose = null,
|
||||
hideWhenEmpty = false,
|
||||
className = '',
|
||||
bodyClassName = 'p-0.5',
|
||||
centered = false,
|
||||
}) {
|
||||
const enabled = useInterInstanceEnabled();
|
||||
const instances = useRemoteInstances();
|
||||
if (!enabled) return null;
|
||||
if (!instances.length && hideWhenEmpty) return null;
|
||||
const actions = onClose ? (
|
||||
<button type="button" className="button-dark" onClick={onClose}>
|
||||
Close
|
||||
</button>
|
||||
) : null;
|
||||
/*
|
||||
This frame is shared by the popup and the admin/lockdown overlay. Keeping
|
||||
the wrapper here means those surfaces get the same external-instance card
|
||||
without placing it inside the login card or duplicating layout behavior.
|
||||
*/
|
||||
return (
|
||||
<CardFrame
|
||||
title="External instances"
|
||||
actions={actions}
|
||||
className={className}
|
||||
bodyClassName={bodyClassName}
|
||||
clipOverflow={false}
|
||||
>
|
||||
{instances.length ? (
|
||||
<InterInstanceCards instances={instances} centered={centered} />
|
||||
) : (
|
||||
<p className="text-sm text-slate-500">No external instances discovered.</p>
|
||||
)}
|
||||
</CardFrame>
|
||||
);
|
||||
}
|
||||
|
||||
export default function InterInstancePanel({ compact = false, centered = false }) {
|
||||
const enabled = useInterInstanceEnabled();
|
||||
const instances = useRemoteInstances();
|
||||
if (!enabled) return null;
|
||||
if (compact) return <ExternalInstancesCompact />;
|
||||
if (!instances.length) {
|
||||
return (
|
||||
<CardFrame title="External instances" bodyClassName="p-0.5 text-sm">
|
||||
<p className="text-slate-500">No external instances discovered.</p>
|
||||
</CardFrame>
|
||||
);
|
||||
}
|
||||
return <InterInstanceCards instances={instances} centered={centered} />;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useSessionSelector } from '../../context/SessionContext.jsx';
|
||||
import { useSharedClock } from '../../hooks/useSharedClock.js';
|
||||
import SocialButton from '../SocialButton/index.jsx';
|
||||
import ChatPanel from '../ChatPanel/index.jsx';
|
||||
import InterInstancePanel from '../InterInstancePanel/index.jsx';
|
||||
import { InterInstanceBrowserFrame } from '../InterInstancePanel/index.jsx';
|
||||
import { isFeatureEnabled } from '../../lib/features.js';
|
||||
|
||||
const PRIVILEGED_ROLES = new Set(['admin', 'lockdown']);
|
||||
@@ -66,8 +66,8 @@ 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-6xl flex-col items-center justify-center gap-0.5">
|
||||
<div className="surface w-full max-w-md space-y-0.5 text-slate-100 shadow-2xl">
|
||||
<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>
|
||||
@@ -95,9 +95,16 @@ export default function ModeGateOverlay() {
|
||||
</p> */}
|
||||
</div>
|
||||
{interInstanceEnabled ? (
|
||||
<div className="w-full">
|
||||
<InterInstancePanel centered />
|
||||
</div>
|
||||
/*
|
||||
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
|
||||
className="max-w-[calc(100vw-0.5rem)]"
|
||||
bodyClassName="max-h-[86vh] overflow-y-auto p-0.5"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -208,14 +208,14 @@ export default function RoverQueuesPanel({
|
||||
const isPrivateOpen = Boolean(rover?.private?.enabled && rover?.private?.open);
|
||||
const isGrantedClosedPrivate = Boolean(rover?.private?.enabled && !rover?.private?.open);
|
||||
const locked = Boolean(rover.locked);
|
||||
const lockedBlocked = !externalMode && locked && !adminCapable && !isGrantedClosedPrivate;
|
||||
const lockedBlocked = locked && (externalMode || (!adminCapable && !isGrantedClosedPrivate));
|
||||
const lockLabel = rover.lockReason ? `locked: ${rover.lockReason}` : 'locked';
|
||||
const buttonLabel = pending[roverId]
|
||||
? '...'
|
||||
: lockedBlocked
|
||||
? lockLabel
|
||||
: externalMode
|
||||
? 'Open'
|
||||
: locked && !isGrantedClosedPrivate
|
||||
? lockLabel
|
||||
: 'request';
|
||||
const canClickRow = canRequest && !lockedBlocked && !pending[roverId];
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user