mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-12 03:33:03 +02:00
Persist login status for room servers. Closes #244.
This commit is contained in:
@@ -61,38 +61,107 @@ function createInitialPaneStates(): RoomPaneStates {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createInitialPaneData(): RoomPaneData {
|
||||||
|
return { status: null, acl: null, lppTelemetry: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// In-memory LRU cache so room login state survives conversation switches
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
interface RoomCacheEntry {
|
||||||
|
authenticated: boolean;
|
||||||
|
loginError: string | null;
|
||||||
|
lastLoginAttempt: ServerLoginAttemptState | null;
|
||||||
|
paneData: RoomPaneData;
|
||||||
|
paneStates: RoomPaneStates;
|
||||||
|
consoleHistory: ConsoleEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_CACHED_ROOMS = 8;
|
||||||
|
const roomCache = new Map<string, RoomCacheEntry>();
|
||||||
|
|
||||||
|
function getCachedRoom(publicKey: string): RoomCacheEntry | null {
|
||||||
|
const cached = roomCache.get(publicKey);
|
||||||
|
if (!cached) return null;
|
||||||
|
// Touch for LRU
|
||||||
|
roomCache.delete(publicKey);
|
||||||
|
roomCache.set(publicKey, cached);
|
||||||
|
return {
|
||||||
|
...cached,
|
||||||
|
paneData: { ...cached.paneData },
|
||||||
|
paneStates: {
|
||||||
|
status: { ...cached.paneStates.status, loading: false },
|
||||||
|
acl: { ...cached.paneStates.acl, loading: false },
|
||||||
|
lppTelemetry: { ...cached.paneStates.lppTelemetry, loading: false },
|
||||||
|
},
|
||||||
|
consoleHistory: cached.consoleHistory.map((e) => ({ ...e })),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCachedRoom(publicKey: string, entry: RoomCacheEntry) {
|
||||||
|
roomCache.delete(publicKey);
|
||||||
|
roomCache.set(publicKey, {
|
||||||
|
...entry,
|
||||||
|
paneData: { ...entry.paneData },
|
||||||
|
paneStates: {
|
||||||
|
status: { ...entry.paneStates.status, loading: false },
|
||||||
|
acl: { ...entry.paneStates.acl, loading: false },
|
||||||
|
lppTelemetry: { ...entry.paneStates.lppTelemetry, loading: false },
|
||||||
|
},
|
||||||
|
consoleHistory: entry.consoleHistory.map((e) => ({ ...e })),
|
||||||
|
});
|
||||||
|
if (roomCache.size > MAX_CACHED_ROOMS) {
|
||||||
|
const lruKey = roomCache.keys().next().value as string | undefined;
|
||||||
|
if (lruKey) roomCache.delete(lruKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetRoomCacheForTests() {
|
||||||
|
roomCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
export function RoomServerPanel({ contact, onAuthenticatedChange }: RoomServerPanelProps) {
|
export function RoomServerPanel({ contact, onAuthenticatedChange }: RoomServerPanelProps) {
|
||||||
const { password, setPassword, rememberPassword, setRememberPassword, persistAfterLogin } =
|
const { password, setPassword, rememberPassword, setRememberPassword, persistAfterLogin } =
|
||||||
useRememberedServerPassword('room', contact.public_key);
|
useRememberedServerPassword('room', contact.public_key);
|
||||||
|
|
||||||
|
const cached = useMemo(() => getCachedRoom(contact.public_key), [contact.public_key]);
|
||||||
|
|
||||||
const [loginLoading, setLoginLoading] = useState(false);
|
const [loginLoading, setLoginLoading] = useState(false);
|
||||||
const [loginError, setLoginError] = useState<string | null>(null);
|
const [loginError, setLoginError] = useState<string | null>(cached?.loginError ?? null);
|
||||||
const [authenticated, setAuthenticated] = useState(false);
|
const [authenticated, setAuthenticated] = useState(cached?.authenticated ?? false);
|
||||||
const [lastLoginAttempt, setLastLoginAttempt] = useState<ServerLoginAttemptState | null>(null);
|
const [lastLoginAttempt, setLastLoginAttempt] = useState<ServerLoginAttemptState | null>(
|
||||||
|
cached?.lastLoginAttempt ?? null
|
||||||
|
);
|
||||||
const [advancedOpen, setAdvancedOpen] = useState(false);
|
const [advancedOpen, setAdvancedOpen] = useState(false);
|
||||||
const [paneData, setPaneData] = useState<RoomPaneData>({
|
const [paneData, setPaneData] = useState<RoomPaneData>(cached?.paneData ?? createInitialPaneData);
|
||||||
status: null,
|
const [paneStates, setPaneStates] = useState<RoomPaneStates>(
|
||||||
acl: null,
|
cached?.paneStates ?? createInitialPaneStates
|
||||||
lppTelemetry: null,
|
);
|
||||||
});
|
const [consoleHistory, setConsoleHistory] = useState<ConsoleEntry[]>(
|
||||||
const [paneStates, setPaneStates] = useState<RoomPaneStates>(createInitialPaneStates);
|
cached?.consoleHistory ?? []
|
||||||
const [consoleHistory, setConsoleHistory] = useState<ConsoleEntry[]>([]);
|
);
|
||||||
const [consoleLoading, setConsoleLoading] = useState(false);
|
const [consoleLoading, setConsoleLoading] = useState(false);
|
||||||
|
|
||||||
|
// Persist to cache on every state change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoginLoading(false);
|
setCachedRoom(contact.public_key, {
|
||||||
setLoginError(null);
|
authenticated,
|
||||||
setAuthenticated(false);
|
loginError,
|
||||||
setLastLoginAttempt(null);
|
lastLoginAttempt,
|
||||||
setAdvancedOpen(false);
|
paneData,
|
||||||
setPaneData({
|
paneStates,
|
||||||
status: null,
|
consoleHistory,
|
||||||
acl: null,
|
|
||||||
lppTelemetry: null,
|
|
||||||
});
|
});
|
||||||
setPaneStates(createInitialPaneStates());
|
}, [
|
||||||
setConsoleHistory([]);
|
contact.public_key,
|
||||||
setConsoleLoading(false);
|
authenticated,
|
||||||
}, [contact.public_key]);
|
loginError,
|
||||||
|
lastLoginAttempt,
|
||||||
|
paneData,
|
||||||
|
paneStates,
|
||||||
|
consoleHistory,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onAuthenticatedChange?.(authenticated);
|
onAuthenticatedChange?.(authenticated);
|
||||||
|
|||||||
@@ -1545,6 +1545,9 @@ function MqttCommunityConfigEditor({
|
|||||||
<option value="none">None</option>
|
<option value="none">None</option>
|
||||||
<option value="password">Username / Password</option>
|
<option value="password">Username / Password</option>
|
||||||
</select>
|
</select>
|
||||||
|
<p className="text-[0.8125rem] text-muted-foreground">
|
||||||
|
LetsMesh uses <code>token</code> auth. MeshRank uses <code>none</code>.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1566,10 +1569,6 @@ function MqttCommunityConfigEditor({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-[0.8125rem] text-muted-foreground">
|
|
||||||
LetsMesh uses <code>token</code> auth. MeshRank uses <code>none</code>.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{authMode === 'token' && (
|
{authMode === 'token' && (
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||||
import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
|
import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
|
||||||
|
|
||||||
import { RoomServerPanel } from '../components/RoomServerPanel';
|
import { RoomServerPanel, resetRoomCacheForTests } from '../components/RoomServerPanel';
|
||||||
import type { Contact } from '../types';
|
import type { Contact } from '../types';
|
||||||
|
|
||||||
vi.mock('../api', () => ({
|
vi.mock('../api', () => ({
|
||||||
@@ -50,6 +50,7 @@ describe('RoomServerPanel', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
|
resetRoomCacheForTests();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps room controls available when login is not confirmed', async () => {
|
it('keeps room controls available when login is not confirmed', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user