- {savedCredentialNodeIds.length > 0 && (
+ {savedCredentialEntries.length > 0 && (
)}
- {repeaters.length === 0 ? (
+ {infraNodes.length === 0 ? (
{t('repeatersPanel.noRepeatersYet')}
@@ -967,7 +1085,7 @@ export default function RepeatersPanel({
-
+
{onToggleFavorite ? (
@@ -1288,6 +1417,19 @@ export default function RepeatersPanel({
{t('repeatersPanel.buttonCli')}
))}
+ {onOpenRoom && node.hw_model === 'Room' ? (
+ {
+ onOpenRoom(node.node_id);
+ }}
+ disabled={!isConnected}
+ className="rounded border border-purple-700 bg-purple-900/50 px-2 py-0.5 text-xs font-medium text-purple-300 transition-colors hover:bg-purple-800/60 disabled:opacity-40"
+ aria-label={t('repeatersPanel.openRoom')}
+ >
+ {t('repeatersPanel.openRoom')}
+
+ ) : null}
void handleDelete(node.node_id)}
@@ -1583,22 +1725,8 @@ export default function RepeatersPanel({
{t('repeatersPanel.cliQuick')}
{[
- 'name',
- 'radio',
- 'neighbors',
- 'version',
- 'status',
- 'config',
- 'help',
- 'clock',
- 'clock sync',
- 'clear stats',
- 'advert',
- 'board',
- 'get path.hash.mode',
- 'set path.hash.mode 0',
- 'set path.hash.mode 1',
- 'set path.hash.mode 2',
+ ...SHARED_CLI_QUICK_COMMANDS,
+ ...(node.hw_model === 'Room' ? ROOM_CLI_QUICK_COMMANDS : []),
].map((cmd) => {
const pathHashLabelKey =
cmd === 'get path.hash.mode'
@@ -1611,6 +1739,16 @@ export default function RepeatersPanel({
? 'repeatersPanel.pathHashCliSet2'
: null;
const ariaLabel = pathHashLabelKey ? t(pathHashLabelKey) : cmd;
+ const shortLabel =
+ cmd === 'get path.hash.mode'
+ ? 'path.hash'
+ : cmd.startsWith('set path.hash.mode')
+ ? cmd.replace('set path.hash.mode ', 'hash ')
+ : cmd === 'allow.read.only on'
+ ? 'ro on'
+ : cmd === 'allow.read.only off'
+ ? 'ro off'
+ : cmd;
return (
- {cmd === 'get path.hash.mode'
- ? 'path.hash'
- : cmd.startsWith('set path.hash.mode')
- ? cmd.replace('set path.hash.mode ', 'hash ')
- : cmd}
+ {shortLabel}
);
})}
+ {node.hw_model === 'Room' ? (
+ {
+ await handleCliCommand(
+ node.node_id,
+ `setperm ${pubkeyHex} ${level}`,
+ );
+ }}
+ />
+ ) : null}
{showCliMultiHopHint ? (
{t('repeatersPanel.cliMultiHopHint')}
diff --git a/src/renderer/components/RoomsPanel.test.tsx b/src/renderer/components/RoomsPanel.test.tsx
index ccf09f5f8..498d6dcae 100644
--- a/src/renderer/components/RoomsPanel.test.tsx
+++ b/src/renderer/components/RoomsPanel.test.tsx
@@ -178,7 +178,7 @@ describe('RoomsPanel', () => {
});
});
- it('forces admin relogin when managing from a read-only session', async () => {
+ it('forces admin relogin when managing from a read-only session', () => {
const room = makeRoom(0x100c, 'Admin Elevate Room');
const nodes = new Map([[room.node_id, room]]);
meshcoreApplyRoomSession(room.node_id, {
@@ -186,21 +186,14 @@ describe('RoomsPanel', () => {
adminPassword: '',
role: 'readonly',
});
- const onLoginRoom = vi.fn().mockResolvedValue(undefined);
- renderRoomsPanel(nodes, { initialRoomTarget: room.node_id, onLoginRoom });
+ const onOpenRepeaterOps = vi.fn();
+ renderRoomsPanel(nodes, { initialRoomTarget: room.node_id, onOpenRepeaterOps });
fireEvent.click(screen.getByText('roomsPanel.manageRoom'));
- await waitFor(() => {
- expect(onLoginRoom).toHaveBeenCalledWith(
- room.node_id,
- 'password',
- expect.objectContaining({
- adminPassword: 'password',
- forceRelogin: true,
- }),
- );
- });
+ expect(onOpenRepeaterOps).toHaveBeenCalledWith(room.node_id);
+ expect(screen.queryByText('roomsPanel.cliPlaceholder')).not.toBeInTheDocument();
+ expect(screen.queryByText('roomsPanel.aclPubkeyLabel')).not.toBeInTheDocument();
});
it('shows login form for room B while room A login is in progress', () => {
@@ -422,7 +415,25 @@ describe('RoomsPanel', () => {
expect(textarea).toHaveValue('retry me');
});
- it('closes manage section when Close is clicked', async () => {
+ it('Refresh ACL in Members calls get acl', async () => {
+ meshcoreClearAllRoomSessions();
+ const room = makeRoom(0x1010, 'ACL Room');
+ const nodes = new Map([[room.node_id, room]]);
+ meshcoreApplyRoomSession(room.node_id, {
+ guestPassword: '',
+ adminPassword: 'password',
+ role: 'admin',
+ });
+ const onSendRoomAdminCli = vi.fn().mockResolvedValue('aabbccdd 3\n');
+ renderRoomsPanel(nodes, { initialRoomTarget: room.node_id, onSendRoomAdminCli });
+ fireEvent.click(screen.getByText(/roomsPanel.membersHeading/));
+ fireEvent.click(screen.getByLabelText('roomsPanel.membersRefreshAcl'));
+ await waitFor(() => {
+ expect(onSendRoomAdminCli).toHaveBeenCalledWith(room.node_id, 'get acl');
+ });
+ });
+
+ it('jumps to Repeaters & Rooms ops from Manage without opening CLI drawer', () => {
meshcoreClearAllRoomSessions();
const room = makeRoom(0x1009, 'Admin Room');
const nodes = new Map([[room.node_id, room]]);
@@ -431,6 +442,7 @@ describe('RoomsPanel', () => {
adminPassword: 'password',
role: 'admin',
});
+ const onOpenRepeaterOps = vi.fn();
render(
{
onLeaveRoom={vi.fn().mockResolvedValue(undefined)}
onSendRoomPost={vi.fn()}
onSendRoomAdminCli={vi.fn()}
+ onOpenRepeaterOps={onOpenRepeaterOps}
/>,
);
fireEvent.click(screen.getByText('roomsPanel.manageRoom'));
- await waitFor(() => {
- expect(screen.getByText('roomsPanel.manageHeading')).toBeInTheDocument();
- });
- fireEvent.click(screen.getByLabelText('roomsPanel.closeManage'));
- expect(screen.queryByText('roomsPanel.cliPlaceholder')).not.toBeInTheDocument();
+ expect(onOpenRepeaterOps).toHaveBeenCalledWith(room.node_id);
+ expect(screen.queryByText('roomsPanel.manageHeading')).not.toBeInTheDocument();
+ expect(screen.queryByPlaceholderText('roomsPanel.cliPlaceholder')).not.toBeInTheDocument();
});
it('shows delivery status badge on own room posts', () => {
diff --git a/src/renderer/components/RoomsPanel.tsx b/src/renderer/components/RoomsPanel.tsx
index 4f290dee9..aa76daaf8 100644
--- a/src/renderer/components/RoomsPanel.tsx
+++ b/src/renderer/components/RoomsPanel.tsx
@@ -26,7 +26,6 @@ import {
} from 'react';
import { useTranslation } from 'react-i18next';
-import { useMeshcoreRoomAuth } from '@/renderer/hooks/useMeshcoreRoomAuth';
import { useMeshcoreRoomLoginQueueRevision } from '@/renderer/hooks/useMeshcoreRoomLoginQueueRevision';
import { useMeshcoreRoomSessionRevision } from '@/renderer/hooks/useMeshcoreRoomSessionRevision';
import { useAppWindowActivity } from '@/renderer/lib/appWindowActivity';
@@ -45,7 +44,6 @@ import { ROOM_LOGIN_PROGRESS_DOT } from '@/renderer/lib/connectionHeaderStatus';
import { errLikeToLogString } from '@/renderer/lib/errLikeToLogString';
import { ICON_MD } from '@/renderer/lib/icons/iconClass';
import { useParentIconTrigger } from '@/renderer/lib/icons/iconMotionContext';
-import type { CliHistoryEntry } from '@/renderer/lib/meshcore/meshcoreHookTypes';
import { repairMeshcoreHydrationStaleRoomSends } from '@/renderer/lib/meshcoreDbCacheHydration';
import {
type MeshcoreRoomAclEntry,
@@ -176,9 +174,8 @@ interface Props {
onLeaveRoom: (nodeId: number) => Promise;
onSendRoomPost: (nodeId: number, text: string) => Promise;
onSendRoomAdminCli: (nodeId: number, command: string) => Promise;
- meshcoreCliHistories?: Map;
- meshcoreCliErrors?: Map;
- onClearCliHistory?: (nodeId: number) => void;
+ /** Jump to Repeaters & Rooms ops for this room (infrastructure CLI / ACL). */
+ onOpenRepeaterOps?: (nodeId: number) => void;
onMessageNode?: (nodeNum: number) => void;
onToggleFavorite?: (nodeId: number, favorited: boolean) => void;
/** Ref for scroll-to-top (Rooms tab inner message stream). */
@@ -253,9 +250,7 @@ export default function RoomsPanel({
onLeaveRoom,
onSendRoomPost,
onSendRoomAdminCli,
- meshcoreCliHistories,
- meshcoreCliErrors,
- onClearCliHistory,
+ onOpenRepeaterOps,
onMessageNode,
onToggleFavorite,
scrollToTopRef,
@@ -266,7 +261,6 @@ export default function RoomsPanel({
const { t } = useTranslation();
const { inactive: appWindowInactive } = useAppWindowActivity();
const parentIconTrigger = useParentIconTrigger();
- const { ensureRoomAuth, RemoteAuthModal } = useMeshcoreRoomAuth();
const [selectedRoomId, setSelectedRoomId] = useState(
() => initialRoomTarget ?? null,
);
@@ -277,11 +271,6 @@ export default function RoomsPanel({
const [loginErrorsByRoom, setLoginErrorsByRoom] = useState |