Compare commits

..

13 commits

Author SHA1 Message Date
Lukas Brübach
2916fa45af [DeckSync] Match server rows by identity and hash the whole deck
Sync keyed local files to server rows by path, but the server names a row
after the deck, not after the file, so any deck whose file name and deck name
differ uploaded again on every run. A second pass then brought the server copy
back as an extra local file.

- Match by the sidecar's row id, then guid, then content, and only then by the
  server path, so renaming either side no longer duplicates a deck.
- Key the server tree by row id: the server allows several rows with the same
  name in one folder, and keying by path silently dropped all but one.
- Stop discarding rows whose name contains '/' ("U/W Control"). They stay
  matchable, and only the download file name is sanitised.
- Build the download path from the folder plus a sanitised row name, including
  every folder segment, so a hostile server name cannot escape the deck folder.
- Download a colliding server row under a unique name and count it, instead of
  skipping it without a trace.
- Hash the whole deck instead of the card hash: SHA-1 over the native deck
  string covers name, printings, banner, tokens and sideboard plans. The client
  and the server hash the exact bytes they exchange, so both agree.

Adds deck_content_hash_test for the client/server hashing invariant and for the
name-only change the card hash cannot see.
2026-11-01 22:23:25 +01:00
Lukas Brübach
ad0da98a86 [DeckSync] Pass the sync summary through the signal and tidy the manager API
- Emit SyncSummary from syncFinished() instead of four positional ints, and
  build it from a single currentSummary() helper, so the counts cannot drift
  between the status message and the signal.
- Use a designated initializer for LocalFile, like the other records.
- Drop setClient() and setColorIdentityProvider(): both fields are already
  constructor parameters and the setters had no callers. setProfile() and
  setDeckPath() stay, since the tab re-reads them before every run.
- Fix the include paths in tab_deck_storage.h and the narrowing conversion of
  the tree item id, so the branch builds again.
2026-11-01 22:23:25 +01:00
BruebachL
c0a62a2486 [Format] Fix include order in tab_deck_storage.h 2026-11-01 22:23:25 +01:00
BruebachL
0757d3d867 [DeckSync] Use designated initializer for ServerFile per review 2026-11-01 22:23:25 +01:00
BruebachL
8a541531da [DeckSync] Prefer ctor params for set-once client/provider per review 2026-11-01 22:23:25 +01:00
BruebachL
e1a9f7766d [DeckSync] Move SyncSummary into DeckSyncManager per review 2026-11-01 22:23:25 +01:00
Lukas Brübach
ad1cb8f133 Fix ZeldaZach review for PR 7493: match uploads by sidecar remoteId/guid, validate server paths 2026-11-01 22:23:25 +01:00
Lukas Brübach
e63b72fe11 [Client] Manual deck collection sync with Sync Now action 2026-11-01 22:23:25 +01:00
Lukas Brübach
fa4c5766eb Cover merge printing preservation with a test
Base deck with 4 Mountain (DOM 250), local adds 2 Shock (M19 156), remote
adds an unrelated card. Asserts the merge keeps one Mountain node with its
printing and adds a single Shock node carrying M19/156.
2026-11-01 22:11:40 +01:00
BruebachL
15673dc481 [DeckDiff] Use namespace instead of static-only class per review 2026-11-01 22:11:40 +01:00
Lukas Brübach
da106fdef7 Fixup: clang-format alignment in deck_diff.h 2026-11-01 22:11:40 +01:00
Lukas Brübach
f661468069 Fix ZeldaZach review for PR 7492: preserve printing in DeckDiff::merge, atomic sidecar save with QSaveFile 2026-11-01 22:11:40 +01:00
Lukas Brübach
be072ffd68 [Client] DeckSyncProfile + sidecar + DeckDiff with tests 2026-11-01 22:11:40 +01:00
3 changed files with 17 additions and 14 deletions

View file

@ -9,15 +9,16 @@
#include <QString>
/**
* @struct DeckSyncProfile
* @class DeckSyncProfile
* @brief Identifies which server and login a sync sidecar belongs to.
*
* A local deck folder can sync against several servers. Every server login
* gets its own profile, and every profile gets its own sidecar file, so
* switching servers never mixes up remote deck ids.
*/
struct DeckSyncProfile
class DeckSyncProfile
{
public:
QString host; ///< Server hostname as entered by the user.
int port = 0; ///< Server port.
QString username; ///< Login name on that server.

View file

@ -37,11 +37,12 @@ bool DeckSyncSidecar::load()
continue;
}
const QJsonObject entry = it.value().toObject();
const DeckSyncRecord record{.guid = entry.value("guid").toString(),
.remoteId = entry.value("remoteId").toInt(-1),
.baseHash = entry.value("baseHash").toString(),
.lastSyncSecs = static_cast<qint64>(entry.value("lastSync").toDouble()),
.deleted = entry.value("deleted").toBool(false)};
DeckSyncRecord record;
record.guid = entry.value("guid").toString();
record.remoteId = entry.value("remoteId").toInt(-1);
record.baseHash = entry.value("baseHash").toString();
record.lastSyncSecs = static_cast<qint64>(entry.value("lastSync").toDouble());
record.deleted = entry.value("deleted").toBool(false);
recordMap.insert(it.key(), record);
}
return true;

View file

@ -70,13 +70,14 @@ DeckDiffResult diff(const DeckList &oldDeck, const DeckList &newDeck)
const int oldCount = oldCounts.value(key, 0);
const ZoneCardInfo newInfo = newInfos.value(key, ZoneCardInfo());
if (oldCount != newInfo.count) {
const DeckCardChange change{.zone = key.first,
.cardName = key.second,
.oldCount = oldCount,
.newCount = newInfo.count,
.cardSetShortName = newInfo.cardSetShortName,
.cardSetNumber = newInfo.cardSetNumber,
.cardProviderId = newInfo.cardProviderId};
DeckCardChange change;
change.zone = key.first;
change.cardName = key.second;
change.oldCount = oldCount;
change.newCount = newInfo.count;
change.cardSetShortName = newInfo.cardSetShortName;
change.cardSetNumber = newInfo.cardSetNumber;
change.cardProviderId = newInfo.cardProviderId;
result.changes.append(change);
}
}