inter-instance UI redo

This commit is contained in:
legop3
2026-07-06 15:58:40 -04:00
parent 07ad43f42f
commit 69b49ae1d6
8 changed files with 97 additions and 53 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-_MGdcdqA.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-LIjDMlbF.css">
<script type="module" crossorigin src="/assets/index-BFWCxjyl.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CI7XPkxh.css">
</head>
<body>
<div id="root"></div>
@@ -26,15 +26,38 @@ function featureEntries(features = {}) {
.map(([name]) => name);
}
function InstanceStatus({ remote }) {
function getRemoteAvailability(remote) {
const mode = remote?.instance?.mode || 'unknown';
const online = Boolean(remote?.online);
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 === 'open') return { blocked: false, label: 'Open', tone: 'emerald' };
return { blocked: false, label: mode, tone: 'slate' };
}
function statusClass(tone) {
switch (tone) {
case 'emerald':
return 'bg-emerald-700/70 text-emerald-50 ring-1 ring-emerald-300/50';
case 'sky':
return 'bg-sky-700/70 text-sky-50 ring-1 ring-sky-300/50';
case 'amber':
return 'bg-amber-600/80 text-amber-50 ring-1 ring-amber-200/60';
case 'red':
return 'bg-red-700/80 text-red-50 ring-1 ring-red-300/60';
default:
return 'bg-slate-700/80 text-slate-50 ring-1 ring-slate-400/40';
}
}
function InstanceStatus({ remote }) {
const availability = getRemoteAvailability(remote);
return (
<div className="flex flex-wrap items-center gap-0.5 text-[0.7rem]">
<span className={classNames('rounded px-1', online ? 'bg-emerald-700/60 text-emerald-100' : 'bg-red-800/60 text-red-100')}>
{online ? 'Online' : 'Offline'}
<span className={classNames('rounded px-1.5 py-0.5 text-xs font-semibold', statusClass(availability.tone))}>
{availability.label}
</span>
<span className="rounded bg-slate-800 px-1 text-slate-200">{mode}</span>
{remote?.latencyMs != null ? (
<span className="rounded bg-slate-800 px-1 text-slate-300">{remote.latencyMs}ms</span>
) : null}
@@ -42,38 +65,30 @@ function InstanceStatus({ remote }) {
);
}
function OfflineInstanceCard({ remote }) {
const name = remote?.instance?.name || remote?.url || 'External server';
return (
<CardFrame title={name} meta="Offline" bodyClassName="space-y-0.5 p-0.5 text-sm">
<p className="text-slate-400">{remote?.url || 'No URL available.'}</p>
{remote?.lastError ? <p className="text-red-300">{remote.lastError}</p> : null}
</CardFrame>
);
}
function InstanceMetadata({ remote }) {
function InstancePanel({ remote, children = null }) {
const instance = remote?.instance || {};
const features = featureEntries(instance.features);
const color = instance.color || '#64748b';
const availability = getRemoteAvailability(remote);
return (
<CardFrame
title={instance.name || remote.url || 'External server'}
meta={<InstanceStatus remote={remote} />}
bodyClassName="space-y-0.5 p-0.5 text-sm"
>
<div className="flex items-start gap-0.5">
<span
className="mt-0.5 h-4 w-4 shrink-0 rounded border border-white/20"
style={{ backgroundColor: color }}
title={color}
/>
<div className="min-w-0 flex-1 space-y-0.5">
{instance.description ? <p className="text-slate-200">{instance.description}</p> : null}
<div className="h-1 w-full" style={{ backgroundColor: color }} title={color} />
<div className="space-y-0.5">
<div className="flex items-start justify-between gap-0.5">
<div className="min-w-0 flex-1 space-y-0.5">
{instance.description ? <p className="text-slate-200">{instance.description}</p> : null}
</div>
<InstanceStatus remote={remote} />
</div>
<div className="min-w-0 space-y-0.5">
<div className="flex flex-wrap items-center gap-0.5">
{instance.publicUrl ? <span className="truncate text-slate-400">{instance.publicUrl}</span> : null}
<button type="button" className="button-dark" onClick={() => openExternalRoverWithPrompt(remote, '')}>
Open server
Visit server
</button>
</div>
{features.length ? (
@@ -89,6 +104,19 @@ function InstanceMetadata({ remote }) {
)}
</div>
</div>
{remote.online ? children : (
/*
Offline servers still belong in the same instance card so the large
inter-instance UI has one visual unit per directory entry. Keeping the
failure details here also avoids presenting a separate rover panel for
a server that cannot currently provide one.
*/
<div className="surface-muted space-y-0.5 text-sm">
<p className="font-semibold text-slate-200">{availability.overlay}</p>
<p className="break-all text-slate-400">{remote?.url || 'No URL available.'}</p>
{remote?.lastError ? <p className="text-red-300">{remote.lastError}</p> : null}
</div>
)}
</CardFrame>
);
}
@@ -136,9 +164,10 @@ export function ExternalInstancesCompact() {
turnQueues={remote.turnQueues}
users={remote.users}
externalInstance={remote}
disabledOverlay={getRemoteAvailability(remote).blocked ? getRemoteAvailability(remote).overlay : ''}
/>
) : (
<OfflineInstanceCard key={remote.url} remote={remote} />
<InstancePanel key={remote.url} remote={remote} />
),
)}
</div>
@@ -189,21 +218,25 @@ export default function InterInstancePanel({ compact = false, centered = false }
)}>
{instances.map((remote) => (
<div key={remote.url} className="w-full max-w-md flex-1 basis-80 space-y-0.5">
<InstanceMetadata remote={remote} />
{remote.online ? (
<InstancePanel remote={remote}>
{/*
The large browser should read as one card per external server:
the server metadata, room snapshots, and the exact existing rover
queues panel are grouped together here. RoverQueuesPanel keeps its
own CardFrame and default title, so this component only controls
where that already-existing panel is placed.
*/}
<>
<RemoteMediaStrip remote={remote} />
<RoverQueuesPanel
title={remote.instance?.name || remote.url}
roster={remote.roster}
turnQueues={remote.turnQueues}
users={remote.users}
externalInstance={remote}
disabledOverlay={getRemoteAvailability(remote).blocked ? getRemoteAvailability(remote).overlay : ''}
/>
</>
) : (
<OfflineInstanceCard remote={remote} />
)}
</InstancePanel>
</div>
))}
</div>
@@ -55,6 +55,7 @@ export default function RoverQueuesPanel({
turnQueues: turnQueuesOverride = null,
users: usersOverride = null,
externalInstance = null,
disabledOverlay = '',
}) {
const role = useSessionSelector((state) => state.session?.role || null);
const localRoster = useSessionSelector((state) => state.session?.roster ?? []);
@@ -73,11 +74,15 @@ export default function RoverQueuesPanel({
const [pending, setPending] = useState({});
const [rebootPending, setRebootPending] = useState(false);
const externalMode = Boolean(externalInstance);
const externalBlocked = Boolean(externalMode && disabledOverlay);
const roster = Array.isArray(rosterOverride) ? rosterOverride : localRoster;
const turnQueues = turnQueuesOverride && typeof turnQueuesOverride === 'object' ? turnQueuesOverride : localTurnQueues;
const users = Array.isArray(usersOverride) ? usersOverride : localUsers;
const canRequest = useMemo(() => externalMode || (role && role !== 'spectator'), [externalMode, role]);
const canRequest = useMemo(
() => (externalMode ? !externalBlocked : role && role !== 'spectator'),
[externalBlocked, externalMode, role],
);
const adminCapable = useMemo(
() => role === 'admin' || role === 'lockdown',
[role],
@@ -103,6 +108,7 @@ export default function RoverQueuesPanel({
async function handleRequest(targetRoverId) {
if (!targetRoverId) return;
if (externalBlocked) return;
if (externalMode) {
/*
External queue cards deliberately reuse the local row layout, but their
@@ -176,7 +182,7 @@ export default function RoverQueuesPanel({
return (
<CardFrame title={title} actions={headerActions} bodyClassName="space-y-0.5 text-sm">
<div className="space-y-0.5">
<div className="relative space-y-0.5">
{rosterItems.length === 0 ? (
<p className="text-sm text-slate-500">No rovers registered.</p>
) : (
@@ -308,6 +314,11 @@ export default function RoverQueuesPanel({
})}
</ul>
)}
{externalBlocked ? (
<div className="absolute inset-0 z-10 flex items-center justify-center rounded bg-black/70 px-2 text-center text-sm font-semibold text-slate-100">
{disabledOverlay}
</div>
) : null}
{!externalMode && interInstanceEnabled ? <ExternalInstancesCompact /> : null}
</div>
</CardFrame>