Skip to content

Commit e0d4ca9

Browse files
committed
Complete remote Storage and M26.4 integration
1 parent 5f632a8 commit e0d4ca9

22 files changed

Lines changed: 279 additions & 42 deletions

CMakeLists.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,13 @@ target_link_libraries(BrickSuite PRIVATE
454454
include(CTest)
455455

456456
if(BUILD_TESTING)
457+
qt_add_executable(BuildActionEligibilityTest EXCLUDE_FROM_ALL
458+
tests/BuildActionEligibilityTest.cpp
459+
src/ui/builds/BuildActionEligibility.h
460+
)
461+
target_link_libraries(BuildActionEligibilityTest PRIVATE Qt6::Core)
462+
add_test(NAME BuildActionEligibility COMMAND BuildActionEligibilityTest)
463+
457464
qt_add_executable(SingleInstanceWindowRegistryTest EXCLUDE_FROM_ALL
458465
tests/SingleInstanceWindowRegistryTest.cpp
459466
src/ui/common/SingleInstanceWindowRegistry.h
@@ -1055,6 +1062,7 @@ if(BUILD_TESTING)
10551062
src/database/DatabaseSchema.h src/database/DatabaseSchema.cpp
10561063
src/models/Workspace.h src/models/Workspace.cpp
10571064
src/models/StorageLocation.h src/models/StorageLocation.cpp
1065+
src/models/StorageLocationType.h src/models/StorageLocationType.cpp
10581066
src/models/InventoryRecord.h src/models/InventoryRecord.cpp
10591067
src/models/InventoryMovement.h src/models/InventoryMovement.cpp
10601068
src/models/InventorySearchCriteria.h src/models/InventorySearchResult.h
@@ -1071,6 +1079,7 @@ if(BUILD_TESTING)
10711079
src/repositories/RepositoryConnection.h
10721080
src/repositories/WorkspaceRepository.h src/repositories/WorkspaceRepository.cpp
10731081
src/repositories/StorageLocationRepository.h src/repositories/StorageLocationRepository.cpp
1082+
src/repositories/StorageLocationTypeRepository.h src/repositories/StorageLocationTypeRepository.cpp
10741083
src/repositories/InventoryRecordRepository.h src/repositories/InventoryRecordRepository.cpp
10751084
src/repositories/InventoryMovementRepository.h src/repositories/InventoryMovementRepository.cpp
10761085
src/repositories/BuildRepository.h src/repositories/BuildRepository.cpp

resources/help/backup_restore.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<!doctype html><html><head><meta charset="utf-8"><title>Backup / Restore</title><style>body{font-family:sans-serif;line-height:1.5;margin:22px}h2{margin-top:1.5em}.note{border-left:4px solid #888;padding-left:10px;margin:14px 0}img{display:block;max-width:100%;height:auto;margin:14px auto}</style></head><body>
22
<h1>Backup / Restore</h1>
3+
<div class="note"><b>Remote Client backups:</b> backing up a computer configured as a BrickSuite Host Client protects that device's local database and catalogs; it does not back up the Host's authoritative workshop data. Run the Host database backup workflow on the Host computer.</div>
34
<h2>Create a manual backup</h2><ol><li>Choose <b>File → Backup Database...</b>.</li><li>Select a destination and filename outside the live application-data location.</li><li>Confirm the operation and wait for its completion message.</li><li>Retain important copies on a separate physical or synchronized storage device.</li></ol><img src="images/backup_restore_backup.png" alt="Backup Database"><p>Manual Backup is the explicit preservation workflow and remains independent of the automatic-backup policy. Automatic retention never removes manual files.</p>
45
<h2>Automatic verified backups</h2><p>Under <b>Settings → Database Backup</b>, select a backup root, frequency, retained count, and enable scheduling. Policy changes take effect only after Apply or OK. Recognized backups are stored beneath <code>&lt;backup root&gt;/v&lt;schema version&gt;</code>, keeping different database formats separate.</p><p>Before Backup Now or a scheduled attempt, a worker-owned database connection consumes the full <code>integrity_check</code> and <code>foreign_key_check</code> results. A healthy source proceeds to SQLite snapshot creation; the snapshot is then reopened and verified before publication. Only after all of those stages pass may retention remove older recognized automatic backups.</p><p><b>Backup Now</b> uses this same health and verification pipeline. It works while scheduling is disabled and bypasses the scheduled failure retry gate, but it does not enable scheduling.</p>
56
<h2>When an automatic attempt fails</h2><ul><li>A confirmed integrity problem or foreign-key violation skips snapshot and retention and preserves every existing backup.</li><li>A busy, locked, open, or query failure is recorded as unsuccessful/deferred, not as confirmed corruption.</li><li>A snapshot or verification failure does not update Last Successful and does not run retention.</li></ul><p>Open <b>Tools → Database Status &amp; Integrity</b> and the Application Log for details.</p>

resources/help/bricksuite_server.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ <h1>BrickSuite Server</h1>
44
<p>BrickSuite Server lets another BrickSuite installation establish a secure connection to the computer that owns the authoritative workshop database. A connected Client can browse Host Workspaces, Storage used by Inventory, My Inventory, Inventory History, Builds, Missing Parts, Pulling, My Collection, Collection Details, and shared user Part Reference customizations. Remote shared data is read-only at this stage; changes still require using the Host computer.</p>
55
<p>The built-in Part Reference manifest remains local and is usable immediately; authenticated Host customizations are overlaid when available. Set, Minifig, and Part catalog presentation and image caches also remain local to the Client. Collection and Part Reference customization add, edit, archive, and remove operations are not available remotely.</p>
66
<p>The Client chooses its Host Workspace independently. This does not change the Workspace selected in the Host computer's UI. The selection is remembered on the Client for that Host identity and validated again after reconnecting.</p>
7-
<div class="note"><b>Hybrid data:</b> operational Workspace, Storage, Inventory, History, Build requirements, allocations, Missing Parts, and Pulling state come from the Host. Part and Color names and images continue to use the Client device's local Rebrickable catalog and image cache. Unknown local catalog items remain visible using Host fallback text.</div>
7+
<div class="note"><b>Hybrid data:</b> operational Workspace, Storage, Inventory, History, Build requirements, allocations, Missing Parts, Pulling state, and Collection come from the Host. Catalog names, compositions, built-in Part Reference content, and images continue to use the Client device's local Rebrickable data and image cache. Unknown local catalog items remain visible using Host fallback text.</div>
88
<p>In <b>Builds</b>, a Client can view the Host Build list, immutable requirement snapshots, Host-calculated Missing Parts, and the current Pulling projection. Pulling is read-only: creating or editing Builds, allocating Inventory, recording pulls, reconciling pull lists, completing, cancelling, archiving, or disassembling a Build is not available remotely yet.</p>
9-
<p>Remote Collection workflows are not yet enabled in the application UI.</p>
9+
<p>The Storage tab shows the selected Host Workspace's complete hierarchy with editing controls disabled. My Collection and Collection Details likewise show Host-owned records read-only. User Part Reference customizations come from the Host and overlay the Client's local built-in manifest; customization changes still require the Host computer.</p>
1010
<h2>Security model</h2>
1111
<p>Connections use <b>WSS/TLS</b>. Each Host generates its own persistent certificate and private key. The Client verifies its SHA-256 fingerprint. A separate access token authorizes the Client through a nonce-based HMAC-SHA-256 challenge response and is stored through the operating-system credential service.</p>
1212
<div class="note"><b>Verify before trusting.</b> Compare the Client fingerprint with the fingerprint shown directly in Server Settings on the Host computer. First-use acceptance without that comparison can trust the wrong computer.</div>

resources/help/quick_start.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<!doctype html><html><head><meta charset="utf-8"><title>Quick Start</title><style>body{font-family:sans-serif;line-height:1.5;margin:22px}h2{margin-top:1.5em}.note{border-left:4px solid #888;padding-left:10px;margin:14px 0}img{display:block;max-width:100%;height:auto;margin:14px auto}</style></head><body>
2-
<h1>Quick Start</h1><p>This sequence establishes a useful workshop without requiring every advanced feature.</p>
2+
<h1>Quick Start</h1><p>This sequence establishes a useful workshop without requiring every advanced feature. To configure a second computer as a read-only Client of an existing workshop, see <a href="bricksuite_server.html">BrickSuite Server</a>.</p>
33
<h2>1. Create a Workspace</h2><p>Use the Workspace tab to create or select the logical workshop. Settings can choose the Workspace selected at later startup.</p>
44
<h2>2. Create Storage</h2><p>Add the hierarchy from broad areas down to usable leaves. Set each location's <b>Used For</b> capability. Loose inventory requires an active Inventory or Both leaf; Collection items require an active Collection or Both leaf.</p><img src="images/quick_start_storage.png" alt="Storage setup">
55
<h2>3. Configure providers when useful</h2><p>Rebrickable API configuration is optional for bulk-file import. Add it under <b>Settings → APIs</b> when you want API-backed images, enrichment, or an explicit <b>Get Parts from Rebrickable...</b> refresh. Add Brickset for Set enrichment and instructions.</p><img src="images/quick_start_rebrickable_api.png" alt="Rebrickable API settings"><img src="images/quick_start_brickset_api.png" alt="Brickset API settings">

resources/help/settings.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ <h2>General</h2>
4343
effect after restarting BrickSuite; it never copies, merges, or deletes either source's data.</p>
4444

4545
<div class="note"><b>Remote data:</b> After authentication, the Client loads Host Workspaces and
46-
can browse Host My Inventory and Inventory History. The selected Host Workspace is remembered for
46+
can browse Host Storage, My Inventory, Inventory History, Builds, Pulling, and My Collection. The selected Host Workspace is remembered for
4747
that Host identity and does not change the Workspace selected in the Host computer's UI. Remote
48-
Inventory is read-only; local catalog browsing and Rebrickable file imports remain available.</div>
48+
workshop data is read-only; local catalog browsing, built-in Part Reference, images, and Rebrickable file imports remain available.</div>
4949

5050
<h2>Server</h2>
5151
<p>The <b>Server</b> page configures the optional BrickSuite WSS Host and remote Client connection.

src/repositories/StorageLocationTypeRepository.cpp

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@
2020

2121
#include "StorageLocationTypeRepository.h"
2222

23-
#include "../database/DatabaseManager.h"
24-
2523
#include <QDebug>
2624
#include <QSqlError>
2725
#include <QSqlQuery>
@@ -30,7 +28,7 @@ QList<StorageLocationType> StorageLocationTypeRepository::getAll() const
3028
{
3129
QList<StorageLocationType> types;
3230

33-
QSqlDatabase database = DatabaseManager::instance().database();
31+
QSqlDatabase database = repositoryDatabase();
3432

3533
QSqlQuery query(database);
3634

@@ -60,7 +58,7 @@ QList<StorageLocationType> StorageLocationTypeRepository::getActive() const
6058
{
6159
QList<StorageLocationType> types;
6260

63-
QSqlDatabase database = DatabaseManager::instance().database();
61+
QSqlDatabase database = repositoryDatabase();
6462

6563
QSqlQuery query(database);
6664

@@ -90,7 +88,7 @@ QList<StorageLocationType> StorageLocationTypeRepository::getActive() const
9088

9189
std::optional<StorageLocationType> StorageLocationTypeRepository::getById(int id) const
9290
{
93-
QSqlDatabase database = DatabaseManager::instance().database();
91+
QSqlDatabase database = repositoryDatabase();
9492

9593
QSqlQuery query(database);
9694

@@ -137,4 +135,4 @@ StorageLocationType StorageLocationTypeRepository::locationTypeFromQuery(const Q
137135
type.setSortOrder(query.value("sort_order").toInt());
138136

139137
return type;
140-
}
138+
}

src/repositories/StorageLocationTypeRepository.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,20 @@
2020

2121
#pragma once
2222

23+
#include "RepositoryConnection.h"
2324
#include "../models/StorageLocationType.h"
2425

2526
#include <QList>
2627
#include <optional>
2728

2829
class QSqlQuery;
2930

30-
class StorageLocationTypeRepository
31+
class StorageLocationTypeRepository : protected RepositoryConnection
3132
{
3233
public:
34+
StorageLocationTypeRepository() = default;
35+
explicit StorageLocationTypeRepository(const QSqlDatabase& database)
36+
: RepositoryConnection(database) {}
3337
QList<StorageLocationType> getAll() const;
3438

3539
QList<StorageLocationType> getActive() const;

src/services/application/HostReadExecutor.cpp

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
#include "HostReadExecutor.h"
22

33
#include "../../repositories/StorageLocationRepository.h"
4+
#include "../../repositories/StorageLocationTypeRepository.h"
45
#include "../../repositories/PartRepository.h"
56
#include "../../repositories/ColorRepository.h"
67
#include "../../repositories/PartCategoryRepository.h"
78
#include "../parts/PartReferenceManifest.h"
89

910
#include <QElapsedTimer>
11+
#include <QHash>
1012
#include <QDateTime>
1113
#include <QPointer>
1214
#include <QSqlError>
1315
#include <QSqlQuery>
16+
#include <QSet>
17+
#include <QStringList>
1418
#include <QUuid>
1519

1620
class HostReadExecutor::Worker : public QObject
@@ -166,6 +170,28 @@ void HostReadExecutor::getWorkspace(int id, QObject* context,
166170
void HostReadExecutor::listStorage(int workspaceId, QObject* context,
167171
std::function<void(const QList<StorageLocation>&)> completion, ErrorCallback failure)
168172
{ HOST_READ_METHOD_BODY("storage.list", StorageLocationRepository(database).getByWorkspace(workspaceId), QList<StorageLocation>); }
173+
void HostReadExecutor::listStoragePortable(int workspaceId, bool includeInactive, QObject* context,
174+
std::function<void(const std::optional<QList<RemoteReadDto::StorageSummary>>&)> completion,
175+
ErrorCallback failure)
176+
{
177+
QPointer<QObject> guard(context);
178+
enqueue(QStringLiteral("storage.list"), [=, completion=std::move(completion)]
179+
(ApplicationServices& services, const QSqlDatabase& database) mutable {
180+
if (!services.workspaces().exists(workspaceId)) {
181+
if (guard) QMetaObject::invokeMethod(guard, [guard,completion]() { if(guard) completion(std::nullopt); }, Qt::QueuedConnection);
182+
return;
183+
}
184+
StorageLocationRepository locations(database); StorageLocationTypeRepository types(database);
185+
const auto rows = includeInactive ? locations.getByWorkspaceIncludingInactive(workspaceId)
186+
: locations.getByWorkspace(workspaceId);
187+
QHash<int,QString> typeNames; for (const auto& type : types.getAll()) typeNames.insert(type.id(),type.name());
188+
QHash<int,StorageLocation> byId; for(const auto& row:rows) byId.insert(row.id(),row);
189+
QList<RemoteReadDto::StorageSummary> result;
190+
for(const auto& row:rows){QStringList names;QSet<int>seen;int id=row.id();while(id>0&&byId.contains(id)&&!seen.contains(id)){seen.insert(id);names.prepend(byId[id].name());id=byId[id].parentLocationId();}
191+
result.append({row.id(),row.parentLocationId(),row.name(),names.join(QStringLiteral(" / ")),typeNames.value(row.locationTypeId()),row.sortOrder(),row.isActive(),row.allowsInventory(),row.allowsCollection()});}
192+
if(guard)QMetaObject::invokeMethod(guard,[guard,completion,result=std::move(result)]()mutable{if(guard)completion(result);},Qt::QueuedConnection);
193+
}, context, std::move(failure));
194+
}
169195
void HostReadExecutor::searchInventory(const InventorySearchCriteria& criteria, QObject* context,
170196
std::function<void(const InventoryApplicationService::Page&)> completion, ErrorCallback failure)
171197
{ HOST_READ_METHOD_BODY("inventory.search", services.inventory().search(criteria), InventoryApplicationService::Page); }

src/services/application/HostReadExecutor.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ class HostReadExecutor : public QObject
3838
void listStorage(int workspaceId, QObject* context,
3939
std::function<void(const QList<StorageLocation>&)> completion,
4040
ErrorCallback failure = {});
41+
void listStoragePortable(int workspaceId, bool includeInactive, QObject* context,
42+
std::function<void(const std::optional<QList<RemoteReadDto::StorageSummary>>&)> completion,
43+
ErrorCallback failure = {});
4144
void searchInventory(const InventorySearchCriteria& criteria, QObject* context,
4245
std::function<void(const InventoryApplicationService::Page&)> completion,
4346
ErrorCallback failure = {});

0 commit comments

Comments
 (0)