ui adjustments

This commit is contained in:
legop3
2026-07-06 20:13:45 -04:00
parent a8bff428c2
commit 4204a66549
10 changed files with 144 additions and 66 deletions
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
@@ -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} />;
}
+13 -6
View File
@@ -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 (