@@ -238,6 +238,7 @@ function saveStateSnapshot(force = false) {
238238 baselineHistory : state . baselineHistory ,
239239 nodeSpeedHistory : state . nodeSpeedHistory ,
240240 spentUdvpn : state . spentUdvpn ,
241+ runSpentUdvpn : state . runSpentUdvpn ?? 0 ,
241242 balanceUdvpn : state . balanceUdvpn ,
242243 balance : state . balance ,
243244 estimatedTotalCost : state . estimatedTotalCost ,
@@ -603,20 +604,22 @@ function saveCurrentRun(label) {
603604 // C-1(b) guard: if the currently-active run is ALREADY a saved run (its index
604605 // entry exists AND aliases the same SQLite dbRunId we'd write to), do NOT
605606 // allocate a new run number / create a duplicate index entry / overwrite the
606- // SQLite spend downward. This happens after load+retest (or retest-then-Save):
607- // state.activeDbRunId still points at the saved run while state.spentUdvpn may
608- // hold only a pass-only figure. Re-persist into the SAME run with
609- // accumulateSpend=false so we never reduce stored spend, then return the
610- // existing number. persistActiveRun keeps index + SQLite in lockstep.
607+ // SQLite spend downward. This happens after load+retest (or resume-then-Save):
608+ // state.activeDbRunId still points at the saved run. Re-persist into the SAME
609+ // run with accumulateSpend=false so we never reduce stored spend, then return
610+ // the existing number. persistActiveRun keeps index + SQLite in lockstep.
611611 if ( state . activeRunNumber != null && state . activeDbRunId != null ) {
612612 const idxGuard = loadRunsIndex ( ) ;
613613 const existing = idxGuard . runs . find ( r => r . number === state . activeRunNumber ) ;
614614 if ( existing && existing . dbRunId != null && Number ( existing . dbRunId ) === Number ( state . activeDbRunId ) ) {
615- // Never reduce stored spend: only re-persist (write-through) when the live
616- // figure is at least the stored cumulative. accumulateSpend=false treats
617- // state.spentUdvpn as the full cumulative (not additive on top of stored).
615+ // Compare against the CUMULATIVE run spend (runSpentUdvpn), NOT the
616+ // balance-delta spentUdvpn: when resuming an already-saved run, the index
617+ // entry still holds the pre-resume total while the post-resume spend lives
618+ // only in runSpentUdvpn — reading spentUdvpn here would record the stale
619+ // (smaller) figure and silently drop the resume-pass spend from the
620+ // on-chain oracle. accumulateSpend=false treats this as the full cumulative.
618621 const storedSpent = Number ( existing . spentUdvpn ) || 0 ;
619- const liveSpent = Number ( state . spentUdvpn ) || 0 ;
622+ const liveSpent = Number ( state . runSpentUdvpn ) || 0 ;
620623 const passSpent = Math . max ( storedSpent , liveSpent ) ;
621624 state . activeRunSaved = true ;
622625 try {
@@ -689,7 +692,7 @@ function saveCurrentRun(label) {
689692 failed : failed . length ,
690693 pass10 : pass10 . length ,
691694 sdk : state . activeSDK ,
692- spentUdvpn : Number ( state . spentUdvpn ) || 0 ,
695+ spentUdvpn : Number ( state . runSpentUdvpn ) || 0 ,
693696 dbRunId : state . activeDbRunId || null ,
694697 auditLog : state . auditLogPath ? path . basename ( state . auditLogPath ) : null ,
695698 } ;
@@ -706,7 +709,7 @@ function saveCurrentRun(label) {
706709 finished_at : Date . now ( ) ,
707710 node_count : results . length ,
708711 pass_count : passed . length ,
709- spent_udvpn : Number ( state . spentUdvpn ) || 0 ,
712+ spent_udvpn : Number ( state . runSpentUdvpn ) || 0 ,
710713 } ) ;
711714 } catch ( dbErr ) {
712715 console . error ( `[db] updateRunOnFinish failed: ${ dbErr . message } ` ) ;
@@ -770,7 +773,7 @@ function persistActiveRun(label, { accumulateSpend = true, passSpent = null } =
770773 const priorSpent = accumulateSpend ? ( Number ( entry ?. spentUdvpn ) || 0 ) : 0 ;
771774 // Snapshot the pass spend the caller captured (defends against the idle
772775 // balance refresher zeroing state.spentUdvpn mid-gap); fall back to live state.
773- const thisPassSpent = passSpent != null ? ( Number ( passSpent ) || 0 ) : ( Number ( state . spentUdvpn ) || 0 ) ;
776+ const thisPassSpent = passSpent != null ? ( Number ( passSpent ) || 0 ) : ( Number ( state . runSpentUdvpn ) || 0 ) ;
774777 const cumulativeSpent = priorSpent + thisPassSpent ;
775778 if ( ! entry ) {
776779 // M-2: no index entry for the active run → create one so index + SQLite
@@ -864,6 +867,7 @@ function loadRunIntoState(num) {
864867 }
865868 }
866869 state . spentUdvpn = _spent ;
870+ state . runSpentUdvpn = _spent ; // loaded run's stored cumulative spend
867871 state . estimatedTotalCost = _spent > 0 ? `${ ( _spent / 1_000_000 ) . toFixed ( 4 ) } P2P` : '0 P2P' ;
868872 // Live Log follows the loaded run: replace the rolling buffer with this run's
869873 // saved execution log (empty if the run predates per-run log capture).
@@ -990,6 +994,7 @@ function clearActiveRunView() {
990994 state . activeRunSaved = false ;
991995 state . status = 'idle' ;
992996 state . spentUdvpn = 0 ;
997+ state . runSpentUdvpn = 0 ;
993998 state . estimatedTotalCost = '0 P2P' ;
994999 state . auditLogPath = null ;
9951000 // Charts + transient run pointers also belong to the now-deleted run: without
@@ -1062,6 +1067,9 @@ function rehydrateState(results) {
10621067 // Only restore for display purposes, capped to prevent negative.
10631068 if ( snap . balanceUdvpn ) state . balanceUdvpn = snap . balanceUdvpn ;
10641069 if ( snap . spentUdvpn ) state . spentUdvpn = Math . min ( snap . spentUdvpn , state . balanceUdvpn ) ;
1070+ // runSpentUdvpn is a cumulative payment total (not a balance delta) — restore
1071+ // uncapped so a stop→bounce→resume preserves the true recorded run spend.
1072+ if ( snap . runSpentUdvpn != null ) state . runSpentUdvpn = snap . runSpentUdvpn ;
10651073 const remaining = Math . max ( 0 , state . balanceUdvpn - state . spentUdvpn ) ;
10661074 state . balance = `${ ( remaining / 1_000_000 ) . toFixed ( 4 ) } P2P` ;
10671075 if ( snap . estimatedTotalCost ) state . estimatedTotalCost = snap . estimatedTotalCost ;
@@ -2251,6 +2259,7 @@ function startFreshRun(label, { mode = 'p2p', plan_id = null } = {}) {
22512259 state . retryCount = 0 ;
22522260 state . estimatedTotalCost = '0 P2P' ;
22532261 state . spentUdvpn = 0 ;
2262+ state . runSpentUdvpn = 0 ;
22542263 // Surface the active mode so the UI can render a clear "what's running" badge:
22552264 // 'subscription' (sub-plan), 'p2p', 'test'. Plan id is null unless mode === 'subscription'.
22562265 state . runMode = mode ;
@@ -2557,12 +2566,15 @@ app.post('/api/retest-skips', adminOnly, async (req, res) => {
25572566 const tracked = withBatchTracking ( broadcast , state . runMode || 'p2p' ) ;
25582567 runRetestSkips ( skipAddrs , state , tracked ) . then ( ( ) => {
25592568 // Snapshot this-pass spend NOW, before the idle balance refresher can zero
2560- // state.spentUdvpn in the gap before persist. Then reset state.spentUdvpn to
2561- // the true cumulative so the live header + any later Save reflect the real total.
2562- const passSpent = Number ( state . spentUdvpn ) || 0 ;
2569+ // state.spentUdvpn in the gap before persist. runRetestSkips resets the
2570+ // cumulative accumulator (runSpentUdvpn) to 0 at its top, so this-pass spend
2571+ // lives in runSpentUdvpn. Then set BOTH state.spentUdvpn (balance-delta) and
2572+ // state.runSpentUdvpn (cumulative) to the true running total so the live
2573+ // header + any later Save reflect the real total.
2574+ const passSpent = Number ( state . runSpentUdvpn ) || 0 ;
25632575 try {
25642576 const r = persistActiveRun ( undefined , { passSpent } ) ;
2565- if ( r ) state . spentUdvpn = r . cumulativeSpent ;
2577+ if ( r ) { state . spentUdvpn = r . cumulativeSpent ; state . runSpentUdvpn = r . cumulativeSpent ; }
25662578 } catch ( e ) { console . error ( '[retest-skips] persistActiveRun failed:' , e . message ) ; }
25672579 } ) . catch ( err => {
25682580 state . status = 'error' ;
@@ -2593,12 +2605,14 @@ app.post('/api/retest-fails', adminOnly, async (req, res) => {
25932605 }
25942606 const tracked = withBatchTracking ( broadcast , state . runMode || 'p2p' ) ;
25952607 runRetestSkips ( failAddrs , state , tracked ) . then ( ( ) => {
2596- // Snapshot this-pass spend before the idle refresher can zero it; reset
2597- // state.spentUdvpn to the true cumulative for the live header + later Save.
2598- const passSpent = Number ( state . spentUdvpn ) || 0 ;
2608+ // Snapshot this-pass spend (from runSpentUdvpn, the cumulative accumulator
2609+ // which runRetestSkips reset to 0 at its top) before the idle refresher can
2610+ // zero it; set BOTH state.spentUdvpn (balance-delta) and state.runSpentUdvpn
2611+ // (cumulative) to the true cumulative for the live header + later Save.
2612+ const passSpent = Number ( state . runSpentUdvpn ) || 0 ;
25992613 try {
26002614 const r = persistActiveRun ( undefined , { passSpent } ) ;
2601- if ( r ) state . spentUdvpn = r . cumulativeSpent ;
2615+ if ( r ) { state . spentUdvpn = r . cumulativeSpent ; state . runSpentUdvpn = r . cumulativeSpent ; }
26022616 } catch ( e ) { console . error ( '[retest-fails] persistActiveRun failed:' , e . message ) ; }
26032617 } ) . catch ( err => {
26042618 state . status = 'error' ;
@@ -2727,6 +2741,7 @@ app.post('/api/clear', adminOnly, (req, res) => {
27272741 // leave spend/total/currentNode stale, so the header would still show the old
27282742 // Net Spend / Total. Reset those transient fields too for a consistent wipe.
27292743 state . spentUdvpn = 0 ;
2744+ state . runSpentUdvpn = 0 ;
27302745 state . estimatedTotalCost = '0 P2P' ;
27312746 state . totalNodes = 0 ;
27322747 state . currentNode = null ;
@@ -2922,12 +2937,14 @@ app.post('/api/auto-retest', adminOnly, async (req, res) => {
29222937 const tracked = withBatchTracking ( broadcast , state . runMode || 'p2p' ) ;
29232938 runRetestSkips ( retestable . map ( r => r . address ) , state , tracked ) . then ( ( ) => {
29242939 // Persist back to the SAME run number — no new run is created.
2925- // Snapshot this-pass spend before the idle refresher can zero it; reset
2926- // state.spentUdvpn to the true cumulative for the live header + later Save.
2927- const passSpent = Number ( state . spentUdvpn ) || 0 ;
2940+ // Snapshot this-pass spend from runSpentUdvpn (the cumulative accumulator
2941+ // runRetestSkips reset to 0 at its top) before the idle refresher can zero
2942+ // it; set BOTH state.spentUdvpn (balance-delta) and state.runSpentUdvpn
2943+ // (cumulative) to the true cumulative for the live header + later Save.
2944+ const passSpent = Number ( state . runSpentUdvpn ) || 0 ;
29282945 try {
29292946 const r = persistActiveRun ( undefined , { passSpent } ) ;
2930- if ( r ) state . spentUdvpn = r . cumulativeSpent ;
2947+ if ( r ) { state . spentUdvpn = r . cumulativeSpent ; state . runSpentUdvpn = r . cumulativeSpent ; }
29312948 } catch ( e ) { console . error ( '[auto-retest] persistActiveRun failed:' , e . message ) ; }
29322949 } ) . catch ( err => {
29332950 state . status = 'error' ;
0 commit comments