mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-17 18:10:47 -04:00
optional but always on transfer
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
@@ -78,7 +78,7 @@
|
|||||||
<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/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>
|
<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>
|
<title>Roomba Rover</title>
|
||||||
<script type="module" crossorigin src="/assets/index-B3q8p1Xf.js"></script>
|
<script type="module" crossorigin src="/assets/index-htN8tzuK.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BfZ_TWsb.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-BfZ_TWsb.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
1. improve spectator page, options on what to see and what not to see
|
1. improve spectator page, options on what to see and what not to see
|
||||||
2. assign rovers based on battery percentage, give people highest one
|
2. assign rovers based on battery percentage, give people highest one
|
||||||
3. add more background gap themes
|
3. add prefix config for discord bot to replace rs with something else
|
||||||
4. fix this:
|
4. add config to disable client snapshot forcing, disable bandwidth saving
|
||||||
|
5. add admin ui for VIP and private requests instead of only through discord
|
||||||
|
6. make discord bots that can be fine with multiple in one server
|
||||||
|
7. add more background gap themes
|
||||||
|
8. fix this:
|
||||||
`Jun 18 15:14:18 roombaserver.local node[216731]: /home/daniel/MultiRoombaRover/server/src/services/roverManager/socketHandlers.js:92
|
`Jun 18 15:14:18 roombaserver.local node[216731]: /home/daniel/MultiRoombaRover/server/src/services/roverManager/socketHandlers.js:92
|
||||||
Jun 18 15:14:18 roombaserver.local node[216731]: cb({ error: err.message });
|
Jun 18 15:14:18 roombaserver.local node[216731]: cb({ error: err.message });
|
||||||
Jun 18 15:14:18 roombaserver.local node[216731]: ^
|
Jun 18 15:14:18 roombaserver.local node[216731]: ^
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import { useMemo, useState } from 'react';
|
|||||||
import { useSessionSelector } from '../../context/SessionContext.jsx';
|
import { useSessionSelector } from '../../context/SessionContext.jsx';
|
||||||
import CardFrame from '../CardFrame/index.jsx';
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
import RoverQueuesPanel from '../RoverQueuesPanel/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 { isFeatureEnabled } from '../../lib/features.js';
|
||||||
|
import { useSettingsNamespace } from '../../settings/index.js';
|
||||||
|
|
||||||
function classNames(...values) {
|
function classNames(...values) {
|
||||||
return values.filter(Boolean).join(' ');
|
return values.filter(Boolean).join(' ');
|
||||||
@@ -72,6 +73,8 @@ function InstancePanel({ remote, children = null }) {
|
|||||||
const features = featureEntries(instance.features);
|
const features = featureEntries(instance.features);
|
||||||
const color = instance.color || '#64748b';
|
const color = instance.color || '#64748b';
|
||||||
const online = Boolean(remote?.online);
|
const online = Boolean(remote?.online);
|
||||||
|
const { value: pageSettings } = useSettingsNamespace('page', { interInstanceTransferSettings: true });
|
||||||
|
const includeSettings = pageSettings?.interInstanceTransferSettings !== false;
|
||||||
return (
|
return (
|
||||||
<CardFrame
|
<CardFrame
|
||||||
title={instance.name || remote.url || 'External server'}
|
title={instance.name || remote.url || 'External server'}
|
||||||
@@ -92,7 +95,11 @@ function InstancePanel({ remote, children = null }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center justify-center gap-1 text-center">
|
<div className="flex flex-wrap items-center justify-center gap-1 text-center">
|
||||||
<InstanceStatus remote={remote} />
|
<InstanceStatus remote={remote} />
|
||||||
<button type="button" className="button-dark" onClick={() => openExternalRoverWithPrompt(remote, '')}>
|
<button
|
||||||
|
type="button"
|
||||||
|
className="button-dark"
|
||||||
|
onClick={() => openExternalRover(remote, '', { includeSettings })}
|
||||||
|
>
|
||||||
Visit server
|
Visit server
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ import { useSharedClock } from '../../hooks/useSharedClock.js';
|
|||||||
import CardFrame from '../CardFrame/index.jsx';
|
import CardFrame from '../CardFrame/index.jsx';
|
||||||
import RoverLabel from '../RoverLabel/index.jsx';
|
import RoverLabel from '../RoverLabel/index.jsx';
|
||||||
import { trackAnalyticsEvent } from '../../analytics/index.js';
|
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 { ExternalInstancesCompact } from '../InterInstancePanel/index.jsx';
|
||||||
import { isFeatureEnabled } from '../../lib/features.js';
|
import { isFeatureEnabled } from '../../lib/features.js';
|
||||||
|
import { useSettingsNamespace } from '../../settings/index.js';
|
||||||
|
|
||||||
function classNames(...values) {
|
function classNames(...values) {
|
||||||
return values.filter(Boolean).join(' ');
|
return values.filter(Boolean).join(' ');
|
||||||
@@ -62,6 +63,7 @@ export default function RoverQueuesPanel({
|
|||||||
const localTurnQueues = useSessionSelector((state) => state.session?.turnQueues ?? {});
|
const localTurnQueues = useSessionSelector((state) => state.session?.turnQueues ?? {});
|
||||||
const localUsers = useSessionSelector((state) => state.session?.users ?? []);
|
const localUsers = useSessionSelector((state) => state.session?.users ?? []);
|
||||||
const interInstanceEnabled = useSessionSelector((state) => isFeatureEnabled(state, 'interInstance'));
|
const interInstanceEnabled = useSessionSelector((state) => isFeatureEnabled(state, 'interInstance'));
|
||||||
|
const { value: pageSettings } = useSettingsNamespace('page', { interInstanceTransferSettings: true });
|
||||||
const selfId = useSessionSelector((state) => state.session?.socketId || null);
|
const selfId = useSessionSelector((state) => state.session?.socketId || null);
|
||||||
const assignedRoverId = useSessionSelector((state) => String(state.session?.assignment?.roverId || '').trim());
|
const assignedRoverId = useSessionSelector((state) => String(state.session?.assignment?.roverId || '').trim());
|
||||||
const assignedRoverName = useSessionSelector((state) => {
|
const assignedRoverName = useSessionSelector((state) => {
|
||||||
@@ -75,6 +77,7 @@ export default function RoverQueuesPanel({
|
|||||||
const [rebootPending, setRebootPending] = useState(false);
|
const [rebootPending, setRebootPending] = useState(false);
|
||||||
const externalMode = Boolean(externalInstance);
|
const externalMode = Boolean(externalInstance);
|
||||||
const externalBlocked = Boolean(externalMode && disabledOverlay);
|
const externalBlocked = Boolean(externalMode && disabledOverlay);
|
||||||
|
const includeInterInstanceSettings = pageSettings?.interInstanceTransferSettings !== false;
|
||||||
const roster = Array.isArray(rosterOverride) ? rosterOverride : localRoster;
|
const roster = Array.isArray(rosterOverride) ? rosterOverride : localRoster;
|
||||||
const turnQueues = turnQueuesOverride && typeof turnQueuesOverride === 'object' ? turnQueuesOverride : localTurnQueues;
|
const turnQueues = turnQueuesOverride && typeof turnQueuesOverride === 'object' ? turnQueuesOverride : localTurnQueues;
|
||||||
const users = Array.isArray(usersOverride) ? usersOverride : localUsers;
|
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
|
External queue cards deliberately reuse the local row layout, but their
|
||||||
action cannot go through this Socket.IO server. The row opens the remote
|
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;
|
return;
|
||||||
}
|
}
|
||||||
setPending((prev) => ({ ...prev, [targetRoverId]: true }));
|
setPending((prev) => ({ ...prev, [targetRoverId]: true }));
|
||||||
|
|||||||
@@ -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() {
|
export default function SettingsPanel() {
|
||||||
const keymap = useControlSelector((control) => control.state.keymap);
|
const keymap = useControlSelector((control) => control.state.keymap);
|
||||||
const roverId = useControlSelector((control) => control.state.roverId);
|
const roverId = useControlSelector((control) => control.state.roverId);
|
||||||
@@ -103,6 +115,7 @@ export default function SettingsPanel() {
|
|||||||
connectionTransport: 'websocket',
|
connectionTransport: 'websocket',
|
||||||
swapMobileControlColumns: false,
|
swapMobileControlColumns: false,
|
||||||
driveMacroBackoffEnabled: true,
|
driveMacroBackoffEnabled: true,
|
||||||
|
interInstanceTransferSettings: true,
|
||||||
});
|
});
|
||||||
const { value: audioSettings, save: saveAudioSettings } = useSettingsNamespace('audio', AUDIO_SETTINGS_DEFAULTS);
|
const { value: audioSettings, save: saveAudioSettings } = useSettingsNamespace('audio', AUDIO_SETTINGS_DEFAULTS);
|
||||||
const { value: videoSettings, save: saveVideoSettings } = useSettingsNamespace('video', VIDEO_SETTINGS_DEFAULTS);
|
const { value: videoSettings, save: saveVideoSettings } = useSettingsNamespace('video', VIDEO_SETTINGS_DEFAULTS);
|
||||||
@@ -112,6 +125,7 @@ export default function SettingsPanel() {
|
|||||||
typeof pageSettings?.driveMacroBackoffEnabled === 'boolean'
|
typeof pageSettings?.driveMacroBackoffEnabled === 'boolean'
|
||||||
? pageSettings.driveMacroBackoffEnabled
|
? pageSettings.driveMacroBackoffEnabled
|
||||||
: true;
|
: true;
|
||||||
|
const interInstanceTransferSettings = pageSettings?.interInstanceTransferSettings !== false;
|
||||||
const masterVolume = Number.isFinite(audioSettings?.masterVolume) ? audioSettings.masterVolume : AUDIO_SETTINGS_DEFAULTS.masterVolume;
|
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 alertVolume = Number.isFinite(audioSettings?.alertVolume) ? audioSettings.alertVolume : AUDIO_SETTINGS_DEFAULTS.alertVolume;
|
||||||
const roverVolume = Number.isFinite(audioSettings?.roverVolume) ? audioSettings.roverVolume : AUDIO_SETTINGS_DEFAULTS.roverVolume;
|
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;
|
const next = event.target.value;
|
||||||
savePageSettings((current) => ({ ...(current ?? {}), connectionTransport: next }));
|
savePageSettings((current) => ({ ...(current ?? {}), connectionTransport: next }));
|
||||||
trackAnalyticsEvent('settings_change', { setting: 'connection_transport', value: next });
|
trackAnalyticsEvent('settings_change', { setting: 'connection_transport', value: next });
|
||||||
if (!socket?.io?.opts) return;
|
reconnectSocketWithTransport(socket, next);
|
||||||
socket.io.opts.transports = next === 'polling' ? ['polling'] : ['websocket', 'polling'];
|
|
||||||
socket.disconnect();
|
|
||||||
socket.connect();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAudioRange = (key) => (event) => {
|
const handleAudioRange = (key) => (event) => {
|
||||||
@@ -180,6 +191,17 @@ export default function SettingsPanel() {
|
|||||||
trackAnalyticsEvent('settings_change', { setting: 'driveMacroBackoffEnabled', value: checked });
|
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 handleVideoFilterChange = (event) => {
|
||||||
const nextFilter = normalizeVideoFilter(event.target.value);
|
const nextFilter = normalizeVideoFilter(event.target.value);
|
||||||
|
|
||||||
@@ -329,6 +351,20 @@ export default function SettingsPanel() {
|
|||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingHelp>Switching reconnects your session.</SettingHelp>
|
<SettingHelp>Switching reconnects your session.</SettingHelp>
|
||||||
</CardFrame>
|
</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>
|
</div>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel id="admin">
|
<TabPanel id="admin">
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export const DEFAULT_OVERCURRENT_LIMITS = {
|
|||||||
outputRateMs: 250,
|
outputRateMs: 250,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const RECOVERED_CAP_THRESHOLD = 0.999;
|
||||||
|
|
||||||
function createInitialCaps() {
|
function createInitialCaps() {
|
||||||
return OVERCURRENT_GROUPS.reduce((acc, group) => {
|
return OVERCURRENT_GROUPS.reduce((acc, group) => {
|
||||||
acc[group.key] = { cap: 1, clearSec: 0 };
|
acc[group.key] = { cap: 1, clearSec: 0 };
|
||||||
@@ -54,7 +56,17 @@ export function useOvercurrentLimiter(roverId, options = {}) {
|
|||||||
[overcurrentFlags],
|
[overcurrentFlags],
|
||||||
);
|
);
|
||||||
const needsRecoveryTick = useMemo(
|
const needsRecoveryTick = useMemo(
|
||||||
() => Object.values(caps || {}).some((entry) => (Number.isFinite(entry?.cap) ? entry.cap : 1) < 0.999),
|
() =>
|
||||||
|
Object.values(caps || {}).some((entry) => {
|
||||||
|
/*
|
||||||
|
Recovery intentionally completes at a tiny tolerance below exactly 1.
|
||||||
|
The limiter advances in timed floating-point steps, so requiring an
|
||||||
|
exact 1 can strand the UI at a visually empty bar while the limiter is
|
||||||
|
still technically active at a value like 0.9992.
|
||||||
|
*/
|
||||||
|
const cap = Number.isFinite(entry?.cap) ? entry.cap : 1;
|
||||||
|
return cap < RECOVERED_CAP_THRESHOLD;
|
||||||
|
}),
|
||||||
[caps],
|
[caps],
|
||||||
);
|
);
|
||||||
const shouldTick = Boolean(roverId) && (hasAnyOvercurrent || needsRecoveryTick);
|
const shouldTick = Boolean(roverId) && (hasAnyOvercurrent || needsRecoveryTick);
|
||||||
@@ -81,9 +93,15 @@ export function useOvercurrentLimiter(roverId, options = {}) {
|
|||||||
const over = group.motors.some((motor) => Boolean(flagsRef.current?.[motor]));
|
const over = group.motors.some((motor) => Boolean(flagsRef.current?.[motor]));
|
||||||
const nextClear = over ? 0 : prevClear + deltaSec;
|
const nextClear = over ? 0 : prevClear + deltaSec;
|
||||||
const allowRecover = !over && nextClear >= releaseDelay;
|
const allowRecover = !over && nextClear >= releaseDelay;
|
||||||
const nextCap = clampUnit(
|
const rawNextCap = clampUnit(
|
||||||
over ? prevCap - downRate * deltaSec : allowRecover ? prevCap + upRate * deltaSec : prevCap,
|
over ? prevCap - downRate * deltaSec : allowRecover ? prevCap + upRate * deltaSec : prevCap,
|
||||||
);
|
);
|
||||||
|
/*
|
||||||
|
Once recovery reaches the shared completion threshold, snap the cap
|
||||||
|
to exactly full strength. This keeps the tick loop, command scaling,
|
||||||
|
and HUD visibility from disagreeing over a harmless fractional tail.
|
||||||
|
*/
|
||||||
|
const nextCap = !over && rawNextCap >= RECOVERED_CAP_THRESHOLD ? 1 : rawNextCap;
|
||||||
if (Math.abs(nextCap - prevCap) > 0.0001 || Math.abs(nextClear - prevClear) > 0.0001) {
|
if (Math.abs(nextCap - prevCap) > 0.0001 || Math.abs(nextClear - prevClear) > 0.0001) {
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
@@ -137,7 +155,17 @@ export function useOvercurrentLimiter(roverId, options = {}) {
|
|||||||
caps,
|
caps,
|
||||||
overcurrent,
|
overcurrent,
|
||||||
scales,
|
scales,
|
||||||
isActive: (scales?.drive?.left ?? 1) < 1 || (scales?.drive?.right ?? 1) < 1 || (scales?.aux?.main ?? 1) < 1 || (scales?.aux?.side ?? 1) < 1,
|
/*
|
||||||
|
HUD and resend behavior should only remain active while the limiter has
|
||||||
|
meaningful scale left to recover. Using the same threshold as the tick
|
||||||
|
loop prevents an empty overcurrent overlay from staying mounted after
|
||||||
|
recovery has already stopped.
|
||||||
|
*/
|
||||||
|
isActive:
|
||||||
|
(scales?.drive?.left ?? 1) < RECOVERED_CAP_THRESHOLD ||
|
||||||
|
(scales?.drive?.right ?? 1) < RECOVERED_CAP_THRESHOLD ||
|
||||||
|
(scales?.aux?.main ?? 1) < RECOVERED_CAP_THRESHOLD ||
|
||||||
|
(scales?.aux?.side ?? 1) < RECOVERED_CAP_THRESHOLD,
|
||||||
config,
|
config,
|
||||||
adminImmune,
|
adminImmune,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ export default function useIncomingInterInstanceTransfer() {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
/*
|
/*
|
||||||
The source page already asked before adding settingsTransfer. A present
|
The source page only adds settingsTransfer when its saved Page setting
|
||||||
transfer param is therefore an explicit instruction to replace the local
|
allows it. A present transfer param is therefore an explicit instruction
|
||||||
settings cookie without asking again on the destination server.
|
to replace the local settings cookie without asking again here.
|
||||||
*/
|
*/
|
||||||
const nextSettings = base64UrlDecodeJson(transfer);
|
const nextSettings = base64UrlDecodeJson(transfer);
|
||||||
settings.saveAll(nextSettings && typeof nextSettings === 'object' ? nextSettings : {});
|
settings.saveAll(nextSettings && typeof nextSettings === 'object' ? nextSettings : {});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Inter-Instance Transfer Helpers
|
// Inter-Instance Transfer Helpers
|
||||||
// Purpose: Builds cross-server links and moves the local settings cookie only when the user opts in before leaving.
|
// Purpose: Builds cross-server links and optionally carries the local settings cookie when page settings allow it.
|
||||||
// Scope: Keeps URL encoding and settings-transfer behavior out of the rover queue rendering code.
|
// Scope: Keeps URL encoding and settings-transfer behavior out of the rover queue rendering code.
|
||||||
import { loadSettings } from '../settings/persistence.js';
|
import { loadSettings } from '../settings/persistence.js';
|
||||||
|
|
||||||
@@ -27,8 +27,9 @@ export function buildExternalRoverUrl(instance, roverId, { includeSettings = fal
|
|||||||
const url = new URL(publicUrl);
|
const url = new URL(publicUrl);
|
||||||
if (roverId) url.searchParams.set('rover', String(roverId));
|
if (roverId) url.searchParams.set('rover', String(roverId));
|
||||||
/*
|
/*
|
||||||
The destination always applies settingsTransfer if present, so this helper
|
The destination always applies settingsTransfer if present. Whether this
|
||||||
only adds it after the current page has already asked for consent.
|
source page includes the payload is controlled by the persistent Page
|
||||||
|
settings toggle, so cross-server navigation does not need a per-click popup.
|
||||||
*/
|
*/
|
||||||
if (includeSettings) {
|
if (includeSettings) {
|
||||||
url.searchParams.set('settingsTransfer', base64UrlEncodeJson(loadSettings()));
|
url.searchParams.set('settingsTransfer', base64UrlEncodeJson(loadSettings()));
|
||||||
@@ -36,13 +37,8 @@ export function buildExternalRoverUrl(instance, roverId, { includeSettings = fal
|
|||||||
return url.toString();
|
return url.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openExternalRoverWithPrompt(instance, roverId) {
|
export function openExternalRover(instance, roverId, { includeSettings = true } = {}) {
|
||||||
const withoutTransfer = buildExternalRoverUrl(instance, roverId);
|
|
||||||
if (!withoutTransfer) return;
|
|
||||||
const instanceName = String(instance?.instance?.name || instance?.url || 'that server');
|
|
||||||
const includeSettings = window.confirm(
|
|
||||||
`Transfer your identity and settings to ${instanceName}? Press Cancel to open without transferring them.`,
|
|
||||||
);
|
|
||||||
const targetUrl = buildExternalRoverUrl(instance, roverId, { includeSettings });
|
const targetUrl = buildExternalRoverUrl(instance, roverId, { includeSettings });
|
||||||
window.location.href = targetUrl || withoutTransfer;
|
if (!targetUrl) return;
|
||||||
|
window.location.href = targetUrl;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user