From 1b76211d535b0ae0dc4a3635b896f9b857fee7a9 Mon Sep 17 00:00:00 2001 From: Jack Kingsman Date: Tue, 24 Feb 2026 19:11:51 -0800 Subject: [PATCH] More code rip out --- app/decoder.py | 1 - frontend/src/components/SettingsModal.tsx | 8 +- frontend/src/hooks/useAirtimeTracking.ts | 2 +- frontend/src/hooks/useAppSettings.ts | 1 - frontend/src/hooks/useConversationMessages.ts | 4 +- frontend/src/hooks/useRepeaterMode.ts | 10 +- frontend/src/hooks/useUnreadCounts.ts | 2 +- frontend/src/messageCache.ts | 7 +- frontend/src/test/contactAvatar.test.ts | 106 +++----- frontend/src/test/messageCache.test.ts | 19 +- frontend/src/test/pathUtils.test.ts | 45 ---- frontend/src/test/settingsModal.test.tsx | 2 +- frontend/src/test/urlHash.test.ts | 119 +-------- frontend/src/test/useRepeaterMode.test.ts | 241 ------------------ frontend/src/types.ts | 8 +- frontend/src/utils/contactAvatar.ts | 4 +- frontend/src/utils/pathUtils.ts | 2 +- frontend/src/utils/radioPresets.ts | 2 +- frontend/src/utils/urlHash.ts | 2 +- frontend/src/utils/visualizerUtils.ts | 18 +- 20 files changed, 70 insertions(+), 533 deletions(-) delete mode 100644 frontend/src/test/useRepeaterMode.test.ts diff --git a/app/decoder.py b/app/decoder.py index 5d0cefb..e24eab9 100644 --- a/app/decoder.py +++ b/app/decoder.py @@ -327,7 +327,6 @@ def parse_advertisement(payload: bytes) -> ParsedAdvertisement | None: # Parse fixed-position fields public_key = payload[0:32].hex() timestamp = int.from_bytes(payload[32:36], byteorder="little") - # signature = payload[36:100] # Not currently verified flags = payload[100] # Parse flags diff --git a/frontend/src/components/SettingsModal.tsx b/frontend/src/components/SettingsModal.tsx index 9eceff6..e7a43ae 100644 --- a/frontend/src/components/SettingsModal.tsx +++ b/frontend/src/components/SettingsModal.tsx @@ -26,13 +26,7 @@ import { } from '../utils/lastViewedConversation'; import { RADIO_PRESETS } from '../utils/radioPresets'; -// Import for local use + re-export so existing imports from this file still work -import { - SETTINGS_SECTION_ORDER, - SETTINGS_SECTION_LABELS, - type SettingsSection, -} from './settingsConstants'; -export { SETTINGS_SECTION_ORDER, SETTINGS_SECTION_LABELS, type SettingsSection }; +import { SETTINGS_SECTION_LABELS, type SettingsSection } from './settingsConstants'; interface SettingsModalBaseProps { open: boolean; diff --git a/frontend/src/hooks/useAirtimeTracking.ts b/frontend/src/hooks/useAirtimeTracking.ts index bee3406..c33e708 100644 --- a/frontend/src/hooks/useAirtimeTracking.ts +++ b/frontend/src/hooks/useAirtimeTracking.ts @@ -108,7 +108,7 @@ function createLocalMessage(conversationKey: string, text: string, outgoing: boo }; } -export interface UseAirtimeTrackingResult { +interface UseAirtimeTrackingResult { /** Returns true if this was an airtime command that was handled */ handleAirtimeCommand: (command: string, conversationId: string) => Promise; /** Stop any active airtime tracking */ diff --git a/frontend/src/hooks/useAppSettings.ts b/frontend/src/hooks/useAppSettings.ts index e6496e9..89b4fed 100644 --- a/frontend/src/hooks/useAppSettings.ts +++ b/frontend/src/hooks/useAppSettings.ts @@ -144,7 +144,6 @@ export function useAppSettings() { return { appSettings, - setAppSettings, favorites, fetchAppSettings, handleSaveAppSettings, diff --git a/frontend/src/hooks/useConversationMessages.ts b/frontend/src/hooks/useConversationMessages.ts index 598a7db..ebbc617 100644 --- a/frontend/src/hooks/useConversationMessages.ts +++ b/frontend/src/hooks/useConversationMessages.ts @@ -57,13 +57,12 @@ export function getMessageContentKey(msg: Message): string { return `${msg.type}-${msg.conversation_key}-${msg.text}-${ts}`; } -export interface UseConversationMessagesResult { +interface UseConversationMessagesResult { messages: Message[]; messagesLoading: boolean; loadingOlder: boolean; hasOlderMessages: boolean; setMessages: React.Dispatch>; - fetchMessages: (showLoading?: boolean) => Promise; fetchOlderMessages: () => Promise; addMessageIfNew: (msg: Message) => boolean; updateMessageAck: (messageId: number, ackCount: number, paths?: MessagePath[]) => void; @@ -433,7 +432,6 @@ export function useConversationMessages( loadingOlder, hasOlderMessages, setMessages, - fetchMessages, fetchOlderMessages, addMessageIfNew, updateMessageAck, diff --git a/frontend/src/hooks/useRepeaterMode.ts b/frontend/src/hooks/useRepeaterMode.ts index cc40701..0e8c3ca 100644 --- a/frontend/src/hooks/useRepeaterMode.ts +++ b/frontend/src/hooks/useRepeaterMode.ts @@ -12,7 +12,7 @@ import { CONTACT_TYPE_REPEATER } from '../types'; import { useAirtimeTracking } from './useAirtimeTracking'; // Format seconds into human-readable duration (e.g., 1d17h2m, 1h5m, 3m) -export function formatDuration(seconds: number): string { +function formatDuration(seconds: number): string { if (seconds < 60) return `${seconds}s`; const days = Math.floor(seconds / 86400); @@ -32,7 +32,7 @@ export function formatDuration(seconds: number): string { } // Format telemetry response as human-readable text -export function formatTelemetry(telemetry: TelemetryResponse): string { +function formatTelemetry(telemetry: TelemetryResponse): string { const lines = [ `Telemetry`, `Battery Voltage: ${telemetry.battery_volts.toFixed(3)}V`, @@ -57,7 +57,7 @@ export function formatTelemetry(telemetry: TelemetryResponse): string { } // Format neighbors list as human-readable text -export function formatNeighbors(neighbors: NeighborInfo[]): string { +function formatNeighbors(neighbors: NeighborInfo[]): string { if (neighbors.length === 0) { return 'Neighbors\nNo neighbors reported'; } @@ -73,7 +73,7 @@ export function formatNeighbors(neighbors: NeighborInfo[]): string { } // Format ACL list as human-readable text -export function formatAcl(acl: AclEntry[]): string { +function formatAcl(acl: AclEntry[]): string { if (acl.length === 0) { return 'ACL\nNo ACL entries'; } @@ -108,7 +108,7 @@ function createLocalMessage( }; } -export interface UseRepeaterModeResult { +interface UseRepeaterModeResult { repeaterLoggedIn: boolean; activeContactIsRepeater: boolean; handleTelemetryRequest: (password: string) => Promise; diff --git a/frontend/src/hooks/useUnreadCounts.ts b/frontend/src/hooks/useUnreadCounts.ts index b4bb06a..8821bd0 100644 --- a/frontend/src/hooks/useUnreadCounts.ts +++ b/frontend/src/hooks/useUnreadCounts.ts @@ -9,7 +9,7 @@ import { import type { Channel, Contact, Conversation, Message, UnreadCounts } from '../types'; import { takePrefetch } from '../prefetch'; -export interface UseUnreadCountsResult { +interface UseUnreadCountsResult { unreadCounts: Record; /** Tracks which conversations have unread messages that mention the user */ mentions: Record; diff --git a/frontend/src/messageCache.ts b/frontend/src/messageCache.ts index 7433864..6528915 100644 --- a/frontend/src/messageCache.ts +++ b/frontend/src/messageCache.ts @@ -12,7 +12,7 @@ import type { Message, MessagePath } from './types'; export const MAX_CACHED_CONVERSATIONS = 20; export const MAX_MESSAGES_PER_ENTRY = 200; -export interface CacheEntry { +interface CacheEntry { messages: Message[]; seenContent: Set; hasOlderMessages: boolean; @@ -139,8 +139,3 @@ export function remove(id: string): void { export function clear(): void { cache.clear(); } - -/** Get current cache size (for testing). */ -export function size(): number { - return cache.size; -} diff --git a/frontend/src/test/contactAvatar.test.ts b/frontend/src/test/contactAvatar.test.ts index 55384c0..42a70ae 100644 --- a/frontend/src/test/contactAvatar.test.ts +++ b/frontend/src/test/contactAvatar.test.ts @@ -1,74 +1,7 @@ import { describe, it, expect } from 'vitest'; -import { getAvatarText, getAvatarColor, getContactAvatar } from '../utils/contactAvatar'; +import { getContactAvatar } from '../utils/contactAvatar'; import { CONTACT_TYPE_REPEATER } from '../types'; -describe('getAvatarText', () => { - it('returns first emoji when name contains emoji', () => { - expect(getAvatarText('John πŸš€ Doe', 'abc123')).toBe('πŸš€'); - expect(getAvatarText('πŸŽ‰ Party', 'abc123')).toBe('πŸŽ‰'); - expect(getAvatarText('Test πŸ˜€ More 🎯', 'abc123')).toBe('πŸ˜€'); - }); - - it('returns full flag emoji (not just first regional indicator)', () => { - expect(getAvatarText('Jason πŸ‡ΊπŸ‡Έ', 'abc123')).toBe('πŸ‡ΊπŸ‡Έ'); - expect(getAvatarText('πŸ‡¬πŸ‡§ London', 'abc123')).toBe('πŸ‡¬πŸ‡§'); - expect(getAvatarText('Test πŸ‡―πŸ‡΅ Japan', 'abc123')).toBe('πŸ‡―πŸ‡΅'); - }); - - it('returns initials when name has space', () => { - expect(getAvatarText('John Doe', 'abc123')).toBe('JD'); - expect(getAvatarText('Alice Bob Charlie', 'abc123')).toBe('AB'); - expect(getAvatarText('jane smith', 'abc123')).toBe('JS'); - }); - - it('returns single letter when no space', () => { - expect(getAvatarText('John', 'abc123')).toBe('J'); - expect(getAvatarText('alice', 'abc123')).toBe('A'); - }); - - it('falls back to public key when name is null', () => { - expect(getAvatarText(null, 'abc123def456')).toBe('AB'); - }); - - it('falls back to public key when name has no letters', () => { - expect(getAvatarText('123 456', 'xyz789')).toBe('XY'); - expect(getAvatarText('---', 'def456')).toBe('DE'); - }); - - it('handles space but no letter after', () => { - expect(getAvatarText('John ', 'abc123')).toBe('J'); - expect(getAvatarText('A 123', 'abc123')).toBe('A'); - }); - - it('emoji takes priority over initials', () => { - expect(getAvatarText('John 🎯 Doe', 'abc123')).toBe('🎯'); - }); -}); - -describe('getAvatarColor', () => { - it('returns consistent colors for same public key', () => { - const color1 = getAvatarColor('abc123def456'); - const color2 = getAvatarColor('abc123def456'); - expect(color1).toEqual(color2); - }); - - it('returns different colors for different public keys', () => { - const color1 = getAvatarColor('abc123def456'); - const color2 = getAvatarColor('xyz789uvw012'); - expect(color1.background).not.toBe(color2.background); - }); - - it('returns valid HSL background color', () => { - const color = getAvatarColor('test123'); - expect(color.background).toMatch(/^hsl\(\d+, \d+%, \d+%\)$/); - }); - - it('returns white or black text color', () => { - const color = getAvatarColor('test123'); - expect(['#ffffff', '#000000']).toContain(color.text); - }); -}); - describe('getContactAvatar', () => { it('returns complete avatar info', () => { const avatar = getContactAvatar('John Doe', 'abc123def456'); @@ -103,4 +36,41 @@ describe('getContactAvatar', () => { expect(avatar0.text).toBe('J'); expect(avatar1.text).toBe('J'); }); + + it('extracts emoji from name', () => { + const avatar = getContactAvatar('John πŸš€ Doe', 'abc123'); + expect(avatar.text).toBe('πŸš€'); + }); + + it('extracts flag emoji', () => { + const avatar = getContactAvatar('Jason πŸ‡ΊπŸ‡Έ', 'abc123'); + expect(avatar.text).toBe('πŸ‡ΊπŸ‡Έ'); + }); + + it('extracts initials from two-word name', () => { + const avatar = getContactAvatar('Jane Smith', 'abc123'); + expect(avatar.text).toBe('JS'); + }); + + it('extracts single letter from one-word name', () => { + const avatar = getContactAvatar('Alice', 'abc123'); + expect(avatar.text).toBe('A'); + }); + + it('falls back to pubkey prefix for names with no letters', () => { + const avatar = getContactAvatar('123 456', 'xyz789'); + expect(avatar.text).toBe('XY'); + }); + + it('returns consistent colors for same public key', () => { + const avatar1 = getContactAvatar('A', 'abc123def456'); + const avatar2 = getContactAvatar('B', 'abc123def456'); + expect(avatar1.background).toBe(avatar2.background); + }); + + it('returns different colors for different public keys', () => { + const avatar1 = getContactAvatar('A', 'abc123def456'); + const avatar2 = getContactAvatar('A', 'xyz789uvw012'); + expect(avatar1.background).not.toBe(avatar2.background); + }); }); diff --git a/frontend/src/test/messageCache.test.ts b/frontend/src/test/messageCache.test.ts index df8421c..3dc9f65 100644 --- a/frontend/src/test/messageCache.test.ts +++ b/frontend/src/test/messageCache.test.ts @@ -24,7 +24,7 @@ function createMessage(overrides: Partial = {}): Message { }; } -function createEntry(messages: Message[] = [], hasOlderMessages = false): messageCache.CacheEntry { +function createEntry(messages: Message[] = [], hasOlderMessages = false) { const seenContent = new Set(); for (const msg of messages) { seenContent.add(`${msg.type}-${msg.conversation_key}-${msg.text}-${msg.sender_timestamp}`); @@ -97,7 +97,6 @@ describe('messageCache', () => { const result = messageCache.get('conv1'); expect(result!.messages[0].text).toBe('second'); - expect(messageCache.size()).toBe(1); }); }); @@ -231,7 +230,6 @@ describe('messageCache', () => { ); expect(result).toBe(true); - expect(messageCache.size()).toBe(1); const entry = messageCache.get('new_conv'); expect(entry).toBeDefined(); expect(entry!.messages).toHaveLength(1); @@ -319,13 +317,12 @@ describe('messageCache', () => { expect(messageCache.get('conv1')).toBeUndefined(); expect(messageCache.get('conv2')).toBeDefined(); - expect(messageCache.size()).toBe(1); }); it('does nothing for non-existent key', () => { messageCache.set('conv1', createEntry()); messageCache.remove('nonexistent'); - expect(messageCache.size()).toBe(1); + expect(messageCache.get('conv1')).toBeDefined(); }); }); @@ -437,16 +434,4 @@ describe('messageCache', () => { }); }); - describe('clear', () => { - it('removes all entries', () => { - messageCache.set('conv1', createEntry()); - messageCache.set('conv2', createEntry()); - messageCache.set('conv3', createEntry()); - - messageCache.clear(); - - expect(messageCache.size()).toBe(0); - expect(messageCache.get('conv1')).toBeUndefined(); - }); - }); }); diff --git a/frontend/src/test/pathUtils.test.ts b/frontend/src/test/pathUtils.test.ts index 1469c21..07fb834 100644 --- a/frontend/src/test/pathUtils.test.ts +++ b/frontend/src/test/pathUtils.test.ts @@ -3,7 +3,6 @@ import { parsePathHops, findContactsByPrefix, calculateDistance, - sortContactsByDistance, resolvePath, formatDistance, formatHopCounts, @@ -150,50 +149,6 @@ describe('calculateDistance', () => { }); }); -describe('sortContactsByDistance', () => { - const contactClose = createContact({ - public_key: 'AA' + 'A'.repeat(62), - name: 'Close', - lat: 40.7228, - lon: -74.006, - }); - const contactFar = createContact({ - public_key: 'BB' + 'B'.repeat(62), - name: 'Far', - lat: 40.9, - lon: -74.006, - }); - const contactNoLocation = createContact({ - public_key: 'CC' + 'C'.repeat(62), - name: 'NoLoc', - lat: null, - lon: null, - }); - - it('sorts by distance ascending', () => { - const sorted = sortContactsByDistance([contactFar, contactClose], 40.7128, -74.006); - expect(sorted[0].name).toBe('Close'); - expect(sorted[1].name).toBe('Far'); - }); - - it('places contacts without location at end', () => { - const sorted = sortContactsByDistance( - [contactNoLocation, contactClose, contactFar], - 40.7128, - -74.006 - ); - expect(sorted[0].name).toBe('Close'); - expect(sorted[1].name).toBe('Far'); - expect(sorted[2].name).toBe('NoLoc'); - }); - - it('returns unsorted if reference is null', () => { - const contacts = [contactFar, contactClose]; - const sorted = sortContactsByDistance(contacts, null, -74.006); - expect(sorted[0].name).toBe(contacts[0].name); - }); -}); - describe('resolvePath', () => { const repeater1 = createContact({ public_key: '1A' + 'A'.repeat(62), diff --git a/frontend/src/test/settingsModal.test.tsx b/frontend/src/test/settingsModal.test.tsx index 22fd424..2f108bc 100644 --- a/frontend/src/test/settingsModal.test.tsx +++ b/frontend/src/test/settingsModal.test.tsx @@ -10,7 +10,7 @@ import type { RadioConfigUpdate, StatisticsResponse, } from '../types'; -import type { SettingsSection } from '../components/SettingsModal'; +import type { SettingsSection } from '../components/settingsConstants'; import { LAST_VIEWED_CONVERSATION_KEY, REOPEN_LAST_CONVERSATION_KEY, diff --git a/frontend/src/test/urlHash.test.ts b/frontend/src/test/urlHash.test.ts index c29fc1a..4255c0d 100644 --- a/frontend/src/test/urlHash.test.ts +++ b/frontend/src/test/urlHash.test.ts @@ -8,12 +8,11 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { parseHashConversation, - getConversationHash, getMapFocusHash, resolveChannelFromHashToken, resolveContactFromHashToken, } from '../utils/urlHash'; -import type { Channel, Contact, Conversation } from '../types'; +import type { Channel, Contact } from '../types'; describe('parseHashConversation', () => { let originalHash: string; @@ -147,122 +146,6 @@ describe('parseHashConversation', () => { }); }); -describe('getConversationHash', () => { - it('returns empty string for null conversation', () => { - const result = getConversationHash(null); - - expect(result).toBe(''); - }); - - it('returns #raw for raw conversation', () => { - const conv: Conversation = { type: 'raw', id: 'raw', name: 'Raw Packet Feed' }; - - const result = getConversationHash(conv); - - expect(result).toBe('#raw'); - }); - - it('returns #map for map conversation', () => { - const conv: Conversation = { type: 'map', id: 'map', name: 'Node Map' }; - - const result = getConversationHash(conv); - - expect(result).toBe('#map'); - }); - - it('generates channel hash', () => { - const conv: Conversation = { type: 'channel', id: 'key123', name: 'Public' }; - - const result = getConversationHash(conv); - - expect(result).toBe('#channel/key123/Public'); - }); - - it('generates contact hash', () => { - const conv: Conversation = { type: 'contact', id: 'pubkey123', name: 'Alice' }; - - const result = getConversationHash(conv); - - expect(result).toBe('#contact/pubkey123/Alice'); - }); - - it('uses channel id even when name starts with #', () => { - const conv: Conversation = { type: 'channel', id: 'key123', name: '#TestChannel' }; - - const result = getConversationHash(conv); - - expect(result).toBe('#channel/key123/TestChannel'); - }); - - it('encodes special characters in ids', () => { - const conv: Conversation = { type: 'contact', id: 'key with space', name: 'John Doe' }; - - const result = getConversationHash(conv); - - expect(result).toBe('#contact/key%20with%20space/John%20Doe'); - }); - - it('uses id regardless of contact display name', () => { - const conv: Conversation = { type: 'contact', id: 'key', name: '#Hashtag' }; - - const result = getConversationHash(conv); - - expect(result).toBe('#contact/key/%23Hashtag'); - }); -}); - -describe('parseHashConversation and getConversationHash roundtrip', () => { - let originalHash: string; - - beforeEach(() => { - originalHash = window.location.hash; - }); - - afterEach(() => { - window.location.hash = originalHash; - }); - - it('channel roundtrip preserves data', () => { - const conv: Conversation = { type: 'channel', id: 'key123', name: 'Test Channel' }; - - const hash = getConversationHash(conv); - window.location.hash = hash; - const parsed = parseHashConversation(); - - expect(parsed).toEqual({ type: 'channel', name: 'key123', label: 'Test Channel' }); - }); - - it('contact roundtrip preserves data', () => { - const conv: Conversation = { type: 'contact', id: 'pubkey', name: 'Alice Bob' }; - - const hash = getConversationHash(conv); - window.location.hash = hash; - const parsed = parseHashConversation(); - - expect(parsed).toEqual({ type: 'contact', name: 'pubkey', label: 'Alice Bob' }); - }); - - it('raw roundtrip preserves type', () => { - const conv: Conversation = { type: 'raw', id: 'raw', name: 'Raw Packet Feed' }; - - const hash = getConversationHash(conv); - window.location.hash = hash; - const parsed = parseHashConversation(); - - expect(parsed).toEqual({ type: 'raw', name: 'raw' }); - }); - - it('map roundtrip preserves type', () => { - const conv: Conversation = { type: 'map', id: 'map', name: 'Node Map' }; - - const hash = getConversationHash(conv); - window.location.hash = hash; - const parsed = parseHashConversation(); - - expect(parsed).toEqual({ type: 'map', name: 'map' }); - }); -}); - describe('resolveChannelFromHashToken', () => { const channels: Channel[] = [ { diff --git a/frontend/src/test/useRepeaterMode.test.ts b/frontend/src/test/useRepeaterMode.test.ts deleted file mode 100644 index 83765da..0000000 --- a/frontend/src/test/useRepeaterMode.test.ts +++ /dev/null @@ -1,241 +0,0 @@ -/** - * Tests for useRepeaterMode hook utilities. - * - * These tests verify the formatting functions used to display - * telemetry data from repeaters. - */ - -import { describe, it, expect } from 'vitest'; -import { - formatDuration, - formatTelemetry, - formatNeighbors, - formatAcl, -} from '../hooks/useRepeaterMode'; -import type { TelemetryResponse, NeighborInfo, AclEntry } from '../types'; - -describe('formatDuration', () => { - it('formats seconds under a minute', () => { - expect(formatDuration(0)).toBe('0s'); - expect(formatDuration(30)).toBe('30s'); - expect(formatDuration(59)).toBe('59s'); - }); - - it('formats minutes only', () => { - expect(formatDuration(60)).toBe('1m'); - expect(formatDuration(120)).toBe('2m'); - expect(formatDuration(300)).toBe('5m'); - expect(formatDuration(3599)).toBe('59m'); - }); - - it('formats hours and minutes', () => { - expect(formatDuration(3600)).toBe('1h'); - expect(formatDuration(3660)).toBe('1h1m'); - expect(formatDuration(7200)).toBe('2h'); - expect(formatDuration(7380)).toBe('2h3m'); - }); - - it('formats days only', () => { - expect(formatDuration(86400)).toBe('1d'); - expect(formatDuration(172800)).toBe('2d'); - }); - - it('formats days and hours', () => { - expect(formatDuration(90000)).toBe('1d1h'); - expect(formatDuration(97200)).toBe('1d3h'); - }); - - it('formats days and minutes (no hours)', () => { - expect(formatDuration(86700)).toBe('1d5m'); - }); - - it('formats days, hours, and minutes', () => { - expect(formatDuration(90060)).toBe('1d1h1m'); - expect(formatDuration(148920)).toBe('1d17h22m'); - }); -}); - -describe('formatTelemetry', () => { - it('formats telemetry response with all fields', () => { - const telemetry: TelemetryResponse = { - pubkey_prefix: 'abc123', - battery_volts: 4.123, - uptime_seconds: 90060, // 1d1h1m - airtime_seconds: 3600, // 1h - rx_airtime_seconds: 7200, // 2h - noise_floor_dbm: -120, - last_rssi_dbm: -90, - last_snr_db: 8.5, - packets_received: 1000, - packets_sent: 500, - recv_flood: 800, - sent_flood: 400, - recv_direct: 200, - sent_direct: 100, - flood_dups: 50, - direct_dups: 10, - tx_queue_len: 2, - full_events: 0, - neighbors: [], - acl: [], - clock_output: null, - }; - - const result = formatTelemetry(telemetry); - - expect(result).toContain('Telemetry'); - expect(result).toContain('Battery Voltage: 4.123V'); - expect(result).toContain('Uptime: 1d1h1m'); - expect(result).toContain('TX Airtime: 1h'); - expect(result).toContain('RX Airtime: 2h'); - expect(result).toContain('Noise Floor: -120 dBm'); - expect(result).toContain('Last RSSI: -90 dBm'); - expect(result).toContain('Last SNR: 8.5 dB'); - expect(result).toContain('Packets: 1,000 rx / 500 tx'); - expect(result).toContain('Flood: 800 rx / 400 tx'); - expect(result).toContain('Direct: 200 rx / 100 tx'); - expect(result).toContain('Duplicates: 50 flood / 10 direct'); - expect(result).toContain('TX Queue: 2'); - }); - - it('formats battery voltage with 3 decimal places', () => { - const telemetry: TelemetryResponse = { - pubkey_prefix: 'abc123', - battery_volts: 3.7, - uptime_seconds: 0, - airtime_seconds: 0, - rx_airtime_seconds: 0, - noise_floor_dbm: 0, - last_rssi_dbm: 0, - last_snr_db: 0, - packets_received: 0, - packets_sent: 0, - recv_flood: 0, - sent_flood: 0, - recv_direct: 0, - sent_direct: 0, - flood_dups: 0, - direct_dups: 0, - tx_queue_len: 0, - full_events: 0, - neighbors: [], - acl: [], - clock_output: null, - }; - - const result = formatTelemetry(telemetry); - expect(result).toContain('Battery Voltage: 3.700V'); - }); -}); - -describe('formatNeighbors', () => { - it('returns "No neighbors" message for empty list', () => { - const result = formatNeighbors([]); - - expect(result).toBe('Neighbors\nNo neighbors reported'); - }); - - it('formats single neighbor', () => { - const neighbors: NeighborInfo[] = [ - { pubkey_prefix: 'abc123', name: 'Alice', snr: 8.5, last_heard_seconds: 60 }, - ]; - - const result = formatNeighbors(neighbors); - - expect(result).toContain('Neighbors (1)'); - expect(result).toContain('Alice, +8.5 dB [1m ago]'); - }); - - it('sorts neighbors by SNR descending', () => { - const neighbors: NeighborInfo[] = [ - { pubkey_prefix: 'aaa', name: 'Low', snr: -5, last_heard_seconds: 10 }, - { pubkey_prefix: 'bbb', name: 'High', snr: 10, last_heard_seconds: 20 }, - { pubkey_prefix: 'ccc', name: 'Mid', snr: 5, last_heard_seconds: 30 }, - ]; - - const result = formatNeighbors(neighbors); - const lines = result.split('\n'); - - expect(lines[1]).toContain('High'); - expect(lines[2]).toContain('Mid'); - expect(lines[3]).toContain('Low'); - }); - - it('uses pubkey_prefix when name is null', () => { - const neighbors: NeighborInfo[] = [ - { pubkey_prefix: 'abc123def456', name: null, snr: 5, last_heard_seconds: 120 }, - ]; - - const result = formatNeighbors(neighbors); - - expect(result).toContain('abc123def456, +5.0 dB [2m ago]'); - }); - - it('formats negative SNR without plus sign', () => { - const neighbors: NeighborInfo[] = [ - { pubkey_prefix: 'abc', name: 'Test', snr: -3.5, last_heard_seconds: 60 }, - ]; - - const result = formatNeighbors(neighbors); - - expect(result).toContain('Test, -3.5 dB'); - }); - - it('formats last heard in various durations', () => { - const neighbors: NeighborInfo[] = [ - { pubkey_prefix: 'a', name: 'Seconds', snr: 0, last_heard_seconds: 45 }, - { pubkey_prefix: 'b', name: 'Minutes', snr: 0, last_heard_seconds: 300 }, - { pubkey_prefix: 'c', name: 'Hours', snr: 0, last_heard_seconds: 7200 }, - ]; - - const result = formatNeighbors(neighbors); - - expect(result).toContain('Seconds, +0.0 dB [45s ago]'); - expect(result).toContain('Minutes, +0.0 dB [5m ago]'); - expect(result).toContain('Hours, +0.0 dB [2h ago]'); - }); -}); - -describe('formatAcl', () => { - it('returns "No ACL entries" message for empty list', () => { - const result = formatAcl([]); - - expect(result).toBe('ACL\nNo ACL entries'); - }); - - it('formats single ACL entry', () => { - const acl: AclEntry[] = [ - { pubkey_prefix: 'abc123', name: 'Alice', permission: 3, permission_name: 'Admin' }, - ]; - - const result = formatAcl(acl); - - expect(result).toContain('ACL (1)'); - expect(result).toContain('Alice: Admin'); - }); - - it('formats multiple ACL entries', () => { - const acl: AclEntry[] = [ - { pubkey_prefix: 'aaa', name: 'Admin User', permission: 3, permission_name: 'Admin' }, - { pubkey_prefix: 'bbb', name: 'Read Only', permission: 1, permission_name: 'Read-only' }, - { pubkey_prefix: 'ccc', name: null, permission: 0, permission_name: 'Guest' }, - ]; - - const result = formatAcl(acl); - - expect(result).toContain('ACL (3)'); - expect(result).toContain('Admin User: Admin'); - expect(result).toContain('Read Only: Read-only'); - expect(result).toContain('ccc: Guest'); - }); - - it('uses pubkey_prefix when name is null', () => { - const acl: AclEntry[] = [ - { pubkey_prefix: 'xyz789', name: null, permission: 2, permission_name: 'Read-write' }, - ]; - - const result = formatAcl(acl); - - expect(result).toContain('xyz789: Read-write'); - }); -}); diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 898d4e5..f151ffe 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -1,4 +1,4 @@ -export interface RadioSettings { +interface RadioSettings { freq: number; bw: number; sf: number; @@ -99,7 +99,7 @@ export interface Message { acked: number; } -export type ConversationType = 'contact' | 'channel' | 'raw' | 'map' | 'visualizer'; +type ConversationType = 'contact' | 'channel' | 'raw' | 'map' | 'visualizer'; export interface Conversation { type: ConversationType; @@ -228,13 +228,13 @@ export interface UnreadCounts { last_message_times: Record; } -export interface BusyChannel { +interface BusyChannel { channel_key: string; channel_name: string; message_count: number; } -export interface ContactActivityCounts { +interface ContactActivityCounts { last_hour: number; last_24_hours: number; last_week: number; diff --git a/frontend/src/utils/contactAvatar.ts b/frontend/src/utils/contactAvatar.ts index bf1c8be..fa927cf 100644 --- a/frontend/src/utils/contactAvatar.ts +++ b/frontend/src/utils/contactAvatar.ts @@ -38,7 +38,7 @@ const emojiRegex = * 2. First letter + first letter after first space (initials) * 3. First letter only */ -export function getAvatarText(name: string | null, publicKey: string): string { +function getAvatarText(name: string | null, publicKey: string): string { if (!name) { // Use first 2 chars of public key as fallback return publicKey.slice(0, 2).toUpperCase(); @@ -76,7 +76,7 @@ export function getAvatarText(name: string | null, publicKey: string): string { * Generate a consistent HSL color from a public key. * Uses saturation and lightness ranges that work well for backgrounds. */ -export function getAvatarColor(publicKey: string): { +function getAvatarColor(publicKey: string): { background: string; text: string; } { diff --git a/frontend/src/utils/pathUtils.ts b/frontend/src/utils/pathUtils.ts index 76bcb58..712a2b5 100644 --- a/frontend/src/utils/pathUtils.ts +++ b/frontend/src/utils/pathUtils.ts @@ -123,7 +123,7 @@ export function formatDistance(km: number): string { * Sort contacts by distance from a reference point * Contacts without location are placed at the end */ -export function sortContactsByDistance( +function sortContactsByDistance( contacts: Contact[], fromLat: number | null, fromLon: number | null diff --git a/frontend/src/utils/radioPresets.ts b/frontend/src/utils/radioPresets.ts index 7d70f12..fcdb597 100644 --- a/frontend/src/utils/radioPresets.ts +++ b/frontend/src/utils/radioPresets.ts @@ -1,5 +1,5 @@ // Radio presets for common LoRa configurations -export interface RadioPreset { +interface RadioPreset { name: string; freq: number; bw: number; diff --git a/frontend/src/utils/urlHash.ts b/frontend/src/utils/urlHash.ts index dede798..667cd45 100644 --- a/frontend/src/utils/urlHash.ts +++ b/frontend/src/utils/urlHash.ts @@ -102,7 +102,7 @@ export function getMapFocusHash(publicKeyPrefix: string): string { } // Generate URL hash from conversation -export function getConversationHash(conv: Conversation | null): string { +function getConversationHash(conv: Conversation | null): string { if (!conv) return ''; if (conv.type === 'raw') return '#raw'; if (conv.type === 'map') return '#map'; diff --git a/frontend/src/utils/visualizerUtils.ts b/frontend/src/utils/visualizerUtils.ts index e9659f5..a4434d3 100644 --- a/frontend/src/utils/visualizerUtils.ts +++ b/frontend/src/utils/visualizerUtils.ts @@ -6,7 +6,7 @@ import { CONTACT_TYPE_REPEATER, type Contact, type RawPacket } from '../types'; // ============================================================================= export type NodeType = 'self' | 'repeater' | 'client'; -export type PacketLabel = 'AD' | 'GT' | 'DM' | 'ACK' | 'TR' | 'RQ' | 'RS' | '?'; +type PacketLabel = 'AD' | 'GT' | 'DM' | 'ACK' | 'TR' | 'RQ' | 'RS' | '?'; export interface Particle { linkKey: string; @@ -18,7 +18,7 @@ export interface Particle { toNodeId: string; } -export interface ObservedPath { +interface ObservedPath { nodes: string[]; snr: number | null; timestamp: number; @@ -32,7 +32,7 @@ export interface PendingPacket { expiresAt: number; } -export interface ParsedPacket { +interface ParsedPacket { payloadType: number; messageHash: string | null; pathBytes: string[]; @@ -44,7 +44,7 @@ export interface ParsedPacket { } // Traffic pattern tracking for smarter repeater disambiguation -export interface TrafficObservation { +interface TrafficObservation { source: string; // Node that originated traffic (could be resolved node ID or ambiguous) nextHop: string | null; // Next hop after this repeater (null if final hop before self) timestamp: number; @@ -56,7 +56,7 @@ export interface RepeaterTrafficData { } // Analysis result for whether to split an ambiguous repeater -export interface RepeaterSplitAnalysis { +interface RepeaterSplitAnalysis { shouldSplit: boolean; // If shouldSplit, maps nextHop -> the sources that exclusively route through it disjointGroups: Map> | null; @@ -95,9 +95,9 @@ 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 -export const MIN_OBSERVATIONS_TO_SPLIT = 20; // Need at least this many unique sources per next-hop group -export const MAX_TRAFFIC_OBSERVATIONS = 200; // Per ambiguous prefix, to limit memory -export const TRAFFIC_OBSERVATION_MAX_AGE_MS = 30 * 60 * 1000; // 30 minutes - old observations are pruned +const MIN_OBSERVATIONS_TO_SPLIT = 20; // Need at least this many unique sources per next-hop group +const MAX_TRAFFIC_OBSERVATIONS = 200; // Per ambiguous prefix, to limit memory +const TRAFFIC_OBSERVATION_MAX_AGE_MS = 30 * 60 * 1000; // 30 minutes - old observations are pruned export const PACKET_LEGEND_ITEMS = [ { label: 'AD', color: COLORS.particleAD, description: 'Advertisement' }, @@ -114,7 +114,7 @@ export const PACKET_LEGEND_ITEMS = [ // UTILITY FUNCTIONS (Data Layer) // ============================================================================= -export function simpleHash(str: string): string { +function simpleHash(str: string): string { let hash = 0; for (let i = 0; i < str.length; i++) { hash = (hash << 5) - hash + str.charCodeAt(i);