mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-06 08:43:36 +02:00
Overhaul repeater interaction to better deal with login failure clearly
This commit is contained in:
@@ -76,6 +76,17 @@ function createInitialPaneData(): PaneData {
|
||||
|
||||
const repeaterDashboardCache = new Map<string, RepeaterDashboardCacheEntry>();
|
||||
|
||||
function getLoginToastTitle(status: string): string {
|
||||
switch (status) {
|
||||
case 'timeout':
|
||||
return 'Login confirmation not heard';
|
||||
case 'error':
|
||||
return 'Login not confirmed';
|
||||
default:
|
||||
return 'Repeater login not confirmed';
|
||||
}
|
||||
}
|
||||
|
||||
function clonePaneData(data: PaneData): PaneData {
|
||||
return { ...data };
|
||||
}
|
||||
@@ -255,13 +266,22 @@ export function useRepeaterDashboard(
|
||||
setLoginLoading(true);
|
||||
setLoginError(null);
|
||||
try {
|
||||
await api.repeaterLogin(publicKey, password);
|
||||
const result = await api.repeaterLogin(publicKey, password);
|
||||
if (activeIdRef.current !== conversationId) return;
|
||||
setLoggedIn(true);
|
||||
if (!result.authenticated) {
|
||||
const msg = result.message ?? 'Repeater login was not confirmed';
|
||||
setLoginError(msg);
|
||||
toast.error(getLoginToastTitle(result.status), { description: msg });
|
||||
}
|
||||
} catch (err) {
|
||||
if (activeIdRef.current !== conversationId) return;
|
||||
const msg = err instanceof Error ? err.message : 'Login failed';
|
||||
setLoggedIn(true);
|
||||
setLoginError(msg);
|
||||
toast.error('Login request failed', {
|
||||
description: `${msg}. The dashboard is still available, but repeater operations may fail until a login succeeds.`,
|
||||
});
|
||||
} finally {
|
||||
if (activeIdRef.current === conversationId) {
|
||||
setLoginLoading(false);
|
||||
|
||||
@@ -35,6 +35,8 @@ vi.mock('../components/ui/sonner', () => ({
|
||||
// Get mock reference — cast to Record<string, Mock> for type-safe mock method access
|
||||
const { api: _rawApi } = await import('../api');
|
||||
const mockApi = _rawApi as unknown as Record<string, Mock>;
|
||||
const { toast } = await import('../components/ui/sonner');
|
||||
const mockToast = toast as unknown as Record<string, Mock>;
|
||||
|
||||
const REPEATER_KEY = 'aa'.repeat(32);
|
||||
|
||||
@@ -58,7 +60,11 @@ describe('useRepeaterDashboard', () => {
|
||||
});
|
||||
|
||||
it('login sets loggedIn on success', async () => {
|
||||
mockApi.repeaterLogin.mockResolvedValueOnce({ status: 'ok' });
|
||||
mockApi.repeaterLogin.mockResolvedValueOnce({
|
||||
status: 'ok',
|
||||
authenticated: true,
|
||||
message: null,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useRepeaterDashboard(repeaterConversation));
|
||||
|
||||
@@ -72,7 +78,11 @@ describe('useRepeaterDashboard', () => {
|
||||
});
|
||||
|
||||
it('login sets error on failure', async () => {
|
||||
mockApi.repeaterLogin.mockRejectedValueOnce(new Error('Auth failed'));
|
||||
mockApi.repeaterLogin.mockResolvedValueOnce({
|
||||
status: 'error',
|
||||
authenticated: false,
|
||||
message: 'Auth failed',
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useRepeaterDashboard(repeaterConversation));
|
||||
|
||||
@@ -80,12 +90,19 @@ describe('useRepeaterDashboard', () => {
|
||||
await result.current.login('bad');
|
||||
});
|
||||
|
||||
expect(result.current.loggedIn).toBe(false);
|
||||
expect(result.current.loggedIn).toBe(true);
|
||||
expect(result.current.loginError).toBe('Auth failed');
|
||||
expect(mockToast.error).toHaveBeenCalledWith('Login not confirmed', {
|
||||
description: 'Auth failed',
|
||||
});
|
||||
});
|
||||
|
||||
it('loginAsGuest calls login with empty password', async () => {
|
||||
mockApi.repeaterLogin.mockResolvedValueOnce({ status: 'ok' });
|
||||
mockApi.repeaterLogin.mockResolvedValueOnce({
|
||||
status: 'ok',
|
||||
authenticated: true,
|
||||
message: null,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useRepeaterDashboard(repeaterConversation));
|
||||
|
||||
@@ -97,6 +114,23 @@ describe('useRepeaterDashboard', () => {
|
||||
expect(result.current.loggedIn).toBe(true);
|
||||
});
|
||||
|
||||
it('login still opens dashboard when request rejects', async () => {
|
||||
mockApi.repeaterLogin.mockRejectedValueOnce(new Error('Network error'));
|
||||
|
||||
const { result } = renderHook(() => useRepeaterDashboard(repeaterConversation));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.login('secret');
|
||||
});
|
||||
|
||||
expect(result.current.loggedIn).toBe(true);
|
||||
expect(result.current.loginError).toBe('Network error');
|
||||
expect(mockToast.error).toHaveBeenCalledWith('Login request failed', {
|
||||
description:
|
||||
'Network error. The dashboard is still available, but repeater operations may fail until a login succeeds.',
|
||||
});
|
||||
});
|
||||
|
||||
it('refreshPane stores data on success', async () => {
|
||||
const statusData = {
|
||||
battery_volts: 4.2,
|
||||
@@ -376,7 +410,11 @@ describe('useRepeaterDashboard', () => {
|
||||
|
||||
it('restores dashboard state when navigating away and back to the same repeater', async () => {
|
||||
const statusData = { battery_volts: 4.2 };
|
||||
mockApi.repeaterLogin.mockResolvedValueOnce({ status: 'ok' });
|
||||
mockApi.repeaterLogin.mockResolvedValueOnce({
|
||||
status: 'ok',
|
||||
authenticated: true,
|
||||
message: null,
|
||||
});
|
||||
mockApi.repeaterStatus.mockResolvedValueOnce(statusData);
|
||||
mockApi.sendRepeaterCommand.mockResolvedValueOnce({
|
||||
command: 'ver',
|
||||
|
||||
@@ -349,6 +349,8 @@ export interface CommandResponse {
|
||||
|
||||
export interface RepeaterLoginResponse {
|
||||
status: string;
|
||||
authenticated: boolean;
|
||||
message: string | null;
|
||||
}
|
||||
|
||||
export interface RepeaterStatusResponse {
|
||||
|
||||
Reference in New Issue
Block a user