diff --git a/src/main/ipc/handlers/tuningHandlers.ts b/src/main/ipc/handlers/tuningHandlers.ts index 64bd0561..f751e341 100644 --- a/src/main/ipc/handlers/tuningHandlers.ts +++ b/src/main/ipc/handlers/tuningHandlers.ts @@ -1067,7 +1067,8 @@ export function registerTuningHandlers(deps: HandlerDependencies): void { _event, recordId: string, verificationMetrics?: FilterMetricsSummary, - verificationPidMetrics?: PIDMetricsSummary + verificationPidMetrics?: PIDMetricsSummary, + verificationTransferFunctionMetrics?: TransferFunctionMetricsSummary ): Promise> => { try { if (!tuningHistoryManager || !profileManager) { @@ -1082,7 +1083,8 @@ export function registerTuningHandlers(deps: HandlerDependencies): void { profileId, recordId, verificationMetrics, - verificationPidMetrics + verificationPidMetrics, + verificationTransferFunctionMetrics ); if (!updated) { return createResponse(undefined, `History record not found: ${recordId}`); diff --git a/src/main/storage/TuningHistoryManager.test.ts b/src/main/storage/TuningHistoryManager.test.ts index aa057eff..9ead461a 100644 --- a/src/main/storage/TuningHistoryManager.test.ts +++ b/src/main/storage/TuningHistoryManager.test.ts @@ -119,6 +119,30 @@ describe('TuningHistoryManager', () => { expect(record.verificationMetrics).toBeNull(); }); + it('archives verificationTransferFunctionMetrics and recommendationTraces', async () => { + const session: TuningSession = { + profileId: 'profile-1', + phase: TUNING_PHASE.COMPLETED, + tuningType: TUNING_TYPE.FLASH, + startedAt: '2026-01-15T10:00:00.000Z', + updatedAt: '2026-01-15T11:00:00.000Z', + verificationTransferFunctionMetrics: { + roll: { bandwidthHz: 45, phaseMarginDeg: 55, dcGainDb: -0.5 }, + pitch: { bandwidthHz: 42, phaseMarginDeg: 50, dcGainDb: -0.8 }, + yaw: { bandwidthHz: 30, phaseMarginDeg: 60, dcGainDb: -1.2 }, + } as any, + recommendationTraces: [ + { ruleId: 'P-OS-P-roll', setting: 'pid_roll_p', from: 45, to: 40, confidence: 'high' }, + ] as any, + }; + + const record = await manager.archiveSession(session); + expect(record.verificationTransferFunctionMetrics).toEqual( + session.verificationTransferFunctionMetrics + ); + expect(record.recommendationTraces).toEqual(session.recommendationTraces); + }); + it('rejects non-completed sessions', async () => { const session: TuningSession = { profileId: 'profile-1', diff --git a/src/main/storage/TuningHistoryManager.ts b/src/main/storage/TuningHistoryManager.ts index 225f4b3d..e5a904a1 100644 --- a/src/main/storage/TuningHistoryManager.ts +++ b/src/main/storage/TuningHistoryManager.ts @@ -64,6 +64,7 @@ export class TuningHistoryManager { verificationMetrics: session.verificationMetrics ?? null, verificationPidMetrics: session.verificationPidMetrics ?? null, transferFunctionMetrics: session.transferFunctionMetrics ?? null, + verificationTransferFunctionMetrics: session.verificationTransferFunctionMetrics ?? null, bfPidProfileIndex: session.bfPidProfileIndex, appVersion: APP_VERSION, ratesConfig: session.ratesConfig, @@ -71,6 +72,7 @@ export class TuningHistoryManager { convergence: session.convergence, verificationSimilarity: session.verificationSimilarity, iterationCount: session.iterationCount, + recommendationTraces: session.recommendationTraces, }; const existing = await this.loadRecords(session.profileId); @@ -123,7 +125,8 @@ export class TuningHistoryManager { profileId: string, recordId: string, verificationMetrics?: FilterMetricsSummary, - verificationPidMetrics?: PIDMetricsSummary + verificationPidMetrics?: PIDMetricsSummary, + verificationTransferFunctionMetrics?: TransferFunctionMetricsSummary ): Promise { const records = await this.loadRecords(profileId); const record = records.find((r) => r.id === recordId); @@ -131,6 +134,9 @@ export class TuningHistoryManager { if (verificationMetrics) record.verificationMetrics = verificationMetrics; if (verificationPidMetrics) record.verificationPidMetrics = verificationPidMetrics; + if (verificationTransferFunctionMetrics) { + record.verificationTransferFunctionMetrics = verificationTransferFunctionMetrics; + } await this.saveRecords(profileId, records); logger.info(`Updated verification metrics on history record ${recordId}`); return true; diff --git a/src/preload/index.ts b/src/preload/index.ts index 3facfb9c..1c10d47f 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -823,13 +823,15 @@ const betaflightAPI: BetaflightAPI = { async updateHistoryVerification( recordId: string, verificationMetrics?: FilterMetricsSummary, - verificationPidMetrics?: PIDMetricsSummary + verificationPidMetrics?: PIDMetricsSummary, + verificationTransferFunctionMetrics?: TransferFunctionMetricsSummary ): Promise { const response = await ipcRenderer.invoke( IPCChannel.TUNING_UPDATE_HISTORY_VERIFICATION, recordId, verificationMetrics, - verificationPidMetrics + verificationPidMetrics, + verificationTransferFunctionMetrics ); if (!response.success) { throw new Error(response.error || 'Failed to update history verification'); diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index b1f7fd39..aa5fc5f0 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -123,6 +123,15 @@ function AppContent() { .catch(() => setAvailableLogIds(new Set())); }; + // Clear stale verification state when session changes (disconnect, reset, new session) + const sessionPhase = tuning.session?.phase; + useEffect(() => { + setPendingVerification(null); + setVerificationPickerLogId(null); + setReanalyzeHistoryRecordId(null); + setIsReanalyze(false); + }, [sessionPhase]); + // Clear erased state when flash has new data (post-flight reconnect via cache push) const flashUsedSize = fcState.blackboxInfo?.usedSize ?? null; const storageType = fcState.blackboxInfo?.storageType ?? 'flash'; @@ -417,6 +426,10 @@ function AppContent() { case 'dismiss': try { setErasedForPhase(null); + setPendingVerification(null); + setVerificationPickerLogId(null); + setReanalyzeHistoryRecordId(null); + setIsReanalyze(false); await tuning.resetSession(); } catch (err) { toast.error(err instanceof Error ? err.message : 'Failed to reset session'); @@ -500,7 +513,8 @@ function AppContent() { await window.betaflight.updateHistoryVerification( historyRecordId, verificationMetrics, - verificationPidMetrics + verificationPidMetrics, + verificationTFMetrics ); await tuningHistory.reload(); } else if (isReanalyzeFlow) { @@ -547,8 +561,10 @@ function AppContent() { const handleVerificationAnalyze = async (sessionIndex: number) => { const verLogId = verificationPickerLogId; const historyRecordId = reanalyzeHistoryRecordId; + const isReanalyzeFlow = isReanalyze; setVerificationPickerLogId(null); setReanalyzeHistoryRecordId(null); + setIsReanalyze(false); if (!verLogId) return; try { @@ -599,7 +615,7 @@ function AppContent() { dataQuality && (dataQuality.tier === 'poor' || dataQuality.tier === 'fair') && !historyRecordId && - !isReanalyze + !isReanalyzeFlow ) { setPendingVerification({ verificationMetrics, @@ -607,7 +623,7 @@ function AppContent() { verificationTFMetrics, dataQuality, historyRecordId, - isReanalyze, + isReanalyze: isReanalyzeFlow, }); return; // Wait for user decision in VerificationQualityWarning modal } @@ -617,7 +633,7 @@ function AppContent() { verificationPidMetrics, verificationTFMetrics, historyRecordId, - isReanalyze + isReanalyzeFlow ); } catch (err) { toast.error(err instanceof Error ? err.message : 'Failed to analyze verification'); @@ -781,6 +797,7 @@ function AppContent() { mode={wizardMode} onExit={() => { setActiveLogId(null); + setPendingVerification(null); document.querySelector('.app-main')?.scrollTo({ top: 0 }); }} onApplyComplete={handleApplyComplete} diff --git a/src/shared/types/ipc.types.ts b/src/shared/types/ipc.types.ts index 05c217ac..b4386143 100644 --- a/src/shared/types/ipc.types.ts +++ b/src/shared/types/ipc.types.ts @@ -333,7 +333,8 @@ export interface BetaflightAPI { updateHistoryVerification( recordId: string, verificationMetrics?: FilterMetricsSummary, - verificationPidMetrics?: PIDMetricsSummary + verificationPidMetrics?: PIDMetricsSummary, + verificationTransferFunctionMetrics?: TransferFunctionMetricsSummary ): Promise; // Telemetry