mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-03 07:22:31 +02:00
Carve out dead code and cruft, and unify repeater status pane
This commit is contained in:
@@ -291,7 +291,7 @@ npm run test:run
|
||||
|
||||
**E2E:**
|
||||
|
||||
Warning: these tests are only guaranteed to run correctly in a narrow subset of environments; they require a busy mesh with messages arriving constantly. E2E tests are generally not necessary to run for normal development work.
|
||||
Warning: these tests are only guaranteed to run correctly in a narrow subset of environments; they require a busy mesh with messages arriving constantly and an available autodetect-able radio, as well as a contact in the test database (which you can provide in `tests/e2e/.tmp/e2e-test.db` after an initial run). E2E tests are generally not necessary to run for normal development work.
|
||||
|
||||
```bash
|
||||
cd tests/e2e
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import type React from 'react';
|
||||
import { toast } from './ui/sonner';
|
||||
import { api } from '../api';
|
||||
import { formatTime } from '../utils/messageParser';
|
||||
import { isValidLocation, calculateDistance, formatDistance } from '../utils/pathUtils';
|
||||
import { getMapFocusHash } from '../utils/urlHash';
|
||||
import { isFavorite } from '../utils/favorites';
|
||||
import { ContactAvatar } from './ContactAvatar';
|
||||
import { ContactStatusInfo } from './ContactStatusInfo';
|
||||
import type { Contact, Conversation, Favorite, RadioConfig } from '../types';
|
||||
|
||||
interface ChatHeaderProps {
|
||||
@@ -81,88 +77,13 @@ export function ChatHeader({
|
||||
(() => {
|
||||
const contact = contacts.find((c) => c.public_key === conversation.id);
|
||||
if (!contact) return null;
|
||||
const parts: React.ReactNode[] = [];
|
||||
if (contact.last_seen) {
|
||||
parts.push(`Last heard: ${formatTime(contact.last_seen)}`);
|
||||
}
|
||||
if (contact.last_path_len === -1) {
|
||||
parts.push('flood');
|
||||
} else if (contact.last_path_len === 0) {
|
||||
parts.push(
|
||||
<span
|
||||
key="path"
|
||||
className="cursor-pointer hover:text-primary hover:underline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (window.confirm('Reset path to flood?')) {
|
||||
api.resetContactPath(contact.public_key).then(
|
||||
() => toast.success('Path reset to flood'),
|
||||
() => toast.error('Failed to reset path')
|
||||
);
|
||||
}
|
||||
}}
|
||||
title="Click to reset path to flood"
|
||||
>
|
||||
direct
|
||||
</span>
|
||||
);
|
||||
} else if (contact.last_path_len > 0) {
|
||||
parts.push(
|
||||
<span
|
||||
key="path"
|
||||
className="cursor-pointer hover:text-primary hover:underline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (window.confirm('Reset path to flood?')) {
|
||||
api.resetContactPath(contact.public_key).then(
|
||||
() => toast.success('Path reset to flood'),
|
||||
() => toast.error('Failed to reset path')
|
||||
);
|
||||
}
|
||||
}}
|
||||
title="Click to reset path to flood"
|
||||
>
|
||||
{contact.last_path_len} hop{contact.last_path_len > 1 ? 's' : ''}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (isValidLocation(contact.lat, contact.lon)) {
|
||||
const distFromUs =
|
||||
config && isValidLocation(config.lat, config.lon)
|
||||
? calculateDistance(config.lat, config.lon, contact.lat, contact.lon)
|
||||
: null;
|
||||
parts.push(
|
||||
<span key="coords">
|
||||
<span
|
||||
className="font-mono cursor-pointer hover:text-primary hover:underline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const url =
|
||||
window.location.origin +
|
||||
window.location.pathname +
|
||||
getMapFocusHash(contact.public_key);
|
||||
window.open(url, '_blank');
|
||||
}}
|
||||
title="View on map"
|
||||
>
|
||||
{contact.lat!.toFixed(3)}, {contact.lon!.toFixed(3)}
|
||||
</span>
|
||||
{distFromUs !== null && ` (${formatDistance(distFromUs)})`}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return parts.length > 0 ? (
|
||||
<span className="font-normal text-sm text-muted-foreground flex-shrink-0">
|
||||
(
|
||||
{parts.map((part, i) => (
|
||||
<span key={i}>
|
||||
{i > 0 && ', '}
|
||||
{part}
|
||||
</span>
|
||||
))}
|
||||
)
|
||||
</span>
|
||||
) : null;
|
||||
return (
|
||||
<ContactStatusInfo
|
||||
contact={contact}
|
||||
ourLat={config?.lat ?? null}
|
||||
ourLon={config?.lon ?? null}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</span>
|
||||
<div className="flex items-center gap-0.5 flex-shrink-0">
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { toast } from './ui/sonner';
|
||||
import { api } from '../api';
|
||||
import { formatTime } from '../utils/messageParser';
|
||||
import { isValidLocation, calculateDistance, formatDistance } from '../utils/pathUtils';
|
||||
import { getMapFocusHash } from '../utils/urlHash';
|
||||
import type { Contact } from '../types';
|
||||
|
||||
interface ContactStatusInfoProps {
|
||||
contact: Contact;
|
||||
ourLat: number | null;
|
||||
ourLon: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the "(Last heard: ..., N hops, lat, lon (dist))" status line
|
||||
* shared between ChatHeader and RepeaterDashboard.
|
||||
*/
|
||||
export function ContactStatusInfo({ contact, ourLat, ourLon }: ContactStatusInfoProps) {
|
||||
const parts: ReactNode[] = [];
|
||||
|
||||
if (contact.last_seen) {
|
||||
parts.push(`Last heard: ${formatTime(contact.last_seen)}`);
|
||||
}
|
||||
|
||||
if (contact.last_path_len === -1) {
|
||||
parts.push('flood');
|
||||
} else if (contact.last_path_len === 0) {
|
||||
parts.push(
|
||||
<span
|
||||
key="path"
|
||||
className="cursor-pointer hover:text-primary hover:underline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (window.confirm('Reset path to flood?')) {
|
||||
api.resetContactPath(contact.public_key).then(
|
||||
() => toast.success('Path reset to flood'),
|
||||
() => toast.error('Failed to reset path')
|
||||
);
|
||||
}
|
||||
}}
|
||||
title="Click to reset path to flood"
|
||||
>
|
||||
direct
|
||||
</span>
|
||||
);
|
||||
} else if (contact.last_path_len > 0) {
|
||||
parts.push(
|
||||
<span
|
||||
key="path"
|
||||
className="cursor-pointer hover:text-primary hover:underline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (window.confirm('Reset path to flood?')) {
|
||||
api.resetContactPath(contact.public_key).then(
|
||||
() => toast.success('Path reset to flood'),
|
||||
() => toast.error('Failed to reset path')
|
||||
);
|
||||
}
|
||||
}}
|
||||
title="Click to reset path to flood"
|
||||
>
|
||||
{contact.last_path_len} hop{contact.last_path_len > 1 ? 's' : ''}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (isValidLocation(contact.lat, contact.lon)) {
|
||||
const distFromUs =
|
||||
ourLat != null && ourLon != null && isValidLocation(ourLat, ourLon)
|
||||
? calculateDistance(ourLat, ourLon, contact.lat, contact.lon)
|
||||
: null;
|
||||
parts.push(
|
||||
<span key="coords">
|
||||
<span
|
||||
className="font-mono cursor-pointer hover:text-primary hover:underline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const url =
|
||||
window.location.origin +
|
||||
window.location.pathname +
|
||||
getMapFocusHash(contact.public_key);
|
||||
window.open(url, '_blank');
|
||||
}}
|
||||
title="View on map"
|
||||
>
|
||||
{contact.lat!.toFixed(3)}, {contact.lon!.toFixed(3)}
|
||||
</span>
|
||||
{distFromUs !== null && ` (${formatDistance(distFromUs)})`}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (parts.length === 0) return null;
|
||||
|
||||
return (
|
||||
<span className="font-normal text-sm text-muted-foreground flex-shrink-0">
|
||||
(
|
||||
{parts.map((part, i) => (
|
||||
<span key={i}>
|
||||
{i > 0 && ', '}
|
||||
{part}
|
||||
</span>
|
||||
))}
|
||||
)
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,10 @@
|
||||
import { type ReactNode } from 'react';
|
||||
import { toast } from './ui/sonner';
|
||||
import { Button } from './ui/button';
|
||||
import { RepeaterLogin } from './RepeaterLogin';
|
||||
import { useRepeaterDashboard } from '../hooks/useRepeaterDashboard';
|
||||
import { api } from '../api';
|
||||
import { formatTime } from '../utils/messageParser';
|
||||
import { isFavorite } from '../utils/favorites';
|
||||
import { ContactStatusInfo } from './ContactStatusInfo';
|
||||
import type { Contact, Conversation, Favorite } from '../types';
|
||||
import { isValidLocation, calculateDistance, formatDistance } from '../utils/pathUtils';
|
||||
import { getMapFocusHash } from '../utils/urlHash';
|
||||
import { TelemetryPane } from './repeater/RepeaterTelemetryPane';
|
||||
import { NeighborsPane } from './repeater/RepeaterNeighborsPane';
|
||||
import { AclPane } from './repeater/RepeaterAclPane';
|
||||
@@ -86,91 +82,7 @@ export function RepeaterDashboard({
|
||||
>
|
||||
{conversation.id}
|
||||
</span>
|
||||
{contact &&
|
||||
(() => {
|
||||
const parts: ReactNode[] = [];
|
||||
if (contact.last_seen) {
|
||||
parts.push(`Last heard: ${formatTime(contact.last_seen)}`);
|
||||
}
|
||||
if (contact.last_path_len === -1) {
|
||||
parts.push('flood');
|
||||
} else if (contact.last_path_len === 0) {
|
||||
parts.push(
|
||||
<span
|
||||
key="path"
|
||||
className="cursor-pointer hover:text-primary hover:underline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (window.confirm('Reset path to flood?')) {
|
||||
api.resetContactPath(contact.public_key).then(
|
||||
() => toast.success('Path reset to flood'),
|
||||
() => toast.error('Failed to reset path')
|
||||
);
|
||||
}
|
||||
}}
|
||||
title="Click to reset path to flood"
|
||||
>
|
||||
direct
|
||||
</span>
|
||||
);
|
||||
} else if (contact.last_path_len > 0) {
|
||||
parts.push(
|
||||
<span
|
||||
key="path"
|
||||
className="cursor-pointer hover:text-primary hover:underline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (window.confirm('Reset path to flood?')) {
|
||||
api.resetContactPath(contact.public_key).then(
|
||||
() => toast.success('Path reset to flood'),
|
||||
() => toast.error('Failed to reset path')
|
||||
);
|
||||
}
|
||||
}}
|
||||
title="Click to reset path to flood"
|
||||
>
|
||||
{contact.last_path_len} hop{contact.last_path_len > 1 ? 's' : ''}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (isValidLocation(contact.lat, contact.lon)) {
|
||||
const distFromUs =
|
||||
radioLat != null && radioLon != null && isValidLocation(radioLat, radioLon)
|
||||
? calculateDistance(radioLat, radioLon, contact.lat, contact.lon)
|
||||
: null;
|
||||
parts.push(
|
||||
<span key="coords">
|
||||
<span
|
||||
className="font-mono cursor-pointer hover:text-primary hover:underline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const url =
|
||||
window.location.origin +
|
||||
window.location.pathname +
|
||||
getMapFocusHash(contact.public_key);
|
||||
window.open(url, '_blank');
|
||||
}}
|
||||
title="View on map"
|
||||
>
|
||||
{contact.lat!.toFixed(3)}, {contact.lon!.toFixed(3)}
|
||||
</span>
|
||||
{distFromUs !== null && ` (${formatDistance(distFromUs)})`}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return parts.length > 0 ? (
|
||||
<span className="font-normal text-sm text-muted-foreground flex-shrink-0">
|
||||
(
|
||||
{parts.map((part, i) => (
|
||||
<span key={i}>
|
||||
{i > 0 && ', '}
|
||||
{part}
|
||||
</span>
|
||||
))}
|
||||
)
|
||||
</span>
|
||||
) : null;
|
||||
})()}
|
||||
{contact && <ContactStatusInfo contact={contact} ourLat={radioLat} ourLon={radioLon} />}
|
||||
</span>
|
||||
<div className="flex items-center gap-0.5 flex-shrink-0">
|
||||
{loggedIn && (
|
||||
|
||||
@@ -46,10 +46,6 @@ export function SettingsDatabaseSection({
|
||||
setAutoDecryptOnAdvert(appSettings.auto_decrypt_dm_on_advert);
|
||||
}, [appSettings]);
|
||||
|
||||
useEffect(() => {
|
||||
setReopenLastConversation(getReopenLastConversationEnabled());
|
||||
}, []);
|
||||
|
||||
const handleCleanup = async () => {
|
||||
const days = parseInt(retentionDays, 10);
|
||||
if (isNaN(days) || days < 1) {
|
||||
|
||||
@@ -22,7 +22,7 @@ const store: Partial<PrefetchMap> =
|
||||
type PrefetchResolved<K extends keyof PrefetchMap> = Awaited<PrefetchMap[K]>;
|
||||
|
||||
/** Take a prefetched promise (consumed once, then gone). */
|
||||
export function takePrefetch<K extends keyof PrefetchMap>(key: K): PrefetchMap[K] | undefined {
|
||||
function takePrefetch<K extends keyof PrefetchMap>(key: K): PrefetchMap[K] | undefined {
|
||||
const p = store[key];
|
||||
delete store[key];
|
||||
return p;
|
||||
|
||||
@@ -93,7 +93,6 @@ export const PARTICLE_COLOR_MAP: Record<PacketLabel, string> = {
|
||||
};
|
||||
|
||||
export const PARTICLE_SPEED = 0.008;
|
||||
export const DEFAULT_OBSERVATION_WINDOW_SEC = 15;
|
||||
// Traffic pattern analysis thresholds
|
||||
// Be conservative - once split, we can't unsplit, so require strong evidence
|
||||
const MIN_OBSERVATIONS_TO_SPLIT = 20; // Need at least this many unique sources per next-hop group
|
||||
|
||||
@@ -60,11 +60,6 @@ include = ["app"]
|
||||
exclude = ["references", ".venv", "tests"]
|
||||
reportMissingImports = true
|
||||
reportMissingTypeStubs = false
|
||||
# Temporarily lenient - fix these incrementally
|
||||
reportOptionalMemberAccess = "warning"
|
||||
reportArgumentType = "warning"
|
||||
reportReturnType = "warning"
|
||||
reportAttributeAccessIssue = "warning"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
|
||||
+1
-1
@@ -3,6 +3,6 @@ set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
|
||||
echo "Starting E2E tests (kicks off a build; this may take a few minutes..."
|
||||
echo "Starting E2E tests..."
|
||||
cd "$SCRIPT_DIR/tests/e2e"
|
||||
npx playwright test "$@"
|
||||
|
||||
@@ -35,8 +35,18 @@ export default defineConfig({
|
||||
],
|
||||
|
||||
webServer: {
|
||||
command:
|
||||
'bash -c "if [ ! -d frontend/dist ]; then cd frontend && npm install && npm run build; fi; uv run uvicorn app.main:app --host 127.0.0.1 --port 8000"',
|
||||
command: `bash -c '
|
||||
echo "[e2e] $(date +%T.%3N) Starting webServer command..."
|
||||
if [ ! -d frontend/dist ]; then
|
||||
echo "[e2e] $(date +%T.%3N) frontend/dist missing — running npm install + build"
|
||||
cd frontend && npm install && npm run build
|
||||
echo "[e2e] $(date +%T.%3N) Frontend build complete"
|
||||
else
|
||||
echo "[e2e] $(date +%T.%3N) frontend/dist exists — skipping build"
|
||||
fi
|
||||
echo "[e2e] $(date +%T.%3N) Launching uvicorn..."
|
||||
uv run uvicorn app.main:app --host 127.0.0.1 --port 8000
|
||||
'`,
|
||||
cwd: projectRoot,
|
||||
port: 8000,
|
||||
reuseExistingServer: false,
|
||||
|
||||
@@ -16,7 +16,7 @@ test.describe('Health & UI basics', () => {
|
||||
test('sidebar shows Channels and Contacts sections', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
await expect(page.getByText('Channels')).toBeVisible();
|
||||
await expect(page.getByText('Contacts')).toBeVisible();
|
||||
await expect(page.getByText('Channels', { exact: true }).first()).toBeVisible();
|
||||
await expect(page.getByText('Contacts', { exact: true }).first()).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user