Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

dbtool-gui: managed profile backups (backup all / restore with target picker)#523

Open
Yaraslaut wants to merge 36 commits into
masterLASTRADA-Software/Lightweight:masterfrom
feature/dbtool-gui-managed-backupsLASTRADA-Software/Lightweight:feature/dbtool-gui-managed-backupsCopy head branch name to clipboard
Open

dbtool-gui: managed profile backups (backup all / restore with target picker)#523
Yaraslaut wants to merge 36 commits into
masterLASTRADA-Software/Lightweight:masterfrom
feature/dbtool-gui-managed-backupsLASTRADA-Software/Lightweight:feature/dbtool-gui-managed-backupsCopy head branch name to clipboard

Conversation

@Yaraslaut

@Yaraslaut Yaraslaut commented Jul 11, 2026

Copy link
Copy Markdown
Member

Summary

Adds a managed backup workflow to dbtool-gui: configure a backup folder once (Settings), press Backup all to back up every profile's database into it — one safely-overwritten <profile>.zip each — and restore any archive into its own profile, a different profile, or a custom connection string. Backup/restore is promoted out of the experimental --enable-backup-restore flag.

Design doc: docs/superpowers/specs/2026-07-11-dbtool-gui-managed-backups-design.md (visual mockups maintained in the Claude Design project dbtool-gui).

What's new

  • ManagedBackupCore — pure, Qt-free decision logic (archive naming/sanitization + collision detection, folder scan, atomic tmp→rename overwrite, ProfileStore+SecretResolver connection-string resolution).
  • ManagedBackupController + BackupStatusListModel — sequential per-profile orchestration on a worker thread; per-row status (queued/running/ok/failed + archive size/date); guards against concurrent migration/backup runs; secrets resolved off the GUI thread.
  • BackupsPage.qml — replaces the bare BackupRestoreDialog; profile list with status pills, per-row Backup/Restore, destructive-restore confirmation with a Restore to target picker (cross-target warning), and a Custom-archive section for ad-hoc paths.
  • Settings: new "Backups folder" card (QSettings backup/folder).
  • Library fixes (benefit dbtool CLI too): SqlBackup/Restore helpers no longer connect via the unset global default connection string; metadata.json now redacts PWD=/Password= values instead of embedding resolved plaintext credentials.
  • Gate removal: --enable-backup-restore, AppController::backupRestoreEnabled, BackupRunner::setEnabled deleted.

Performance

Backup/restore run with jobs = min(8, hardware_concurrency) and zstd (fallback deflate). Benchmarked on an 87 MB / 800k-row database: restore ~2.4–3 s (MSSQL 2022 jobs=8 / SQLite) vs ~6 s at dbtool's CLI default --jobs 1 on MSSQL.

Risk assessment

  • Destructive-op safety: restores confirm in a dialog naming archive, timestamp, and target; archive-name collisions between profiles are refused (never silently merged) on backup-all, per-profile backup, and restore.
  • Threading: single worker thread; all model/phase mutations marshalled to the GUI thread via queued invokes; a latent use-after-scope in the progress bridge (pre-existing in BackupRunner) was fixed in both copies.
  • Library change blast radius: the SqlConnection{std::nullopt} fix is behavior-preserving for all existing callers (every site immediately calls Connect(...)); metadata redaction is write-only data no restore path consumes.
  • Windows: narrow-string fs::path conversions (pre-existing pattern) may mishandle non-ASCII folder names — follow-up candidate.

Code coverage (clang-coverage, dbtool-gui label)

ManagedBackupCore.cpp 100% · BackupStatusListModel.cpp 98.2% · ManagedBackupController.cpp 95.8% (residual: gcov brace artifacts + library-boundary error paths, documented).

Databases tested

  • sqlite3: full LightweightTest (1242 passed) + all dbtool-gui suites (204 assertions / 56 cases, qmltest, smoke).
  • mssql2022 (Docker): LightweightTest [SqlBackup] 2558 assertions / 170 cases + end-to-end dbtool backup verifying PWD=*** in archive metadata.
  • postgres: skipped — no psqlodbc driver installable in this environment (documented; the touched library code is DBMS-agnostic connection plumbing).

Update — live per-table backup "Details" panel

Adds a live master-detail view to the Backups page so a running backup/restore shows real-time per-table progress, and fixes a Windows launch blocker.

Design doc: docs/superpowers/specs/2026-07-11-backup-live-details-design.md · plan: docs/superpowers/plans/2026-07-11-backup-live-details.md.

What's new

  • BackupTableListModel — GUI-thread-confined per-table model (one per profile), keyed by table name: rows create-on-first-sight and update-in-place, tolerating the up-to-8 concurrent table workers reporting in any order. Cleared at each run start; rows persist after a run so a finished profile still shows its last per-table results.
  • Structured progress plumbingManagedBackupController gains a queued tableProgress(profile, table, current, total, state, message) signal; EmittingProgressManager now carries the profile name and emits it alongside the existing logLine. A GUI-thread slot routes each update into the right profile's table model. Restore progress routes under the archive profile too.
  • BackupDetailPanel.qml — the live pane: per-table progress bar (indeterminate until the row total is known), a state badge (queued/running/done/error/warning), and the latest message, plus an "N workers" caption surfacing the existing 8-thread table parallelism (see the later follow-up correcting the MSSQL worker claim).
  • BackupsPage.qml master-detail split — the Profiles card becomes a profile list + detail pane (side-by-side when wide, stacked when narrow). Selection auto-follows the running profile and a click pins a specific one.
  • Windows launch fixdbtool-gui.exe (and the test exes) failed to start with an SxS "requestedExecutionLevel level missing" error: lld-link's /MANIFEST:EMBED merge rewrote the manifest into the wrong XML namespace. Now a hand-written manifest is embedded verbatim via an .rc RT_MANIFEST resource. Also fixed a ::getpid portability break in the GUI test target on clang-cl.

Parallelism

No engine change — backups already run tables with jobs = min(8, hardware_concurrency). This change makes that concurrency visible in the UI.

Risk assessment

  • Threading: all table-model mutation is GUI-thread-confined; the worker only posts queued invokeMethod calls (args copied into the event, no this/manager capture). A whole-branch review traced the worker->GUI hand-off, clear-before-progress FIFO ordering, and mid-run profile-reload lifetime — no data race or use-after-free.
  • Lifetime: per-profile table models are parented to the status model and explicitly deleteLater()'d on profile reload (a leak found in review and fixed with a QPointer regression test).
  • Windows-only manifest/.rc change (no-op elsewhere); DBMS-agnostic (Progress contract is per-DBMS-neutral).

Tests

  • C++ dbtool-gui-tests: 240 assertions / 66 cases pass (was 204/56) — new per-table-model tests, the reload-leak regression test, and a controller test that runs a real SQLite backup and asserts tableProgress fires and the profile's model is populated.
  • QML dbtool-gui-qmltest: 18/18 (adds a BackupDetailPanel smoke test).
  • Windows clangcl-release; the real GUI binary launches clean (no SxS error, no QML warnings).
  • Deferred follow-up: a page-level QML test for the selection logic (pin toggle / detail-profile derivation).

Layout fix — details as a dedicated master-detail region

The first cut nested the live details inside the Profiles card's Column, where Layout.* sizing is ignored, so the list and the details overlapped. Reworked into a page-level GridLayout: a fixed-width left rail (folder + profile master-list + custom archive) and a flexible right region that gives the details their own column (stacks on a narrow window). Also fixed a latent bug where the panel keyed visibility on tablesModel.rowCount — an invokable method, so the table list would never have appeared during a run — now keyed on the ListView's reactive count. Verified in the running GUI (no overlap; correct empty state).

🤖 Generated with Claude Code

Follow-up — independent scrolling + summary/active-only details

Two more fixes from real-fleet feedback (~300 profiles, hundreds of tables):

  • Independent scroll regions. The page was one big ScrollView, so scrolling the 300-profile master-list scrolled the details away — you had to scroll back to the top to watch progress. It's now a fixed header + a fill-height master-detail body, where the profile list and the detail panel's active-table list each own their own scrollbar and scroll independently. (The Profiles master-list moved out of a content-sized Card into a fill-height bordered panel so its inner ListView can take the slack.)
  • Summary + active-only detail. The detail panel no longer renders a wall of finished tables. It shows a compact summary — "N / M tables done", an aggregate progress bar, and per-state chips (running / queued / error / warning) — then a live list of only the in-flight tables (running/error/warning). Done and queued rows are filtered out via a new BackupActiveTablesModel (QSortFilterProxyModel) that re-filters automatically as tables transition state — done in C++, not via fragile zero-height QML delegates. New StatChip.qml renders the summary chips; BackupTableListModel gains NOTIFY-backed per-state count properties feeding the summary.
  • Tests: summary tallies + summaryChanged firing (BackupTableListModelTests), and active-only filtering incl. live re-filter on state transition, roleNames pass-through, and source reset (BackupActiveTablesModelTests). Suite now 283 assertions / 76 cases; qmltest 18/18. Running-state layout verified by screenshot (summary + 6 active rows, 140 done rows hidden; both regions scroll independently).

Follow-up — profile search + corrected MSSQL worker claim

Two more fixes from a run with 325 profiles:

  • Profile search. The master-list had no way to reach a profile short of scrolling hundreds of rows. Added a search field above the list that filters by name (case-insensitive substring), with a clear (✕) affordance and a "no profiles match" placeholder. Filtering is done in C++ by a new BackupProfileFilterModel (QSortFilterProxyModel over the status model), mirroring BackupActiveTablesModel: the ListView binds to the proxy while selection / auto-follow still resolve against the unfiltered status model, so filtering never changes which profile's details are shown. Search text is whitespace-trimmed (a stray space won't hide every row) and the list caption switches to "X of Y match" while filtering.
  • Corrected a false worker-count tooltip. It claimed "MS SQL Server is limited to 1 worker by the driver" — which is wrong: SqlBackup::Backup spawns concurrency worker threads for every DBMS (iota over 0..concurrency) with no MSSQL special-casing; concurrency is only floored at max(1U, …), never capped to 1 for SQL Server, and the GUI passes BackupConcurrency() (min(hw, 8)) for all backends alike. Tooltip now reads "Tables are backed up in parallel by up to N worker thread(s)." Confirmed against a live MSSQL run — all workers active.
  • Tests: BackupProfileFilterModelTests — empty source, empty-text pass-through, case-insensitive match, no-match, clear, whitespace trim, filterTextChanged fires only on a real change, and re-filter on source reset. Suite now 301 assertions / 84 cases; qmltest 18/18. Search + parallel MSSQL execution both manually confirmed by the maintainer.

Yaraslaut and others added 21 commits July 11, 2026 16:40
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n string)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ResolveConnectionString returned profile.connectionString as-is before
ever consulting secretRef, so a profile combining both (per
ProfileStore's contract that secretRef "applies to both forms") never
got its password appended. Delegate to Profile::ToConnectInfo for both
the raw-string and DSN forms and serialize its SqlConnectInfo result;
ToConnectInfo already returns a raw string unmodified when no password
was resolved, so the existing "used as-is" behavior is preserved with
no extra branching.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implement QAbstractListModel subclass for backup status display. Provides rows per configured profile with role-based access to archive metadata (exists/size/mtime) and live run state (idle/queued/running/ok/failed). Used by ManagedBackupController to update the UI during backup operations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds ManagedBackupController: the QObject that owns the managed backup
folder setting (persisted via QSettings), computes the effective/default
folder, feeds BackupStatusListModel from ManagedBackupCore's pure
scan/plan logic, and surfaces folderProblem when the configured path is
unusable. Backup/restore invokables land in later tasks; this slice only
covers construction, the folder property, setProfiles/setBusyProbe, and
refreshStatus.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… first

CreateMetadata() and three Restore.cpp helpers (RestoreIndexes,
ApplyDatabaseConstraints, RecreateDatabaseSchema) each declared
`SqlConnection conn;` before immediately calling `conn.Connect(connectionString)`
with the real connection string. The default SqlConnection constructor
auto-connects using the process-global DefaultConnectionString() and throws
on failure — so whenever that global default was never set (any real
embedder that doesn't call SqlConnection::SetDefaultConnectionString(), e.g.
a GUI backing multiple named profiles), these helpers threw before ever
reaching the intended Connect() call, surfacing as a generic
"IM002 ... Data source name not found" error.

The existing SqlBackup/Restore test suite masked this because the Catch2
test binary always calls SetDefaultConnectionString() from --test-env,
making the accidental first connect happen to succeed against the same
database. Switch all four sites to `SqlConnection conn { std::nullopt }`
so only the explicit connectionString parameter is ever used, matching the
existing pattern already used by Backup()'s mainConn and ConnectionPool.

Verified via LightweightTest (sqlite3 in-process: 1240/1241 passed, 1
pre-existing skip; SqlBackup subset also green against mssql2022 via
Docker). No API/behavior change for callers that do set a global default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds ManagedBackupController::backupAll()/backupProfile(name): a shared
StartRunGuard() refuses (with a logged Warning/Error line) when a run is
already in progress, the busy probe vetoes, or the managed folder is
unusable, then RunBackups() sequences the SqlBackup::Backup() calls on the
existing single-thread QThreadPool, publishing per-profile status through
BackupStatusListModel via QMetaObject::invokeMethod(..., QueuedConnection)
so the model is only ever touched from the GUI thread. Each profile backs
up to a `.tmp` sibling first and only replaces the final `.zip` via
ManagedBackup::CommitArchive() on success, so a crash or failure mid-run
never corrupts a previous good archive; failures are recorded per-profile
and the run continues to the next profile. FunctionTask and
EmittingProgressManager are copied from BackupRunner.cpp into an anonymous
namespace (same worker-closure and progress-forwarding pattern).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RunRestore now takes (archiveProfile, targetProfileName, rawConnectionString,
schema): the busy/folder guard runs first so an in-progress run always beats
an unknown-target-profile diagnostic, and a looked-up target profile's secret
is resolved inside the pool-thread worker (mirroring RunBackups) instead of
synchronously in the Q_INVOKABLE restoreArchive body, so a "stdin:" secretRef
can no longer block the Qt main thread.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wire ManagedBackupController into AppController: a managedBackups
Q_PROPERTY/accessor/member, log forwarding into the unified feed, a
busy probe that defers to the migration and backup runners, and a
setProfiles call in loadProfiles so the managed-backup status model
stays in sync with the loaded profile store.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Backup/restore is graduating from experimental to a regular feature.
Removes the --enable-backup-restore CLI option, AppController's
backupRestoreEnabled property/SeedBackupRestoreEnabled/_backupRestoreEnabled,
and BackupRunner's setEnabled/_enabled gate (including the early-return
warning blocks in runBackup/runRestore). Updates the QML bindings in
BackupRestoreDialog.qml, SimpleView.qml, and ToolBar.qml that referenced
the now-removed backupRestoreEnabled property, and drops the
--enable-backup-restore arg from the GUI smoke test harness.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add BackupsPage.qml: a full-page managed-backups view (backup folder card
with Settings link + folderProblem banner, per-profile status rows with
Backup/Restore actions and a last-run summary, and the ported custom-archive
back up/restore controls). Restore is confirmed through an in-page dialog
implementing the restore-confirm mockups: a target picker over all profiles
(source preselected) plus a custom connection-string entry, with a
cross-target warning.

Wire navigation in Main.qml (showBackups flag, StackLayout index 3, toolbar
Backup/Restore now refreshes status then opens the page; openSettings routes
to the Settings page). Delete the obsolete BackupRestoreDialog.qml and update
CMake QML file list. StatusPill gains an optional `label` to decouple the
visible caption from its palette key. New tst_backups_page.qml load test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Docs: replace the experimental backup/restore mentions in the dbtool-gui
README with a "Managed backups" section, and cross-link it from
docs/dbtool.md. Update the README's test-layer table and Layout tree for
ManagedBackupCore/ManagedBackupController/BackupStatusListModel and their
test files.

clang-tidy (scoped to the three new .cpp files via `clang-tidy -p
out/build/clang-release`, since dbtool-gui-lib opts out of clang-tidy
project-wide): fixed bugprone-empty-catch (log instead of silently
swallowing in FunctionTask::run()), readability-avoid-nested-conditional-
operator (extracted LevelForState()), cppcoreguidelines-owning-memory x2
(unique_ptr::release() into QThreadPool::start()), and
performance-enum-size for Phase/Role. Left cppcoreguidelines-use-enum-class
for BackupStatusListModel::Role as an accepted match to five pre-existing
sibling Model classes using the identical unscoped-enum-role idiom (same
carve-out already granted for Qt identifier-naming).

Coverage: dbtool-gui-lib had no enable_coverage_for_target() wiring, so
`clang-coverage` builds never actually instrumented any GUI code; added
the missing call. Closed reachable gaps with new unit tests (UTF-8
multi-byte SanitizeArchiveName cases, CheckWritableFolder failure
branches, BackupStatusListModel invalid-index/role handling, and several
ManagedBackupController guard/collision/resolve-failure paths).
ManagedBackupCore.cpp now at 100% lines/functions; BackupStatusListModel.cpp
98.2%; ManagedBackupController.cpp 95.8% (up from 90.5%), with the residual
gaps being either gcov closing-brace/header-inline tooling artifacts or
genuinely hard-to-reach defensive/library-boundary paths (documented in
.superpowers/sdd/task-10-report.md).

Regression: full clang-release rebuild + ctest (all 5 labels) green against
SQLite; LightweightTest also re-run directly against the running MSSQL 2022
Docker container (1238 passed, 3 pre-existing skips, 0 failed). PostgreSQL
skipped — no ODBC driver installed on this machine (pre-existing
environment limitation, not specific to this change).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CreateMetadata wrote the raw connection string into every archive's
metadata.json. With the managed-backup path resolving a profile's
secretRef, that embedded plaintext passwords into the archive. Nothing
reads the field at restore time (verified by grep) — it is diagnostic
only — so redact PWD=/Password= values to *** before writing, at the
library level so dbtool benefits too.

Adds MetadataRedactionTests (SQLite ignores PWD, so a fake password
keeps the connect valid) asserting the plaintext is gone and the value
masked. Documents the redaction in sql-backup-format.md and the
dbtool-gui README.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Collision detection previously ran only inside backupAll over the
profiles it was handed, so a single-profile backup (one-entry plan,
never collides) and RunRestore (independent SanitizeArchiveName) could
silently overwrite another profile's archive or restore the wrong data
when two names sanitize to the same file.

Add PlanEntryFor(name) which plans over the FULL profile list and
returns the entry, then honor its collision flag in backupProfile,
RunBackups (single-profile path) and RunRestore (refuse before any
destructive work). refreshStatus already planned over all names.

Also fix the EmittingProgressManager use-after-scope in both this file
and BackupRunner: the queued lambda captured the stack-local manager's
`this` and dereferenced it on the GUI thread after Backup()/Restore()
had returned and destroyed it. Replace with a single queued invoke that
reads the target pointer now and copies the args into the event.

Adds tests for the refused-collision backup/restore paths (red-first),
keeping the earlier owner of the name working.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- ManagedBackupController.hpp: document that _pool must be declared
  first (destroyed last) so ~QThreadPool joins the worker before the
  members it dereferences are gone.
- ManagedBackupCore ScanArchive: check the error_code after file_size /
  last_write_time and report not-exists on error so a TOCTOU'd file
  never surfaces uintmax_t(-1) bytes; comment why CheckWritableFolder
  discards the probe-removal ec.
- ManagedBackupCore.hpp: correct the ResolveConnectionString doc — it
  does NOT mirror connectToProfile; it always resolves secretRef and
  fails when it cannot.
- AppController: refresh the migration model after a successful managed
  restore into the connected profile (extracted the runner's post-run
  refresh into RefreshMigrationModelsAfterRun and reused it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Yaraslaut
Yaraslaut requested a review from a team as a code owner July 11, 2026 20:09
@github-actions github-actions Bot added documentation Improvements or additions to documentation CLI command line interface tools tests labels Jul 11, 2026
Yaraslau Tamashevich and others added 5 commits July 11, 2026 22:50
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Qt's qt_add_executable finalizer embeds a com.yourcompany manifest through
lld-link's /MANIFEST:EMBED merge, which re-serializes requestedExecutionLevel
into the asm.v2 namespace (ms_asmv2:level). The Windows SxS loader only honors
that attribute in asm.v3, so it reports the level attribute as missing and
refuses to start the GUI.

Ship a hand-written manifest and embed it verbatim via an RT_MANIFEST .rc
resource (no XML re-serialization). Its presence also trips Qt's user-manifest
skip branch so Qt stops generating the broken one. Windows/MSVC-linker only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…aunch

ManagedBackupCoreTests.cpp used ::getpid() unconditionally, which clang-cl
rejects on Windows (only _getpid() from <process.h> exists there); added a
portable CurrentProcessId() shim. dbtool-gui-tests and dbtool-gui-qmltest
hit the same lld-link manifest-merge SxS bug already fixed for the
dbtool-gui executable (commit f60871f) - both targets now embed the shared
../dbtool-gui.manifest via a new dbtool-gui-tests.rc RT_MANIFEST resource
instead of letting Qt/lld-link generate and corrupt one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Yaraslau Tamashevich and others added 10 commits July 12, 2026 09:01
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
resetProfiles() left the previous rows' BackupTableListModel children
parented to the status model but unreachable; release them explicitly
before rebuilding _rows. Adds a QPointer regression test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…models

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…panel

_pillStatus mapped the "warning" state to the neutral "empty" palette, so a
table that finished with a warning looked identical to an untouched row. Map
it to the warn-coloured "pending" key. Also introduced a `hasTotal` property
on the table-row delegate to dedupe the repeated `totalRows > 0` check shared
by the meta label and progress bar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…required

The detail panel's empty-state binding depends on the model's non-reactive
rowCount(); the `_detailTablesModel = null` pass-through forces the property
change that makes the panel re-read it. Flagged by the whole-branch review as
fragile-if-removed. Comment only, no behaviour change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The live per-table details were rendered by a GridLayout nested inside the
Profiles Card's plain Column, where Layout.preferredWidth is ignored (see
Card.qml's own note) — so the profile list and the details panel both drew at
full width and overlapped, and the details were unreadable.

Restructure the Backups page into a real page-level GridLayout: a fixed-width
left rail (backup folder, profile master-list, custom archive) and a flexible
right region that gives the live details their own column. Collapses to a
single stacked column on a narrow window. Widen the centred content to 1180px
to make room for the split.

Also fix a latent binding bug in BackupDetailPanel: the empty-state and the
table ListView keyed their visibility on the model's rowCount — but rowCount
is an invokable METHOD on QAbstractListModel, so that expression is a function
object, never == 0 or > 0. The result: the empty state only showed for a null
model and the live table list would NEVER appear during a run. Key both on the
ListView's reactive count instead. The panel is now transparent (it fills a
host Card) with a header divider, a workers chip, and a proper empty state.

Verified: full build clean, dbtool-gui-tests 240/66 and dbtool-gui-qmltest
pass; the running GUI renders the two-region layout with no overlap and shows
the empty state (screenshotted).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two fixes to the managed-backups master-detail page, both from user
feedback on a run with ~300 profiles and hundreds of tables:

1. Independent scrolling. The whole page previously lived inside one
   ScrollView, so scrolling the 300-profile list scrolled the details
   region away with it — to see live progress you had to scroll back to
   the top. The page is now a fixed header + a fill-height master-detail
   body. The profile master-list is its own ListView with its own
   scrollbar, and the detail panel's active-table ListView has another;
   neither scrolls the other.

2. Summary + active-only detail. The detail panel no longer lists every
   table. It shows a compact summary — "142 / 700 tables done", an
   aggregate progress bar, and per-state chips (running / queued / error
   / warning) — then a live list of ONLY the in-flight tables (running,
   error, warning). Completed and queued tables are filtered out so the
   handful actually in progress are never buried.

Implementation:
- BackupTableListModel gains cached per-state tallies (totalCount,
  doneCount, runningCount, queuedCount, errorCount, warningCount) exposed
  as NOTIFY-backed Q_PROPERTYs, recomputed on every applyProgress/clear.
  The summary reads these instead of scanning rows in QML.
- New BackupActiveTablesModel (QSortFilterProxyModel) surfaces only
  running/error/warning rows and re-filters automatically as tables
  transition state — done in C++ rather than fragile zero-height delegate
  collapsing in QML. The detail panel owns one and points it at the
  selected profile's table model.
- New StatChip.qml — compact count badge (coloured dot + count) for the
  summary chips, keyed by state.
- BackupsPage.qml: page-level fixed-header + fill-height GridLayout; the
  Profiles master-list is a bordered fill-height panel wrapping a ListView
  (a Card is content-sized and can't host a scrolling list); the detail
  region is likewise a fill-height panel so its inner list can scroll.

Tests:
- BackupTableListModelTests: summary tallies (by state, in-place
  transitions, unknown-state bucket) + summaryChanged signal firing.
- BackupActiveTablesModelTests: active-only filtering, live re-filter on
  state transition, roleNames pass-through, source reset.

Verified: dbtool-gui-tests 283 assertions / 76 cases pass;
dbtool-gui-qmltest 18/18 pass; GUI launches clean; running-state layout
confirmed by screenshot (summary + 6 active rows, 140 done rows hidden;
both regions scroll independently).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two backups-page fixes from user feedback on a run with 325 profiles:

1. Profile search. The profile master-list had no way to jump to a
   profile short of scrolling hundreds of rows. Add a search field above
   the list that filters by name (case-insensitive substring), with a
   clear affordance and a "no matches" placeholder. Filtering is done in
   C++ by a new BackupProfileFilterModel (QSortFilterProxyModel over the
   status model), mirroring BackupActiveTablesModel — the ListView binds
   to the proxy while selection / auto-follow still resolve against the
   unfiltered status model, so filtering never changes which profile's
   details are shown. Whitespace is trimmed so a stray space does not hide
   every row, and the caption switches to "X of Y match" while filtering.

2. Correct the worker-count tooltip. It claimed "MS SQL Server is limited
   to 1 worker by the driver", which is false: SqlBackup::Backup spawns
   `concurrency` worker threads for every DBMS (SqlBackup.cpp, iota over
   0..concurrency) with no MSSQL special-casing — concurrency is only
   floored at max(1U, …), never capped to 1 for SQL Server. The GUI passes
   BackupConcurrency() (min(hw, 8)) for all backends alike. Tooltip now
   reads "Tables are backed up in parallel by up to N worker thread(s)."
   Verified against a live MSSQL run — all 8 workers active.

Tests:
- BackupProfileFilterModelTests: empty source, empty-text pass-through,
  case-insensitive substring match, no-match, clear, whitespace trim,
  filterTextChanged fires only on a real change, re-filter on source reset.

Verified: dbtool-gui-tests 301 assertions / 84 cases pass;
dbtool-gui-qmltest 18/18 pass; GUI smoke test alive; manually confirmed by
the user (search + parallel MSSQL execution both working).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLI command line interface tools documentation Improvements or additions to documentation tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Morty Proxy This is a proxified and sanitized view of the page, visit original site.