mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
move neato and lift into activities
This commit is contained in:
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-BjmvF4mq.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-CfAeG6CY.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CdgtactY.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// Lift Service
|
||||
// Purpose: Provides a single global, verified-gated lift controller with serialized interlocked motion.
|
||||
// Purpose: Provides a single global lift controller with serialized interlocked motion.
|
||||
// Scope: Owns lift command sequencing, anti-spam controls, HA wiring, and shared state publication for UI sync.
|
||||
const EventEmitter = require('events');
|
||||
const io = require('../../globals/io');
|
||||
const logger = require('../../globals/logger').child('liftService');
|
||||
const { loadConfig } = require('../../helpers/configLoader');
|
||||
const { isVerified } = require('../verificationService');
|
||||
const { getMode, MODES } = require('../modeManager');
|
||||
const { isLockdownAdmin } = require('../roleService');
|
||||
const {
|
||||
@@ -183,7 +182,8 @@ io.on('connection', (socket) => {
|
||||
if (getMode() === MODES.LOCKDOWN && !isLockdownAdmin(socket)) {
|
||||
throw new Error('Server in lockdown');
|
||||
}
|
||||
if (!isVerified(socket)) throw new Error('VIP verification required');
|
||||
// Lift movement is now a public activity feature. Lockdown still wins
|
||||
// above because that mode is the global safety/admin gate for the room.
|
||||
const resp = await moveUp(socket.id || 'socket');
|
||||
cb({ success: true, ...resp });
|
||||
} catch (err) {
|
||||
@@ -196,7 +196,8 @@ io.on('connection', (socket) => {
|
||||
if (getMode() === MODES.LOCKDOWN && !isLockdownAdmin(socket)) {
|
||||
throw new Error('Server in lockdown');
|
||||
}
|
||||
if (!isVerified(socket)) throw new Error('VIP verification required');
|
||||
// Public access intentionally mirrors lift:up so both directions share
|
||||
// the same policy and cannot drift into different permission behavior.
|
||||
const resp = await moveDown(socket.id || 'socket');
|
||||
cb({ success: true, ...resp });
|
||||
} catch (err) {
|
||||
|
||||
@@ -265,9 +265,8 @@ io.on('connection', (socket) => {
|
||||
socket.on('neato:start', async (_, cb = () => {}) => {
|
||||
try {
|
||||
assertLockdownAccess();
|
||||
if (!isVerified(socket)) {
|
||||
throw new Error('VIP verification required');
|
||||
}
|
||||
// Neato commands are public activity features. The lockdown check above
|
||||
// remains the room-wide safety/admin gate when the server is restricted.
|
||||
await startCleaning();
|
||||
cb({ success: true });
|
||||
} catch (err) {
|
||||
@@ -278,9 +277,7 @@ io.on('connection', (socket) => {
|
||||
socket.on('neato:sendHome', async (_, cb = () => {}) => {
|
||||
try {
|
||||
assertLockdownAccess();
|
||||
if (!isVerified(socket)) {
|
||||
throw new Error('VIP verification required');
|
||||
}
|
||||
// Keep send-home public for consistency with the rest of the Neato card.
|
||||
await sendHome();
|
||||
cb({ success: true });
|
||||
} catch (err) {
|
||||
@@ -291,9 +288,7 @@ io.on('connection', (socket) => {
|
||||
socket.on('neato:locate', async (_, cb = () => {}) => {
|
||||
try {
|
||||
assertLockdownAccess();
|
||||
if (!isVerified(socket)) {
|
||||
throw new Error('VIP verification required');
|
||||
}
|
||||
// Locate is a public activity action; lockdown still blocks it above.
|
||||
await locateRobot();
|
||||
cb({ success: true });
|
||||
} catch (err) {
|
||||
@@ -304,9 +299,8 @@ io.on('connection', (socket) => {
|
||||
socket.on('neato:clearErrors', async (_, cb = () => {}) => {
|
||||
try {
|
||||
assertLockdownAccess();
|
||||
if (!isVerified(socket)) {
|
||||
throw new Error('VIP verification required');
|
||||
}
|
||||
// Error clearing is grouped with the public Neato controls so the UI does
|
||||
// not show a button that only some public users can actually run.
|
||||
await clearErrors();
|
||||
cb({ success: true });
|
||||
} catch (err) {
|
||||
@@ -317,9 +311,8 @@ io.on('connection', (socket) => {
|
||||
socket.on('neato:powerCycle', async (_, cb = () => {}) => {
|
||||
try {
|
||||
assertLockdownAccess();
|
||||
if (!isVerified(socket)) {
|
||||
throw new Error('VIP verification required');
|
||||
}
|
||||
// Power cycle follows the same public policy as the rest of the card;
|
||||
// operational safety remains controlled by lockdown mode.
|
||||
await powerCycle();
|
||||
cb({ success: true });
|
||||
} catch (err) {
|
||||
|
||||
@@ -43,6 +43,8 @@ import { useTelemetryVisualPolicy } from './context/TelemetryContext.jsx';
|
||||
import ButtonBoxPanel from './components/ButtonBoxPanel/index.jsx';
|
||||
import BarcodeGamesPanel from './components/BarcodeGamesPanel/index.jsx';
|
||||
import OdometerPanel from './components/OdometerPanel/index.jsx';
|
||||
import LiftCard from './components/LiftCard/index.jsx';
|
||||
import NeatoCard from './components/NeatoCard/index.jsx';
|
||||
import RewardRunOverlay from './components/RewardRunOverlay/index.jsx';
|
||||
import SocketConnectionPill from './components/SocketConnectionPill/index.jsx';
|
||||
import DuplicateIdentityOverlay from './components/DuplicateIdentityOverlay/index.jsx';
|
||||
@@ -195,6 +197,8 @@ function MobileFeatureTabs({
|
||||
<TabPanel id="activities">
|
||||
<div className={`flex flex-col ${themeGapClass}`}>
|
||||
<BarcodeGamesPanel />
|
||||
<LiftCard />
|
||||
<NeatoCard />
|
||||
<OdometerPanel />
|
||||
<ButtonBoxPanel />
|
||||
<KinectPanel />
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// Vip Lift Card
|
||||
// Lift Card
|
||||
// Purpose: Renders a shared lift controller panel synced from server session state.
|
||||
// Scope: Presents verified-user controls while reflecting global busy/position/cooldown state.
|
||||
// Scope: Presents public activity controls while reflecting global busy/position/cooldown state.
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
|
||||
import CardFrame from '../CardFrame/index.jsx';
|
||||
|
||||
function badgeClass(tone) {
|
||||
@@ -19,10 +20,17 @@ function positionLabel(value) {
|
||||
return '--';
|
||||
}
|
||||
|
||||
export default function VipLiftCard({ lift, onUp, onDown, fullWidth = false }) {
|
||||
export default function LiftCard() {
|
||||
/*
|
||||
The lift card is a complete Activities-tab feature, so it reads the shared
|
||||
lift state and socket actions directly. Keeping that wiring inside the card
|
||||
means each tab only decides where the card appears, and the command policy
|
||||
cannot accidentally diverge between mobile and desktop layouts.
|
||||
*/
|
||||
const lift = useSessionSelector((state) => state.session?.lift || null);
|
||||
const { liftUp, liftDown } = useSessionActions();
|
||||
const [working, setWorking] = useState('');
|
||||
const [nowMs, setNowMs] = useState(() => Date.now());
|
||||
const wrapClass = fullWidth ? 'w-full' : 'w-full max-w-xl';
|
||||
|
||||
const configured = Boolean(lift?.configured);
|
||||
const connected = Boolean(lift?.enabled && lift?.connected);
|
||||
@@ -71,7 +79,7 @@ export default function VipLiftCard({ lift, onUp, onDown, fullWidth = false }) {
|
||||
<CardFrame
|
||||
title="Lift Controls"
|
||||
|
||||
className={`relative ${wrapClass}`}
|
||||
className="relative w-full"
|
||||
bodyClassName="text-sm text-slate-200"
|
||||
actions={
|
||||
<span className={`inline-flex w-auto rounded px-1 py-0.25 text-xs font-semibold ${badgeClass(statusTone)}`}>
|
||||
@@ -108,7 +116,7 @@ export default function VipLiftCard({ lift, onUp, onDown, fullWidth = false }) {
|
||||
<button
|
||||
type="button"
|
||||
disabled={!canRun}
|
||||
onClick={() => run('down', onDown)}
|
||||
onClick={() => run('down', liftDown)}
|
||||
className={`button-dark w-full text-sm disabled:opacity-50 ${position === 'down' || activeTarget === 'down' ? 'bg-emerald-500 text-white hover:bg-emerald-500' : ''}`}
|
||||
>
|
||||
{working === 'down' || activeTarget === 'down' ? 'Lowering...' : 'Down'}
|
||||
@@ -116,7 +124,7 @@ export default function VipLiftCard({ lift, onUp, onDown, fullWidth = false }) {
|
||||
<button
|
||||
type="button"
|
||||
disabled={!canRun}
|
||||
onClick={() => run('up', onUp)}
|
||||
onClick={() => run('up', liftUp)}
|
||||
className={`button-dark w-full text-sm disabled:opacity-50 ${position === 'up' || activeTarget === 'up' ? 'bg-emerald-500 text-white hover:bg-emerald-500' : ''}`}
|
||||
>
|
||||
{working === 'up' || activeTarget === 'up' ? 'Raising...' : 'Up'}
|
||||
+24
-19
@@ -1,7 +1,8 @@
|
||||
// Vip Neato Card
|
||||
// Purpose: Defines the Vip Neato Card module and the local helpers/components used in this file.
|
||||
// Scope: Keeps behavior unchanged while isolating this concern into a clear, single-responsibility unit.
|
||||
// Neato Card
|
||||
// Purpose: Defines the public Neato activity card and the local helpers/components used in this file.
|
||||
// Scope: Keeps Neato state display and commands together because each button's availability depends on the same shared robot telemetry.
|
||||
import { useState } from 'react';
|
||||
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
|
||||
import CardFrame from '../CardFrame/index.jsx';
|
||||
|
||||
function normalizeState(value) {
|
||||
@@ -39,17 +40,21 @@ function StatusTile({ label, value, tone = 'muted', valueClass = '', hideLabel =
|
||||
);
|
||||
}
|
||||
|
||||
export default function VipNeatoCard({
|
||||
neato,
|
||||
onStart,
|
||||
onSendHome,
|
||||
onLocate,
|
||||
onClearErrors,
|
||||
onPowerCycle,
|
||||
fullWidth = false,
|
||||
}) {
|
||||
export default function NeatoCard() {
|
||||
/*
|
||||
Neato is a standalone public activity card. It owns its session selector and
|
||||
command actions so callers do not need to know the socket event names or
|
||||
keep a parallel list of Neato props in every tab layout.
|
||||
*/
|
||||
const neato = useSessionSelector((state) => state.session?.neato || null);
|
||||
const {
|
||||
neatoStart,
|
||||
neatoSendHome,
|
||||
neatoLocate,
|
||||
neatoClearErrors,
|
||||
neatoPowerCycle,
|
||||
} = useSessionActions();
|
||||
const [working, setWorking] = useState('');
|
||||
const wrapClass = fullWidth ? 'w-full' : 'w-full max-w-xl';
|
||||
|
||||
const configured = Boolean(neato?.configured);
|
||||
const connected = Boolean(neato?.enabled && neato?.connected);
|
||||
@@ -102,7 +107,7 @@ export default function VipNeatoCard({
|
||||
<CardFrame
|
||||
title="Neato Controls"
|
||||
|
||||
className={wrapClass}
|
||||
className="w-full"
|
||||
bodyClassName="text-sm text-slate-200"
|
||||
actions={
|
||||
<span className={`inline-flex w-auto rounded px-1 py-0.25 text-xs font-semibold ${metricToneClass(headerTone)}`}>
|
||||
@@ -121,7 +126,7 @@ export default function VipNeatoCard({
|
||||
<button
|
||||
type="button"
|
||||
disabled={!canRunStart || Boolean(working)}
|
||||
onClick={() => runAction('start', onStart)}
|
||||
onClick={() => runAction('start', neatoStart)}
|
||||
className="rounded-md border border-sky-300 bg-emerald-600 px-1 py-1 text-base font-semibold text-white transition hover:border-sky-500 hover:bg-emerald-500 disabled:opacity-50"
|
||||
>
|
||||
{working === 'start' ? 'Starting...' : 'Start cleaning'}
|
||||
@@ -129,7 +134,7 @@ export default function VipNeatoCard({
|
||||
<button
|
||||
type="button"
|
||||
disabled={!canRunSendHome || Boolean(working)}
|
||||
onClick={() => runAction('sendHome', onSendHome)}
|
||||
onClick={() => runAction('sendHome', neatoSendHome)}
|
||||
className="rounded-md border border-sky-300 bg-sky-600 px-1 py-1 text-base font-semibold text-white transition hover:border-sky-500 hover:bg-sky-500 disabled:opacity-50"
|
||||
>
|
||||
{working === 'sendHome' ? 'Sending...' : 'Send to dock'}
|
||||
@@ -138,7 +143,7 @@ export default function VipNeatoCard({
|
||||
<button
|
||||
type="button"
|
||||
disabled={!canRunLocate || Boolean(working)}
|
||||
onClick={() => runAction('locate', onLocate)}
|
||||
onClick={() => runAction('locate', neatoLocate)}
|
||||
className="w-full rounded-md border border-sky-300 bg-fuchsia-600 px-1 py-0.5 text-xs font-semibold text-white transition hover:border-sky-500 hover:bg-fuchsia-500 disabled:opacity-50"
|
||||
>
|
||||
{working === 'locate' ? 'Playing...' : 'Play sound'}
|
||||
@@ -146,7 +151,7 @@ export default function VipNeatoCard({
|
||||
<button
|
||||
type="button"
|
||||
disabled={!canRunClearErrors || Boolean(working)}
|
||||
onClick={() => runAction('clearErrors', onClearErrors)}
|
||||
onClick={() => runAction('clearErrors', neatoClearErrors)}
|
||||
className="w-full rounded-md border border-sky-300 bg-amber-500 px-1 py-0.5 text-xs font-semibold text-slate-900 transition hover:border-sky-500 hover:bg-amber-400 disabled:opacity-50"
|
||||
>
|
||||
{working === 'clearErrors' ? 'Clearing...' : 'Clear errors'}
|
||||
@@ -154,7 +159,7 @@ export default function VipNeatoCard({
|
||||
<button
|
||||
type="button"
|
||||
disabled={!canRunPowerCycle || Boolean(working)}
|
||||
onClick={() => runAction('powerCycle', onPowerCycle)}
|
||||
onClick={() => runAction('powerCycle', neatoPowerCycle)}
|
||||
className="w-full rounded-md border border-rose-300 bg-rose-600 px-1 py-0.5 text-xs font-semibold text-white transition hover:border-rose-500 hover:bg-rose-500 disabled:opacity-50"
|
||||
>
|
||||
{working === 'powerCycle' ? 'Cycling...' : 'Power cycle'}
|
||||
@@ -28,6 +28,8 @@ import { useSettingsNamespace } from '../../settings/index.js';
|
||||
import ButtonBoxPanel from '../ButtonBoxPanel/index.jsx';
|
||||
import BarcodeGamesPanel from '../BarcodeGamesPanel/index.jsx';
|
||||
import OdometerPanel from '../OdometerPanel/index.jsx';
|
||||
import LiftCard from '../LiftCard/index.jsx';
|
||||
import NeatoCard from '../NeatoCard/index.jsx';
|
||||
import OverseerPreferencePanel from '../OverseerPreferencePanel/index.jsx';
|
||||
import CardFrame from '../CardFrame/index.jsx';
|
||||
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
@@ -367,6 +369,8 @@ export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
|
||||
<TabPanel id="activities">
|
||||
<div className={`flex flex-col ${themeGapClass}`}>
|
||||
<BarcodeGamesPanel />
|
||||
<LiftCard />
|
||||
<NeatoCard />
|
||||
<OdometerPanel />
|
||||
<ButtonBoxPanel />
|
||||
<KinectPanel />
|
||||
|
||||
@@ -9,8 +9,6 @@ import VipAudioUploadCard from '../vip/VipAudioUploadCard/index.jsx';
|
||||
import VipVerificationCard from '../vip/VipVerificationCard.jsx';
|
||||
import VipIdentityCard from '../vip/VipIdentityCard.jsx';
|
||||
import VipPrivateRoverAccessCard from '../vip/VipPrivateRoverAccessCard.jsx';
|
||||
import VipNeatoCard from '../vip/VipNeatoCard.jsx';
|
||||
import VipLiftCard from '../vip/VipLiftCard.jsx';
|
||||
import VipProfileImageCard from '../vip/VipProfileImageCard.jsx';
|
||||
|
||||
export default function VipPanel({ isActive = true }) {
|
||||
@@ -24,13 +22,6 @@ export default function VipPanel({ isActive = true }) {
|
||||
startMicWhip,
|
||||
readyMicWhip,
|
||||
stopMicWhip,
|
||||
neatoStart,
|
||||
neatoSendHome,
|
||||
neatoLocate,
|
||||
neatoClearErrors,
|
||||
neatoPowerCycle,
|
||||
liftUp,
|
||||
liftDown,
|
||||
} = useSessionActions();
|
||||
const { value: identity, save: saveIdentity } = useSettingsNamespace('identity', { cookieUserId: '' });
|
||||
const { value: profile } = useSettingsNamespace('profile', { nickname: '' });
|
||||
@@ -77,21 +68,6 @@ export default function VipPanel({ isActive = true }) {
|
||||
<div className="lg:col-span-2">
|
||||
{isVerified ? (
|
||||
<div className="space-y-2">
|
||||
<VipNeatoCard
|
||||
neato={session?.neato || null}
|
||||
onStart={neatoStart}
|
||||
onSendHome={neatoSendHome}
|
||||
onLocate={neatoLocate}
|
||||
onClearErrors={neatoClearErrors}
|
||||
onPowerCycle={neatoPowerCycle}
|
||||
fullWidth
|
||||
/>
|
||||
<VipLiftCard
|
||||
lift={session?.lift || null}
|
||||
onUp={liftUp}
|
||||
onDown={liftDown}
|
||||
fullWidth
|
||||
/>
|
||||
<VipAudioUploadCard
|
||||
ownRoverId={ownRoverId}
|
||||
audioForwardByRover={session?.audioForward || {}}
|
||||
@@ -105,7 +81,7 @@ export default function VipPanel({ isActive = true }) {
|
||||
) : (
|
||||
<section className="surface h-full">
|
||||
<div className="flex h-full flex-col items-center justify-center text-center text-xs text-slate-400">
|
||||
Verify your account to unlock VIP controls.
|
||||
Verify your account to unlock VIP features.
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user