fix(ui): stop spurious "Blockchain rescan complete" toast after restoring from minimize

While minimized the main loop skips the frame body (SDL_Delay+continue), so App::update() —
which drains the daemon output via outputSince(daemon_output_offset_) — never runs and the
offset isn't advanced. On restore the whole accumulated backlog is parsed in one batch: a
background witness rebuild's progress lines (parsed as a rescan → state_.sync.rescanning=true)
AND its "rebuilt … in Xms" completion (parsed as finished) arrive together and fire
"Blockchain rescan complete" for a scan the user never initiated.

On WINDOW_RESTORED, discard the daemon-output backlog (advance the offset to the current end,
via App::skipDaemonOutputBacklog) before the resumed update parses it — so only new output is
parsed. Genuine user-initiated rescans still surface completion via the getrescaninfo monitor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-01 00:49:06 -05:00
parent f7df315695
commit a3892c0fd3
3 changed files with 49 additions and 0 deletions

View File

@@ -5047,6 +5047,18 @@ void App::refreshNow()
invalidateShieldedHistoryScanProgress(true); invalidateShieldedHistoryScanProgress(true);
} }
void App::skipDaemonOutputBacklog()
{
// While minimized, App::update() is paused, so daemon_output_offset_ is never advanced and a large
// backlog of daemon output piles up. Parsing it all at once on restore would replay a background
// witness rebuild's progress + completion in a single batch and fire a spurious "Blockchain rescan
// complete" toast. Advance the offset to the current end so only NEW (post-restore) output is parsed.
// A genuine user-initiated rescan still surfaces completion via the getrescaninfo monitor.
if (daemon_controller_ && daemon_controller_->isRunning()) {
(void)daemon_controller_->outputSince(daemon_output_offset_); // advances daemon_output_offset_ to the end
}
}
void App::handlePaymentURI(const std::string& uri) void App::handlePaymentURI(const std::string& uri)
{ {
auto payment = util::parsePaymentURI(uri); auto payment = util::parsePaymentURI(uri);

View File

@@ -393,6 +393,10 @@ public:
// Force refresh // Force refresh
void refreshNow(); void refreshNow();
// Called on window restore: drop the daemon-output backlog that accumulated while minimized (the
// per-frame update loop was paused), so a background witness rebuild that started+finished during
// the minimize isn't parsed in one batch and mistaken for a completed rescan (spurious toast).
void skipDaemonOutputBacklog();
void refreshMiningInfo(); void refreshMiningInfo();
void refreshPeerInfo(); void refreshPeerInfo();
void refreshMarketData(); void refreshMarketData();

View File

@@ -1263,6 +1263,36 @@ int main(int argc, char* argv[])
SDL_SetWindowMinimumSize(window, (int)(1024 * currentDpiScale), (int)(720 * currentDpiScale)); SDL_SetWindowMinimumSize(window, (int)(1024 * currentDpiScale), (int)(720 * currentDpiScale));
} }
// DEV/TEST hook (dormant unless the env is set): DRAGONX_WIN_GEOM="WxH" forces an exact window
// size, placing it on the largest display that can hold it and bypassing the primary-monitor
// clamp. Lets a headless WSLg sweep render at sizes wider than the 1280 primary (e.g. 2560x1440
// on the mirrored 4K/1440p outputs). Needs the x11 backend so absolute positioning takes effect.
int wantW = 0, wantH = 0;
if (const char* geom = std::getenv("DRAGONX_WIN_GEOM"))
sscanf(geom, "%dx%d", &wantW, &wantH);
if (wantW > 0 && wantH > 0) {
int count = 0;
SDL_DisplayID* disp = SDL_GetDisplays(&count);
SDL_DisplayID best = 0; SDL_Rect bestUsable{0, 0, 0, 0};
for (int i = 0; i < count; ++i) {
SDL_Rect u;
if (!SDL_GetDisplayUsableBounds(disp[i], &u)) continue;
bool fits = (u.w >= wantW && u.h >= wantH);
bool bestFits = (bestUsable.w >= wantW && bestUsable.h >= wantH);
// Prefer a display that fits; among those, the smallest; else the largest available.
if ((fits && !bestFits) ||
(fits && bestFits && (long)u.w * u.h < (long)bestUsable.w * bestUsable.h) ||
(!fits && !bestFits && (long)u.w * u.h > (long)bestUsable.w * bestUsable.h)) {
bestUsable = u; best = disp[i];
}
}
if (disp) SDL_free(disp);
if (best) {
SDL_SetWindowMinimumSize(window, 320, 240);
SDL_SetWindowPosition(window, bestUsable.x + 10, bestUsable.y + 10);
SDL_SetWindowSize(window, wantW, wantH);
}
} else {
// Clamp to the current display's work area — runs on EVERY startup (this clamp used to live // Clamp to the current display's work area — runs on EVERY startup (this clamp used to live
// inside the HiDPI branch, so a size saved on a larger/disconnected monitor could open the // inside the HiDPI branch, so a size saved on a larger/disconnected monitor could open the
// window off-screen or bigger than the screen on a same-DPI cold start). // window off-screen or bigger than the screen on a same-DPI cold start).
@@ -1282,6 +1312,7 @@ int main(int argc, char* argv[])
curW, curH, newW, newH, currentDpiScale); curW, curH, newW, newH, currentDpiScale);
} }
} }
}
#endif #endif
winlog("STARTUP savedSize=%dx%d currentDpiScale=%.3f", savedWinW, savedWinH, currentDpiScale); winlog("STARTUP savedSize=%dx%d currentDpiScale=%.3f", savedWinW, savedWinH, currentDpiScale);
winlogState(window, " startup"); winlogState(window, " startup");
@@ -1552,6 +1583,7 @@ int main(int argc, char* argv[])
// Window restored from minimized — trigger immediate data refresh // Window restored from minimized — trigger immediate data refresh
if (waitEvent.type == SDL_EVENT_WINDOW_RESTORED && if (waitEvent.type == SDL_EVENT_WINDOW_RESTORED &&
waitEvent.window.windowID == SDL_GetWindowID(window)) { waitEvent.window.windowID == SDL_GetWindowID(window)) {
app.skipDaemonOutputBacklog(); // drop the minimized-period backlog (avoids a spurious "rescan complete" toast)
app.refreshNow(); app.refreshNow();
} }
// Handle DPI change that arrived while idle (same logic as poll loop) // Handle DPI change that arrived while idle (same logic as poll loop)
@@ -1681,6 +1713,7 @@ int main(int argc, char* argv[])
// Window restored from minimized — trigger immediate data refresh // Window restored from minimized — trigger immediate data refresh
if (event.type == SDL_EVENT_WINDOW_RESTORED && if (event.type == SDL_EVENT_WINDOW_RESTORED &&
event.window.windowID == SDL_GetWindowID(window)) { event.window.windowID == SDL_GetWindowID(window)) {
app.skipDaemonOutputBacklog(); // drop the minimized-period backlog (avoids a spurious "rescan complete" toast)
app.refreshNow(); app.refreshNow();
} }
// Handle DPI/display scale changes (e.g. window dragged to a // Handle DPI/display scale changes (e.g. window dragged to a