Fix non-cache refresh on unfocused active threads; doc and test improvements

This commit is contained in:
Jack Kingsman
2026-03-04 10:16:17 -08:00
parent 1f37da8d2d
commit e0fb093612
8 changed files with 236 additions and 8 deletions
+12 -4
View File
@@ -27,6 +27,7 @@ frontend/src/
├── prefetch.ts # Consumes prefetched API promises started in index.html
├── index.css # Global styles/utilities
├── styles.css # Additional global app styles
├── themes.css # Color theme definitions
├── lib/
│ └── utils.ts # cn() — clsx + tailwind-merge helper
├── hooks/
@@ -53,7 +54,8 @@ frontend/src/
│ ├── lastViewedConversation.ts # localStorage for last-viewed conversation
│ ├── contactMerge.ts # Merge WS contact updates into list
│ ├── localLabel.ts # Local label (text + color) in localStorage
── radioPresets.ts # LoRa radio preset configurations
── radioPresets.ts # LoRa radio preset configurations
│ └── theme.ts # Theme switching helpers
├── components/
│ ├── StatusBar.tsx
│ ├── Sidebar.tsx
@@ -75,6 +77,7 @@ frontend/src/
│ ├── ContactStatusInfo.tsx # Contact status info component
│ ├── RepeaterDashboard.tsx # Layout shell — delegates to repeater/ panes
│ ├── RepeaterLogin.tsx # Repeater login form (password + guest)
│ ├── ChannelInfoPane.tsx # Channel detail sheet (stats, top senders)
│ ├── NeighborsMiniMap.tsx # Leaflet mini-map for repeater neighbor locations
│ ├── settings/
│ │ ├── settingsConstants.ts # Settings section type, ordering, labels
@@ -85,7 +88,8 @@ frontend/src/
│ │ ├── SettingsDatabaseSection.tsx # DB size, cleanup, auto-decrypt, local label
│ │ ├── SettingsBotSection.tsx # Bot list, code editor, add/delete/reset
│ │ ├── SettingsStatisticsSection.tsx # Read-only mesh network stats
│ │ ── SettingsAboutSection.tsx # Version, author, license, links
│ │ ── SettingsAboutSection.tsx # Version, author, license, links
│ │ └── ThemeSelector.tsx # Color theme picker
│ ├── repeater/
│ │ ├── repeaterPaneShared.tsx # Shared: RepeaterPane, KvRow, format helpers
│ │ ├── RepeaterTelemetryPane.tsx # Battery, airtime, packet counts
@@ -125,6 +129,10 @@ frontend/src/
├── sidebar.test.tsx
├── unreadCounts.test.ts
├── urlHash.test.ts
├── appSearchJump.test.tsx
├── channelInfoKeyVisibility.test.tsx
├── chatHeaderKeyVisibility.test.tsx
├── searchView.test.tsx
├── useConversationMessages.test.ts
├── useConversationMessages.race.test.ts
├── useRepeaterDashboard.test.ts
@@ -177,7 +185,7 @@ frontend/src/
- Auto reconnect (3s) with cleanup guard on unmount.
- Heartbeat ping every 30s.
- Event handlers: `health`, `message`, `contact`, `raw_packet`, `message_acked`, `error`, `success`, `pong` (ignored).
- Event handlers: `health`, `message`, `contact`, `raw_packet`, `message_acked`, `contact_deleted`, `channel_deleted`, `error`, `success`, `pong` (ignored).
- For `raw_packet` events, use `observation_id` as event identity; `id` is a storage reference and may repeat.
## URL Hash Navigation (`utils/urlHash.ts`)
@@ -310,7 +318,7 @@ Do not rely on old class-only layout assumptions.
## Testing
Run all quality checks (backend + frontend, parallelized) from the repo root:
Run all quality checks (backend + frontend) from the repo root:
```bash
./scripts/all_quality.sh
+11
View File
@@ -307,10 +307,20 @@ export function App() {
onContactDeleted: (publicKey: string) => {
setContacts((prev) => prev.filter((c) => c.public_key !== publicKey));
messageCache.remove(publicKey);
const active = activeConversationRef.current;
if (active?.type === 'contact' && active.id === publicKey) {
pendingDeleteFallbackRef.current = true;
setActiveConversation(null);
}
},
onChannelDeleted: (key: string) => {
setChannels((prev) => prev.filter((c) => c.key !== key));
messageCache.remove(key);
const active = activeConversationRef.current;
if (active?.type === 'channel' && active.id === key) {
pendingDeleteFallbackRef.current = true;
setActiveConversation(null);
}
},
onRawPacket: (packet: RawPacket) => {
setRawPackets((prev) => appendRawPacketUnique(prev, packet, MAX_RAW_PACKETS));
@@ -331,6 +341,7 @@ export function App() {
setConfig,
activeConversationRef,
hasNewerMessagesRef,
setActiveConversation,
setContacts,
setChannels,
triggerReconcile,
+3
View File
@@ -76,6 +76,9 @@ export function addMessage(id: string, msg: Message, contentKey: string): boolea
.sort((a, b) => b.received_at - a.received_at)
.slice(0, MAX_MESSAGES_PER_ENTRY);
}
// Promote to MRU so actively-messaged conversations aren't evicted
cache.delete(id);
cache.set(id, entry);
return true;
}
+17
View File
@@ -239,6 +239,23 @@ describe('messageCache', () => {
expect(entry!.seenContent.has('CHAN-channel123-First contact-1700000000')).toBe(true);
});
it('promotes entry to MRU on addMessage', () => {
// Fill cache to capacity
for (let i = 0; i < MAX_CACHED_CONVERSATIONS; i++) {
messageCache.set(`conv${i}`, createEntry([createMessage({ id: i })]));
}
// addMessage to conv0 (currently LRU) should promote it
const msg = createMessage({ id: 999, text: 'Incoming WS message' });
messageCache.addMessage('conv0', msg, 'CHAN-channel123-Incoming WS message-1700000000');
// Add one more — conv1 should now be LRU and get evicted, not conv0
messageCache.set('conv_new', createEntry());
expect(messageCache.get('conv0')).toBeDefined(); // Was promoted by addMessage
expect(messageCache.get('conv1')).toBeUndefined(); // Was LRU, evicted
});
it('returns false for duplicate delivery to auto-created entry', () => {
const msg = createMessage({ id: 10, text: 'Echo' });
const contentKey = 'CHAN-channel123-Echo-1700000000';