optional but always on transfer

This commit is contained in:
legop3
2026-07-08 12:53:39 -04:00
parent 5d60544904
commit a6f6ccf079
12 changed files with 253 additions and 178 deletions
@@ -5,8 +5,9 @@ import { useMemo, useState } from 'react';
import { useSessionSelector } from '../../context/SessionContext.jsx';
import CardFrame from '../CardFrame/index.jsx';
import RoverQueuesPanel from '../RoverQueuesPanel/index.jsx';
import { openExternalRoverWithPrompt } from '../../lib/interInstanceTransfer.js';
import { openExternalRover } from '../../lib/interInstanceTransfer.js';
import { isFeatureEnabled } from '../../lib/features.js';
import { useSettingsNamespace } from '../../settings/index.js';
function classNames(...values) {
return values.filter(Boolean).join(' ');
@@ -72,6 +73,8 @@ function InstancePanel({ remote, children = null }) {
const features = featureEntries(instance.features);
const color = instance.color || '#64748b';
const online = Boolean(remote?.online);
const { value: pageSettings } = useSettingsNamespace('page', { interInstanceTransferSettings: true });
const includeSettings = pageSettings?.interInstanceTransferSettings !== false;
return (
<CardFrame
title={instance.name || remote.url || 'External server'}
@@ -92,7 +95,11 @@ function InstancePanel({ remote, children = null }) {
</div>
<div className="flex flex-wrap items-center justify-center gap-1 text-center">
<InstanceStatus remote={remote} />
<button type="button" className="button-dark" onClick={() => openExternalRoverWithPrompt(remote, '')}>
<button
type="button"
className="button-dark"
onClick={() => openExternalRover(remote, '', { includeSettings })}
>
Visit server
</button>
</div>
@@ -7,9 +7,10 @@ import { useSharedClock } from '../../hooks/useSharedClock.js';
import CardFrame from '../CardFrame/index.jsx';
import RoverLabel from '../RoverLabel/index.jsx';
import { trackAnalyticsEvent } from '../../analytics/index.js';
import { openExternalRoverWithPrompt } from '../../lib/interInstanceTransfer.js';
import { openExternalRover } from '../../lib/interInstanceTransfer.js';
import { ExternalInstancesCompact } from '../InterInstancePanel/index.jsx';
import { isFeatureEnabled } from '../../lib/features.js';
import { useSettingsNamespace } from '../../settings/index.js';
function classNames(...values) {
return values.filter(Boolean).join(' ');
@@ -62,6 +63,7 @@ export default function RoverQueuesPanel({
const localTurnQueues = useSessionSelector((state) => state.session?.turnQueues ?? {});
const localUsers = useSessionSelector((state) => state.session?.users ?? []);
const interInstanceEnabled = useSessionSelector((state) => isFeatureEnabled(state, 'interInstance'));
const { value: pageSettings } = useSettingsNamespace('page', { interInstanceTransferSettings: true });
const selfId = useSessionSelector((state) => state.session?.socketId || null);
const assignedRoverId = useSessionSelector((state) => String(state.session?.assignment?.roverId || '').trim());
const assignedRoverName = useSessionSelector((state) => {
@@ -75,6 +77,7 @@ export default function RoverQueuesPanel({
const [rebootPending, setRebootPending] = useState(false);
const externalMode = Boolean(externalInstance);
const externalBlocked = Boolean(externalMode && disabledOverlay);
const includeInterInstanceSettings = pageSettings?.interInstanceTransferSettings !== false;
const roster = Array.isArray(rosterOverride) ? rosterOverride : localRoster;
const turnQueues = turnQueuesOverride && typeof turnQueuesOverride === 'object' ? turnQueuesOverride : localTurnQueues;
const users = Array.isArray(usersOverride) ? usersOverride : localUsers;
@@ -113,9 +116,10 @@ export default function RoverQueuesPanel({
/*
External queue cards deliberately reuse the local row layout, but their
action cannot go through this Socket.IO server. The row opens the remote
instance, optionally carrying settings after the source-page prompt.
instance and follows the saved Page setting for cookie/settings transfer
instead of interrupting each click with a confirmation popup.
*/
openExternalRoverWithPrompt(externalInstance, targetRoverId);
openExternalRover(externalInstance, targetRoverId, { includeSettings: includeInterInstanceSettings });
return;
}
setPending((prev) => ({ ...prev, [targetRoverId]: true }));
+40 -4
View File
@@ -91,6 +91,18 @@ function RangeSetting({ label, value, disabled = false, onChange }) {
);
}
function reconnectSocketWithTransport(socket, transport) {
if (!socket?.io?.opts) return;
/*
Socket.IO reads its manager options when reconnecting. Keep this mutation in
one helper instead of inside the component body so the settings handler only
expresses the user-facing action: save preference, then reconnect.
*/
socket.io.opts.transports = transport === 'polling' ? ['polling'] : ['websocket', 'polling'];
socket.disconnect();
socket.connect();
}
export default function SettingsPanel() {
const keymap = useControlSelector((control) => control.state.keymap);
const roverId = useControlSelector((control) => control.state.roverId);
@@ -103,6 +115,7 @@ export default function SettingsPanel() {
connectionTransport: 'websocket',
swapMobileControlColumns: false,
driveMacroBackoffEnabled: true,
interInstanceTransferSettings: true,
});
const { value: audioSettings, save: saveAudioSettings } = useSettingsNamespace('audio', AUDIO_SETTINGS_DEFAULTS);
const { value: videoSettings, save: saveVideoSettings } = useSettingsNamespace('video', VIDEO_SETTINGS_DEFAULTS);
@@ -112,6 +125,7 @@ export default function SettingsPanel() {
typeof pageSettings?.driveMacroBackoffEnabled === 'boolean'
? pageSettings.driveMacroBackoffEnabled
: true;
const interInstanceTransferSettings = pageSettings?.interInstanceTransferSettings !== false;
const masterVolume = Number.isFinite(audioSettings?.masterVolume) ? audioSettings.masterVolume : AUDIO_SETTINGS_DEFAULTS.masterVolume;
const alertVolume = Number.isFinite(audioSettings?.alertVolume) ? audioSettings.alertVolume : AUDIO_SETTINGS_DEFAULTS.alertVolume;
const roverVolume = Number.isFinite(audioSettings?.roverVolume) ? audioSettings.roverVolume : AUDIO_SETTINGS_DEFAULTS.roverVolume;
@@ -144,10 +158,7 @@ export default function SettingsPanel() {
const next = event.target.value;
savePageSettings((current) => ({ ...(current ?? {}), connectionTransport: next }));
trackAnalyticsEvent('settings_change', { setting: 'connection_transport', value: next });
if (!socket?.io?.opts) return;
socket.io.opts.transports = next === 'polling' ? ['polling'] : ['websocket', 'polling'];
socket.disconnect();
socket.connect();
reconnectSocketWithTransport(socket, next);
};
const handleAudioRange = (key) => (event) => {
@@ -180,6 +191,17 @@ export default function SettingsPanel() {
trackAnalyticsEvent('settings_change', { setting: 'driveMacroBackoffEnabled', value: checked });
};
const handleInterInstanceTransferSettings = (event) => {
const checked = Boolean(event.target.checked);
/*
This replaces the old per-click transfer confirmation. Keeping the choice
in Page settings makes external-server navigation immediate while still
letting users opt out of sending their current settings cookie.
*/
savePageSettings((current) => ({ ...(current ?? {}), interInstanceTransferSettings: checked }));
trackAnalyticsEvent('settings_change', { setting: 'interInstanceTransferSettings', value: checked });
};
const handleVideoFilterChange = (event) => {
const nextFilter = normalizeVideoFilter(event.target.value);
@@ -329,6 +351,20 @@ export default function SettingsPanel() {
</SettingRow>
<SettingHelp>Switching reconnects your session.</SettingHelp>
</CardFrame>
<CardFrame title="Inter-instance" bodyClassName="space-y-1 p-1 text-sm">
<SettingRow className="grid-cols-[auto_minmax(0,1fr)] max-[420px]:grid-cols-[auto_minmax(0,1fr)]">
<input
type="checkbox"
className="h-3.5 w-3.5 accent-emerald-500"
checked={interInstanceTransferSettings}
onChange={handleInterInstanceTransferSettings}
/>
<span className="font-semibold text-white">Transfer settings when opening external servers</span>
</SettingRow>
<SettingHelp>
Sends this browser's saved identity and page settings to the destination server automatically.
</SettingHelp>
</CardFrame>
</div>
</TabPanel>
<TabPanel id="admin">