From fa6c2d75d61e58c214f30a7394b974e8bae87679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= <44814898+BruebachL@users.noreply.github.com> Date: Sun, 8 Nov 2026 19:37:34 +0100 Subject: [PATCH] [DeckSync] Keep the sync baseline when a server change is skipped The check's server_time became the new baseline even when the run could not apply a server change (a failed download, or a mirrored delete whose local copy could not be removed), and manual Sync now goes through the same fast path, so the change never came back. Track those skips and keep the previous baseline so the next check reports them again. Also disable Sync now while any run is in progress, drop the redundant baseline assignment in onCheckFinished, and tell the user that only two-way sync checks for changes automatically. --- .../interface/deck_sync/deck_sync_manager.cpp | 16 ++++++++++++++-- .../src/interface/deck_sync/deck_sync_manager.h | 1 + .../widgets/dialogs/dlg_sync_onboarding.cpp | 3 ++- .../interface/widgets/tabs/tab_deck_storage.cpp | 11 +++++++++++ .../interface/widgets/tabs/tab_deck_storage.h | 1 + 5 files changed, 29 insertions(+), 3 deletions(-) diff --git a/cockatrice/src/interface/deck_sync/deck_sync_manager.cpp b/cockatrice/src/interface/deck_sync/deck_sync_manager.cpp index 80ef22687..34b71a452 100644 --- a/cockatrice/src/interface/deck_sync/deck_sync_manager.cpp +++ b/cockatrice/src/interface/deck_sync/deck_sync_manager.cpp @@ -321,6 +321,7 @@ void DeckSyncManager::startSync(bool _autoRun) sessionDefault = -1; firstSyncRun = false; pendingServerTime = -1; + serverChangeSkipped = false; emit syncStarted(); emit statusMessage(tr("Scanning local decks...")); @@ -426,7 +427,6 @@ void DeckSyncManager::onCheckFinished(const Response &response, const CommandCon return; } - pendingServerTime = status.has_server_time() ? static_cast(status.server_time()) : -1; unchangedCount = localFiles.size(); emit statusMessage(tr("Decks are already in sync.")); finishSync(); @@ -641,6 +641,10 @@ void DeckSyncManager::classify(const QMap &serverFiles) const QString backupPath = absolutePath + BACKUP_SUFFIX + QString::number(QDateTime::currentDateTime().toSecsSinceEpoch()); if (!QFile::copy(absolutePath, backupPath) || !QFile::remove(absolutePath)) { + // The server row is gone but the local file could not be + // removed. Keep the baseline so the next check still reports + // the delete instead of hiding it behind a newer sync time. + serverChangeSkipped = true; ++skippedCount; continue; } @@ -1249,6 +1253,7 @@ void DeckSyncManager::onDownloadFinished(const Response &response, return; } if (response.response_code() != Response::RespOk) { + serverChangeSkipped = true; ++skippedCount; processNext(); return; @@ -1275,6 +1280,7 @@ void DeckSyncManager::applyDownloadedDeck(const QString &relativePath, { DeckList deck; if (!deck.loadFromString_Native(deckString)) { + serverChangeSkipped = true; ++skippedCount; processNext(); return; @@ -1283,6 +1289,7 @@ void DeckSyncManager::applyDownloadedDeck(const QString &relativePath, const QString cleanDeckPath = QDir::cleanPath(deckPath); const QString absolutePath = QDir::cleanPath(deckPath + "/" + relativePath); if (absolutePath != cleanDeckPath && !absolutePath.startsWith(cleanDeckPath + "/")) { + serverChangeSkipped = true; ++skippedCount; processNext(); return; @@ -1291,6 +1298,7 @@ void DeckSyncManager::applyDownloadedDeck(const QString &relativePath, const QString backupPath = absolutePath + BACKUP_SUFFIX + QString::number(QDateTime::currentDateTime().toSecsSinceEpoch()); if (!QFile::copy(absolutePath, backupPath)) { + serverChangeSkipped = true; ++skippedCount; processNext(); return; @@ -1298,6 +1306,7 @@ void DeckSyncManager::applyDownloadedDeck(const QString &relativePath, } QDir().mkpath(QFileInfo(absolutePath).absolutePath()); if (!DeckLoader::saveToFile(deck, absolutePath, DeckFileFormat::Cockatrice)) { + serverChangeSkipped = true; ++skippedCount; processNext(); return; @@ -1406,7 +1415,10 @@ void DeckSyncManager::finishSync() { syncing = false; DeckSyncSidecar sidecar(sidecarPath); - if (sidecar.load()) { + if (sidecar.load() && !serverChangeSkipped) { + // Advance the baseline only when every server change was applied. If one + // was skipped, keep the previous baseline so the next check reports it + // again instead of hiding it behind a newer sync time. sidecar.lastSyncSecs = pendingServerTime >= 0 ? pendingServerTime : QDateTime::currentDateTime().toSecsSinceEpoch(); sidecar.save(); diff --git a/cockatrice/src/interface/deck_sync/deck_sync_manager.h b/cockatrice/src/interface/deck_sync/deck_sync_manager.h index e3d748996..220f28039 100644 --- a/cockatrice/src/interface/deck_sync/deck_sync_manager.h +++ b/cockatrice/src/interface/deck_sync/deck_sync_manager.h @@ -255,6 +255,7 @@ private: int removedCount = 0; int unchangedCount = 0; int skippedCount = 0; + bool serverChangeSkipped = false; ///< A server change could not be applied; keep the sync baseline. int conflictCount = 0; bool autoRun = false; }; diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_sync_onboarding.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_sync_onboarding.cpp index 23bf663d5..32504fa75 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_sync_onboarding.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_sync_onboarding.cpp @@ -37,7 +37,8 @@ void DlgSyncOnboarding::retranslateUi() setWindowTitle(tr("Sync decks to this server")); detailLabel->setText( tr("You have %1 local decks and %2 decks stored on this server. This choice sets how decks sync with this " - "server from now on; you can reset it to two-way sync from the deck storage tab.") + "server from now on; you can reset it to two-way sync from the deck storage tab. Only two-way sync keeps " + "checking for changes automatically.") .arg(localCount) .arg(serverCount)); mergeRadio->setText(tr("Merge: keep everything, sync both ways (recommended)")); diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp index 25a183c12..e38d72fef 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.cpp @@ -283,6 +283,7 @@ TabDeckStorage::TabDeckStorage(TabSupervisor *_tabSupervisor, connect(&SettingsCache::instance().deckSync(), &DeckSyncSettings::syncIntervalMinutesChanged, syncManager, &DeckSyncManager::setSyncIntervalMinutes); connect(syncManager, &DeckSyncManager::statusMessage, this, &TabDeckStorage::onSyncStatusMessage); + connect(syncManager, &DeckSyncManager::syncStarted, this, &TabDeckStorage::onSyncStarted); connect(syncManager, &DeckSyncManager::syncFinished, this, &TabDeckStorage::onSyncFinished); connect(syncManager, &DeckSyncManager::firstSyncDetected, this, &TabDeckStorage::onFirstSyncDetected); connect(syncManager, &DeckSyncManager::conflictRequest, this, &TabDeckStorage::onConflictRequest); @@ -656,9 +657,19 @@ void TabDeckStorage::onSyncStatusMessage(const QString &message) renderSyncStatus(); } +void TabDeckStorage::onSyncStarted() +{ + // An auto run starts from the watcher or the interval timer, so the button + // has to follow the manager: a click during a run would be dropped by + // syncNow() and leave the user pressing a button that looks live. + aSyncNow->setEnabled(false); + aSyncReset->setEnabled(false); +} + void TabDeckStorage::onSyncFinished(const SyncSummary &summary, const QString &error) { aSyncNow->setEnabled(remoteEnabled); + aSyncReset->setEnabled(remoteEnabled); if (!error.isEmpty()) { syncStatusType = SyncStatusType::Failed; syncStatusDetail = error; diff --git a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.h b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.h index 9889e9a51..20fdab0fb 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_deck_storage.h +++ b/cockatrice/src/interface/widgets/tabs/tab_deck_storage.h @@ -135,6 +135,7 @@ private slots: void onFirstSyncDetected(int localCount, int serverCount); void onConflictRequest(const DeckSyncConflict &conflict); void onSyncStatusMessage(const QString &message); + void onSyncStarted(); void onSyncFinished(const SyncSummary &summary, const QString &error); public: