From 93d31adecd03220d6c422edecefd0cd01473aa49 Mon Sep 17 00:00:00 2001 From: Jack Kingsman Date: Thu, 2 Apr 2026 13:21:21 -0700 Subject: [PATCH] Don't change historical migrations (cruft from rebasing) and don't overwrite data --- app/models.py | 2 +- frontend/src/components/RepeaterDashboard.tsx | 24 ++++++-- frontend/src/test/repeaterDashboard.test.tsx | 55 +++++++++++++++++++ 3 files changed, 75 insertions(+), 6 deletions(-) diff --git a/app/models.py b/app/models.py index c55411d..6251bc1 100644 --- a/app/models.py +++ b/app/models.py @@ -808,7 +808,7 @@ class AppSettings(BaseModel): default_factory=list, description="List of favorited conversations" ) auto_decrypt_dm_on_advert: bool = Field( - default=False, + default=True, description="Whether to attempt historical DM decryption on new contact advertisement", ) sidebar_sort_order: Literal["recent", "alpha"] = Field( diff --git a/frontend/src/components/RepeaterDashboard.tsx b/frontend/src/components/RepeaterDashboard.tsx index bf8100e..96feadf 100644 --- a/frontend/src/components/RepeaterDashboard.tsx +++ b/frontend/src/components/RepeaterDashboard.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { api } from '../api'; import { toast } from './ui/sonner'; @@ -100,20 +100,34 @@ export function RepeaterDashboard({ // Telemetry history: preload from stored data, refresh from live status const [telemetryHistory, setTelemetryHistory] = useState([]); + const telemetryHistorySourceRef = useRef<'none' | 'preload' | 'live'>('none'); + const telemetryHistoryRequestRef = useRef(0); + useEffect(() => { + telemetryHistoryRequestRef.current += 1; + telemetryHistorySourceRef.current = 'none'; + setTelemetryHistory([]); + if (!loggedIn) return; + + const requestId = telemetryHistoryRequestRef.current; api .repeaterTelemetryHistory(conversation.id) - .then(setTelemetryHistory) + .then((history) => { + if (telemetryHistoryRequestRef.current !== requestId) return; + if (telemetryHistorySourceRef.current === 'live') return; + telemetryHistorySourceRef.current = 'preload'; + setTelemetryHistory(history); + }) .catch(() => {}); }, [loggedIn, conversation.id]); // When a live status fetch returns embedded telemetry_history, replace local state useEffect(() => { const liveHistory = paneData.status?.telemetry_history; - if (liveHistory && liveHistory.length > 0) { - setTelemetryHistory(liveHistory); - } + if (!liveHistory) return; + telemetryHistorySourceRef.current = 'live'; + setTelemetryHistory(liveHistory); }, [paneData.status?.telemetry_history]); const isFav = isFavorite(favorites, 'contact', conversation.id); diff --git a/frontend/src/test/repeaterDashboard.test.tsx b/frontend/src/test/repeaterDashboard.test.tsx index e93062d..d51d785 100644 --- a/frontend/src/test/repeaterDashboard.test.tsx +++ b/frontend/src/test/repeaterDashboard.test.tsx @@ -126,6 +126,16 @@ const defaultProps = { onDeleteContact: vi.fn(), }; +function createDeferred() { + let resolve!: (value: T) => void; + let reject!: (reason?: unknown) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; +} + describe('RepeaterDashboard', () => { beforeEach(() => { vi.clearAllMocks(); @@ -645,6 +655,11 @@ describe('RepeaterDashboard', () => { }); describe('telemetry history', () => { + beforeEach(async () => { + const { api } = await import('../api'); + vi.mocked(api.repeaterTelemetryHistory).mockResolvedValue([]); + }); + it('loads telemetry history on mount when logged in', async () => { const { api } = await import('../api'); mockHook.loggedIn = true; @@ -699,5 +714,45 @@ describe('RepeaterDashboard', () => { expect(screen.getByText('1 samples')).toBeInTheDocument(); }); }); + + it('does not let an older preload overwrite newer live status history', async () => { + const { api } = await import('../api'); + const historySpy = vi.mocked(api.repeaterTelemetryHistory); + const deferred = createDeferred<{ timestamp: number; data: { battery_volts: number } }[]>(); + historySpy.mockReturnValue(deferred.promise); + + mockHook.loggedIn = true; + mockHook.paneData.status = { + battery_volts: 4.2, + tx_queue_len: 0, + noise_floor_dbm: -120, + last_rssi_dbm: -85, + last_snr_db: 7.5, + packets_received: 100, + packets_sent: 50, + airtime_seconds: 600, + rx_airtime_seconds: 1200, + uptime_seconds: 86400, + sent_flood: 10, + sent_direct: 40, + recv_flood: 30, + recv_direct: 70, + flood_dups: 1, + direct_dups: 0, + full_events: 0, + telemetry_history: [{ timestamp: 1700000000, data: { battery_volts: 4.2 } }], + }; + + render(); + + await waitFor(() => { + expect(screen.getByText('1 samples')).toBeInTheDocument(); + }); + + deferred.resolve([{ timestamp: 1690000000, data: { battery_volts: 3.9 } }]); + await deferred.promise; + + expect(screen.getByText('1 samples')).toBeInTheDocument(); + }); }); });