mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -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/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-B3q8p1Xf.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-htN8tzuK.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BfZ_TWsb.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
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
|
||||
3. add more background gap themes
|
||||
4. fix this:
|
||||
3. add prefix config for discord bot to replace rs with something else
|
||||
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]: cb({ error: err.message });
|
||||
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 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 }));
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -17,6 +17,8 @@ export const DEFAULT_OVERCURRENT_LIMITS = {
|
||||
outputRateMs: 250,
|
||||
};
|
||||
|
||||
const RECOVERED_CAP_THRESHOLD = 0.999;
|
||||
|
||||
function createInitialCaps() {
|
||||
return OVERCURRENT_GROUPS.reduce((acc, group) => {
|
||||
acc[group.key] = { cap: 1, clearSec: 0 };
|
||||
@@ -54,7 +56,17 @@ export function useOvercurrentLimiter(roverId, options = {}) {
|
||||
[overcurrentFlags],
|
||||
);
|
||||
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],
|
||||
);
|
||||
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 nextClear = over ? 0 : prevClear + deltaSec;
|
||||
const allowRecover = !over && nextClear >= releaseDelay;
|
||||
const nextCap = clampUnit(
|
||||
const rawNextCap = clampUnit(
|
||||
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) {
|
||||
changed = true;
|
||||
}
|
||||
@@ -137,7 +155,17 @@ export function useOvercurrentLimiter(roverId, options = {}) {
|
||||
caps,
|
||||
overcurrent,
|
||||
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,
|
||||
adminImmune,
|
||||
}),
|
||||
|
||||
@@ -25,9 +25,9 @@ export default function useIncomingInterInstanceTransfer() {
|
||||
}
|
||||
try {
|
||||
/*
|
||||
The source page already asked before adding settingsTransfer. A present
|
||||
transfer param is therefore an explicit instruction to replace the local
|
||||
settings cookie without asking again on the destination server.
|
||||
The source page only adds settingsTransfer when its saved Page setting
|
||||
allows it. A present transfer param is therefore an explicit instruction
|
||||
to replace the local settings cookie without asking again here.
|
||||
*/
|
||||
const nextSettings = base64UrlDecodeJson(transfer);
|
||||
settings.saveAll(nextSettings && typeof nextSettings === 'object' ? nextSettings : {});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// 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.
|
||||
import { loadSettings } from '../settings/persistence.js';
|
||||
|
||||
@@ -27,8 +27,9 @@ export function buildExternalRoverUrl(instance, roverId, { includeSettings = fal
|
||||
const url = new URL(publicUrl);
|
||||
if (roverId) url.searchParams.set('rover', String(roverId));
|
||||
/*
|
||||
The destination always applies settingsTransfer if present, so this helper
|
||||
only adds it after the current page has already asked for consent.
|
||||
The destination always applies settingsTransfer if present. Whether this
|
||||
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) {
|
||||
url.searchParams.set('settingsTransfer', base64UrlEncodeJson(loadSettings()));
|
||||
@@ -36,13 +37,8 @@ export function buildExternalRoverUrl(instance, roverId, { includeSettings = fal
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
export function openExternalRoverWithPrompt(instance, roverId) {
|
||||
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.`,
|
||||
);
|
||||
export function openExternalRover(instance, roverId, { includeSettings = true } = {}) {
|
||||
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