This document is the AI roadmap for the DBack Go desktop application (dback module). Use it before exploring the whole repository. For the WordPress plugin REST agent, see wordpress/dback-db-tools/wordpress_agent.md.
Agent rule — version on every change: After any code or build change to the Go app, you must bump the app version in main.go and build.sh, update Current app version in Versioning below, and tell the user which git tag to push for release (see Build and embed). Never finish a change set without syncing version + tag instructions.
DBack is a Gio desktop app (not a web server) for MySQL/MariaDB backup, restore, and SQL queries against remote hosts. Data (profiles, templates, history, logs, sync settings) lives in a local encrypted vault. Backup files (.sql.gz) are stored on disk under each host’s destination folder.
Stack: Go 1.22 · Gio UI · SSH/shell transport · WordPress REST plugin transport · MinIO S3 sync · Argon2id + AES-GCM vault.
Supported desktop targets: Linux and Windows (primary release platforms). All new and changed Go/UI code must work correctly on both — not Linux-only.
dback/
├── main.go # Entry: embed logo, ui.New().Run()
├── agent.md # This file (Go app roadmap)
├── models/models.go # Domain types: Profile, bundles, vault payload
├── ui/ # Gio UI (screens, widgets, theme, state)
├── internal/
│ ├── app/ # Business orchestration (Backup, Restore, sync, vault API)
│ ├── store/ # Persistence, vault, import/export bundles
│ ├── sync/s3.go # S3-compatible push/pull
│ └── secrets/ # Argon2id + AES-GCM
├── backend/
│ ├── ssh/ # SSH, JumpHost, Localhost executor
│ ├── db/ # Shell command builders, validation, query parsing
│ ├── transfer/ # Backup/restore strategies
│ ├── verify/ # SHA256 quick check, fingerprint capture, deep-verify report
│ ├── preflight/ # Remote preflight (SSH path)
│ └── wordpress/ # REST client, plugin zip generation
└── wordpress/dback-db-tools/ # Embedded PHP plugin (see wordpress_agent.md)
Runtime data directory: ~/.config/dback (via ui.DesktopPlatform.AppDataDir()).
| File | Role |
|---|---|
app_data.vault.json |
Encrypted vault (profiles, templates, history, logs, sync) |
ssh_known_hosts |
SSH host key store |
{Destination}/{HostName}/*.sql.gz |
Backup files (not in vault) |
flowchart TB
subgraph ui [ui/]
Hosts[Hosts screen]
Backups[Backups screen]
Profile[Profile editor]
Settings[Settings + Sync]
end
subgraph app [internal/app]
BackupFn[App.Backup]
RestoreFn[App.Restore]
QueryFn[App.RunImportQuery]
SyncFn[App.SyncPush / SyncPull]
end
subgraph transport [Transport layer]
SSH[backend/ssh Executor]
Transfer[backend/transfer]
WP[backend/wordpress Client]
end
subgraph remote [Remote]
Shell[mysqldump / mysql CLI]
Plugin[WP plugin dback/v1 REST]
end
ui --> app
BackupFn --> Transfer
RestoreFn --> Transfer
QueryFn --> SSH
QueryFn --> WP
Transfer --> SSH
Transfer --> WP
SSH --> Shell
WP --> Plugin
Layering rule: UI never calls backend/* directly. Always go through internal/app.App.
Defined in models/models.go:
ConnectionTypeSSH = "SSH"
ConnectionTypeJumpHost = "JumpHost"
ConnectionTypeLocalhost = "Localhost"
ConnectionTypeWordPress = "WordPress"| Type | Transport | Backup | Import | Query | Preflight |
|---|---|---|---|---|---|
| SSH | ssh.Client → shell |
transfer.BackupSSH |
transfer.RestoreSSH |
db.BuildQueryCommand + executor |
preflight.Run (remote bash) |
| Jump Host | SSH via bastion (newJumpClient) |
Same; tmp-file strategy first | Same | Same | Same |
| Localhost | LocalClient → local/WSL bash |
Same | Same | Same | Same |
| WordPress | HTTP REST dback/v1 |
transfer.BackupWordPress |
transfer.RestoreWordPress |
wordpress.Client.Query |
GET /preflight |
Branching in app layer (internal/app/app.go):
if profile.UsesWordPress() {
// wordpress path
} else {
// ssh.NewExecutor + transfer.BackupSSH / RestoreSSH
}Helpers on models.Profile:
| Method | Meaning |
|---|---|
UsesSSH() |
SSH or JumpHost |
UsesWordPress() |
WordPress REST |
IsLocalhost() |
Local shell |
SupportsSQLQuery() |
MySQL/MariaDB, or WordPress |
AllowsImport() |
!ImportProtected |
| Field | Purpose |
|---|---|
ID, Name, Group |
Identity |
ConnectionType |
SSH / JumpHost / Localhost / WordPress |
TargetDBName |
DB name for filenames, query placeholders, WordPress import target |
Destination |
Local folder for backup files |
PreImportQuery, RunQueryBeforeImport |
SQL before restore |
PostImportQuery, RunQueryAfterImport |
SQL after restore |
ImportProtected |
Block restore to this host |
DBType |
MySQL or MariaDB (WordPress defaults to MySQL in UI) |
| Field | Notes |
|---|---|
Host, Port |
SSH target (hidden for Localhost) |
SSHUser, SSHPassword, AuthType, AuthKeyPath, AuthKeyPEM |
SSH auth |
JumpHost, JumpPort, JumpUser, JumpPassword, JumpAuthType, JumpAuthKeyPath, JumpAuthKeyPEM |
Jump Host only |
DBHost, DBPort, DBUser, DBPassword |
Remote DB (unless Docker) |
IsDocker, ContainerID |
docker exec path |
| Field | Notes |
|---|---|
WPUrl |
Site URL (https://example.com) |
WPKey |
API key → header X-DBACK-KEY |
Host |
Synced from WPUrl on save |
| DB credential fields | Not used — plugin uses wp-config |
TargetDBName |
Empty → WordPress default DB (DB_NAME); set → X-DBACK-DATABASE on import/query |
Legacy (migration only, never written on save): ExportSettings, ImportSettings — flattened by store.flattenProfile.
| Layer | Symbol | File |
|---|---|---|
| UI | UI.runBackup |
ui/hosts.go |
| App | App.Backup |
internal/app/app.go |
App.Backup
→ transfer.BackupSSH
→ db.ValidateProfileForRemoteOps
→ ssh.NewExecutor(profile)
→ preflight.Run(client, profile, 0, operationID)
→ estimateBackupTotal (optional progress)
→ mkdir {Destination}/{safeName(Name)}/
→ file: {TargetDBName}_{DD_MM_YYYY_HH_MM_SS}.sql.gz
→ strategies: streaming → tmp-file (JumpHost: tmp-file first)
→ validateBackupIntegrity, checksum
→ ExportRecord → vault history
Strategies (backend/transfer/transfer.go):
| Strategy | Behavior |
|---|---|
StrategyStreaming |
RunCommandStream(BuildExportCommand) → write local file |
StrategyTmpFile |
Remote dump to tmp → download with resume (.meta metadata) |
Preflight (backend/preflight/): OS, dump tools, gzip/zstd, disk space, Docker status, writable tmp dirs.
App.Backup → transfer.BackupWordPress
→ client.Preflight (GET /preflight)
→ client.Export (GET /export) → stream gzip to local file
No tmp-file fallback. Plugin details: wordpress_agent.md.
- Minimum backup size: 128 bytes (app + transfer).
- Jump Host: prefer tmp-file — double SSH tunnel streaming is unreliable for large dumps.
Two-layer verification compares each backup against a fingerprint captured at backup time — never against the live source database.
flowchart TB
subgraph backupTime [At backup time]
File[".sql.gz on disk"]
SHA[SHA256 checksum]
FP["table row counts from source DB"]
Vault[ExportRecord in vault]
File --> SHA --> Vault
File --> FP --> Vault
end
subgraph quick [Quick verify — automatic]
QCheck["QuickCheck: SHA256 vs stored checksum"]
QCheck --> QuickVerified
end
subgraph deep [Deep verify — optional]
TempDB["temp DB dback_verify_*"]
Restore["RestoreSSH / RestoreWordPress to temp DB"]
Count["COUNT(*) per table"]
Report["BuildTableReport vs fingerprint"]
TempDB --> Restore --> Count --> Report
end
Vault --> QCheck
Vault --> Restore
After a successful backup in App.Backup (internal/app/app.go):
verify.ChecksumFile→ExportRecord.Sha256verify.CaptureFingerprint(defaultModeFast) →ExportRecord.FingerprintapplyAutoQuickVerify→ExportRecord.QuickVerified
Fingerprint query (fast mode): information_schema.tables — table_name, table_rows for the backup schema.
| Transport | Schema in query |
|---|---|
| SSH / Jump / Localhost | table_schema = '{TargetDBName}' |
WordPress, empty TargetDBName |
table_schema = DATABASE() |
| WordPress, named DB | table_schema = '{TargetDBName}' |
Fingerprint capture uses App.RunImportQuery via appQueryRunner (internal/app/verify.go).
| Symbol | Location |
|---|---|
verify.QuickCheck |
backend/verify/quick.go |
App.QuickVerify |
internal/app/verify.go |
App.BackupVerifyStatus |
display helper for list/detail |
Recomputes SHA256 of the local .sql.gz and compares to ExportRecord.Sha256. No database connection. Runs automatically after every new backup; can be re-run via App.QuickVerify.
| Symbol | Location |
|---|---|
App.DeepVerify |
internal/app/verify.go |
verify.CountTablesExact, verify.BuildTableReport |
backend/verify/fingerprint.go, report.go |
verify.TempDBName |
dback_verify_{unix} |
Prerequisites: Fingerprint present, quick check passes, destination AllowsImport() (!ImportProtected).
Flow:
App.DeepVerify
→ verify.QuickCheck (abort if SHA256 mismatch)
→ tempDB = verify.TempDBName()
→ WordPress only: prepareVerifyDatabase (DROP+CREATE via RunImportQuery, connectDB=false)
→ transfer.RestoreSSH or RestoreWordPress with TargetDBOverride=tempDB
→ verify.CountTablesExact on temp DB (exact COUNT(*) per table)
→ verify.BuildTableReport vs record.Fingerprint
→ persist ExportRecord.DeepVerified
→ dropVerifyDatabase (defer cleanup on failure)
SSH / Jump / Localhost: RestoreRequest.TargetDBOverride switches restore to a temp database only — restoreProfile overrides TargetDBName; uses BuildImportPrepareTempCommand and BuildImportStreamCommandForVerify so production DB is not dropped.
WordPress: prepareVerifyDatabase creates temp DB without X-DBACK-DATABASE; restore/import uses X-DBACK-DATABASE: tempDB.
Result semantics (App.BackupDeepVerifyStatus):
| State | Meaning |
|---|---|
matched |
SHA256 OK and all table row counts match fingerprint |
row_diff |
SHA256 OK but row counts differ (fast fingerprint is approximate; file may still be valid) |
none |
Deep verify not run |
Deep verify failure with mismatches returns error from App.DeepVerify but still persists DeepVerified with Passed: false and per-table Report.
| Field | Role |
|---|---|
Sha256 |
SHA256 of backup file at creation |
Fingerprint |
BackupFingerprint — Mode, Tables, TotalRows, CapturedAt |
QuickVerified |
Last quick (SHA256) verify result |
DeepVerified |
Last deep verify result + Report |
LastVerified |
Legacy; prefer QuickVerified / DeepVerified |
AppVaultPayload.ImportDestByProfile maps source host profile ID → last chosen destination profile ID for import and deep verify.
| API | File |
|---|---|
Store.ImportDestForProfile, SetImportDestForProfile |
internal/store/store.go |
App.ImportDestForProfile, SetImportDestForProfile |
internal/app/import_prefs.go |
db.parseTSVBlock accepts single-column MySQL CLI output without tabs (e.g. COUNT(*)\n0) — required for exact row counts during deep verify.
- Fast fingerprint uses
information_schema.table_rows(approximate on InnoDB). Deep verify compares restored data against that snapshot, not a live re-read of production. - Old backups without
Fingerprintcannot deep verify — user must create a new backup. - Deep verify cost: full restore to a writable host (CPU, disk, time); temp DB name must match
dback_verify_*prefix (safety check). - Quick verify requires the backup file on local disk (
backup file not foundif only metadata synced).
| Area | Path |
|---|---|
| Checksum / quick check | backend/verify/quick_test.go |
| Fingerprint parse / report | backend/verify/fingerprint_test.go, report_test.go |
| Import dest prefs | internal/store/vault_test.go — TestVaultPersistsImportDestByProfile |
| Layer | Symbol | File |
|---|---|---|
| UI | UI.runRestore |
ui/backups.go |
| App | App.Restore |
internal/app/app.go |
1. Check destination.AllowsImport() (ImportProtected)
2. Pre-import query (optional) → RunImportQuery, connectDB=false
3. transfer.RestoreSSH or RestoreWordPress
4. Post-import query (optional) → RunImportQuery, connectDB=true
Important: Pre-import query failure aborts restore before any import upload starts. Pre-import runs when PreImportQuery is non-empty (WordPress and SSH). Post-import query failure returns an error after the database import completes.
WordPress before-import: DBack sends no X-DBACK-DATABASE header so DROP/CREATE DATABASE can run against the server default connection. After-import and manual queries with connectDB use TargetDBName when set.
transfer.RestoreSSH
→ preflight.Run(client, profile, fileSize, operationID)
→ detectCompression (gzip / zstd magic)
→ BuildImportPrepareCommand: DROP + CREATE DATABASE (when applicable)
→ strategies: streaming (pipe stdin) → tmp-file upload + import from file
Key builders: backend/db/commands.go — BuildImportStreamCommand, BuildImportFromFileCommand, BuildImportPrepareCommand.
transfer.RestoreWordPress
→ client.Preflight
→ client.Import(body, db.WordPressImportDatabase(profile))
Import body from $request->get_body() (not php://input). Target DB via X-DBACK-DATABASE.
| Context | Call chain |
|---|---|
| Host editor → Queries tab | ui/query.go → App.RunImportQuery |
| Pre/post import | App.Restore |
| Connection test (DB step) | App.TestDatabaseConnection |
WordPress:
wordpress.NewClient(profile)
→ client.Query(ctx, sql, db.WordPressImportDatabase(profile))
→ POST /query + optional X-DBACK-DATABASE
→ JSON → db.QueryResult
SSH / Jump / Localhost:
db.BuildQueryCommand(profile, query, connectDB) // base64 pipe, anti-injection
→ ssh.NewExecutor → RunCommand
→ db.ParseMySQLBatchOutput
Placeholders (models.SubstituteQuery): {databasename}, {host}, {profile}, {dbuser}.
UI limits: 100 rows, 20 columns (ui/query.go).
Two-step timeline (ui/host_connection.go):
| Step | SSH/Jump/Local | WordPress |
|---|---|---|
| Server | echo dback-server-ok via executor |
GET /ping |
| Database | SELECT 1 via mysql CLI |
SELECT 1 via REST query |
Labels: serverConnectionStepLabel() — Local shell / WordPress REST API / Server connection.
| Item | Location |
|---|---|
| S3 client | internal/sync/s3.go |
| App API | internal/app/sync.go — SyncPush, SyncDownload, PreviewSyncImport, ImportAppDataFromBundle |
| UI | ui/settings_sync.go |
| Model | models.SyncSettings, models.SyncActivity |
Remote object: {bucket}/dback/app-data.json (sync.ObjectKey).
SyncSettings fields: Endpoint, Region, Bucket, AccessKeyID, SecretKey, UseSSL.
App.SyncPush
→ store.MarshalAppDataBundleForSync (encrypt with vault master key)
→ sync.Push → PutObject
→ store.RecordSyncPush (local LastPushAt only)
App.SyncDownload → sync.Pull
→ PreviewSyncImport / ImportAppDataFromBundle
→ merge profiles/templates + Reload
→ RecordSyncPull
Included in sync bundle: profiles, templates, history metadata, logs, sync credentials.
Excluded: backup .sql.gz files, SyncActivity timestamps.
Encryption: Sync uses vault master key (unlock passphrase). Local file export uses a separate export password (App.ExportAppData).
| Concern | File / symbol |
|---|---|
| Vault file | {baseDir}/app_data.vault.json — store.VaultPath() |
| Payload | models.AppVaultPayload — profiles, templates, history, logs, sync |
| Crypto | internal/secrets/ — Argon2id + AES-256-GCM |
| Lifecycle | CreateVault, Unlock, Lock, Reload — internal/app/app.go |
| Legacy migration | Plaintext JSON files → vault on first unlock |
| Secret stripping | store.stripSecrets — passwords, keys, WPKey on export without secrets |
All store writes require unlocked vault (ErrVaultLocked otherwise).
| Item | Location |
|---|---|
| Plugin source | wordpress/dback-db-tools/ |
| Embed | wordpress/dback-db-tools/embed.go — //go:embed |
| Zip build | backend/wordpress/pluginzip.go — BuildPluginZip(siteURL, apiKey) |
| App facade | internal/app/pluginzip.go — BuildWordPressPluginZip |
| UI | Host form → Generate Token, Download Plugin — ui/settings_form.go |
Output filename: dback-{hostname}-{pluginVersion}.zip. Token hardcoded in DBACK_HARDCODED_API_KEY placeholder.
REST contract: namespace dback/v1, auth X-DBACK-KEY, optional X-DBACK-DATABASE. Full API: wordpress_agent.md.
-
Model —
models/models.go- Add
ConnectionTypeconstant - Add
Profilefields if needed - Add
UsesX(), updateSupportsSQLQuery()if applicable
- Add
-
Validation —
backend/db/validate*.goValidateProfileForX, wire intoValidateProfileForOps
-
Transport
- Shell-based: extend
ssh.NewExecutoror reuse it - HTTP/API-based: new package like
backend/wordpresswith a client
- Shell-based: extend
-
Transfer —
backend/transfer/BackupX,RestoreXusing sameBackupRequest/RestoreRequesttypes
-
App orchestration —
internal/app/app.go- Branch in:
Backup,Restore,RunImportQuery,TestServerConnection,TestDatabaseConnection
- Branch in:
-
Preflight
- SSH: extend
db.BuildPreflightScript+preflight.Run - REST: new endpoint + client parser (WordPress pattern)
- SSH: extend
-
Store —
internal/store/store.gonormalizeProfiledefaults; do not filter out new type inflattenProfiles
-
UI —
ui/settings_form.go- Add to
connTypeValues - Conditional fields in
SettingsForm.layout - Update
hostConnectionSubtitleinui/helpers.go - Update
serverConnectionStepLabelinui/host_connection.go
- Add to
-
Tests — model helpers, store, client
httptest, app integration
Pattern to copy for HTTP hosts: WordPress (backend/wordpress/ + plugin embed).
Pattern to copy for shell hosts: Localhost (special case in ssh.NewExecutor).
| Token | Default | Use |
|---|---|---|
Padding |
24dp | Main content inset (layoutContent) |
CardPadding |
20dp | Inside card() |
Gap |
16dp | Vertical spacing — vgap(theme) |
SectionGap |
28dp | Between major sections |
Radius |
12dp | Cards |
RadiusSm |
8dp | Inputs, small buttons |
Colors: GitHub dark palette — Bg, Surface, Accent (green), Link (blue), Danger, Text, TextMuted.
| Pattern | Where | How |
|---|---|---|
| Desktop shell | layoutDesktop |
248dp sidebar + Flexed(1) content |
| Content padding | layoutContent |
Inset with theme.Padding on all sides |
| Full-width content | Inside content | gtx.Constraints.Min.X = gtx.Constraints.Max.X |
| Centered modal/card | Login, dialogs | layout.Center.Layout + max width (e.g. 440dp) |
| Right-aligned actions | Profile footer | Flexed(1) empty spacer + buttons |
| Horizontal button groups | Forms | layout.Flex{Axis: Horizontal} + hgap(theme) |
| Scrollable forms | Profile editor | scrollArea + widget.List |
vgap(theme) // vertical Gap (16dp)
hgap(theme) // horizontal Gap (16dp)
spacer(theme, unit.Dp(n)) // fixed vertical spaceLabel → field gap: 6dp inset in labeledField.
| Component | Function | Notes |
|---|---|---|
| Card | card(gtx, theme, widget) |
Surface + 1dp border, CardPadding |
| Primary button | primaryButton(gtx, th, theme, &btn, label, onClick) |
Green accent |
| Secondary button | secondaryButton(...) |
Outlined |
| Danger button | dangerButton(...) |
Delete actions |
| Tab | tabButton(...) |
Profile Connection / Queries |
| Text input | editorField inside borderedInput |
Focus ring = Link color |
| Password | passwordField |
Toggle visibility |
| Enum / dropdown | enumField |
Connection type, DB type |
| Checkbox | checkboxField |
Import protected, Docker |
| Muted help | mutedLabel |
Secondary instructions |
| Section title | sectionTitle (H4) / material.Subtitle1 in cards |
|
| Page header | pageHeader(gtx, th, theme, title, action) |
Title left, action right |
| Dialog | ui/dialog.go |
Overlay stack on main layout |
| Loading | DialogLoading |
Long operations |
- Long ops run in goroutines (
ui/jobs.go,hosts.go,backups.go). - Update UI via
u.invalidate()after state changes. - Use
operationJobfor cancel + progress. - Never block the Gio event loop.
ui/platform.go—Platforminterface (AppDataDir,OpenFolder,IsMobile); Windows usesexplorer/start, Linux usesxdg-open/explorerfallbacks.- File picks:
ui/explorer.go—pickOpenFile,pickSaveFile,pickSaveBytes,pickFolder(Gio explorer on both desktop OSes). - See Cross-platform requirement — mandatory for all file I/O and download flows.
Do:
- Reuse
AppThemetokens for all spacing — no magic numbers unless one-off (e.g. login 20dp spacer). - Wrap related fields in
card(). - Hide irrelevant fields by connection type (see
SettingsForm.layoutpattern withisWordPress,isLocal,isJump). - Call
u.core.*app methods from UI — never importbackend/*in UI for operations. - Use
secondaryButtonfor secondary actions,primaryButtonfor main CTA.
Don't:
- Add raw
material.Buttonwithout theme colors. - Hardcode colors outside
AppTheme. - Run SSH/HTTP on the UI thread.
- Duplicate vault or transfer logic in UI.
DBack ships on Linux and Windows. Every feature you add or change must work correctly on both platforms before merge. Treat Linux-only or Windows-only shortcuts as bugs unless explicitly documented and approved.
| Layer | Must be cross-platform |
|---|---|
Go app (internal/, backend/, models/) |
Yes — paths, filenames, I/O, zip, crypto |
Gio UI (ui/) |
Yes — file dialogs, folders, errors, async saves |
| WordPress plugin zip download | Yes — Windows save dialog + filename rules |
| Remote SSH targets | Linux servers only (expected); the desktop app still runs on Win/Linux |
- Use
filepath.Join,filepath.ToSlash,os.UserHomeDir,os.UserConfigDirfor OS paths — never hardcode/for local disk paths. - Use
path.Joinonly inside zip archive entry names (always forward slashes). - Do not assume
~/.config/dbackliterally — useui.DesktopPlatform.AppDataDir()/ store paths. 0644/0755permissions onos.WriteFile/MkdirAllare fine (Windows ignores Unix mode bits).- Avoid shelling out without
runtime.GOOSbranching — seebackend/ssh/local.go(Windows → WSL when needed).
- Any user-facing download name (plugin zip, exports) must pass through sanitizers — see
sanitizeDownloadFilename/sanitizeFilenameinbackend/wordpress/pluginzip.go. - Strip or replace Windows-forbidden characters:
\ / : * ? " < > | - Avoid Windows reserved basenames:
CON,PRN,AUX,NUL,COM1–COM9,LPT1–LPT9 - Trim trailing
.and spaces; cap length (~200 chars for suggested save names). - Plugin zip internal folder name must match the zip basename (without
.zip).
- Use
gioui.org/x/explorer—CreateFile/ChooseFile— not hand-rolled paths per OS except where already wrapped (chooseFolderDialog). pickSaveBytespattern: write withwc.Write(data)thenwc.Close(); surface errors viashowError; treatexplorer.ErrUserDeclineas cancel (no error toast).- Never fail silently when save/write fails — user must see
showError. - Folder open:
ui/platform.go—exploreron Linux,starton Windows.
- Remote backup commands assume bash on Linux servers — unchanged.
- Localhost on Windows uses WSL when available (
local.go) — do not assume/bin/bashon the desktop OS. - Do not use Unix-only tools in code paths that run on the Windows desktop without a
GOOSguard.
- REST client is OS-agnostic; keep JSON, streaming, and zip building in memory (
bytes.Buffer+archive/zip). - Hardcoded plugin token uses base64url — safe in PHP single-quoted strings.
Before finishing a task that touches paths, files, dialogs, or zip:
- Run
go test ./...(CI runs on Linux; catches logic regressions). - Mentally verify Windows paths: save dialog, filename with dots, cancel vs error.
- For plugin zip: assert all entries live under stable
dback-db-tools/root (TestBuildPluginZipUsesStableRootFolder). - For explorer changes: verify both code paths —
*os.Filefrom Gio and error handling.
If you cannot run Windows locally, document assumptions and add unit tests for sanitizers and pure logic.
- Branch with
runtime.GOOS == "windows"when OS behavior differs. - Return clear errors to the UI — never swallow I/O failures in goroutines.
- Keep business logic free of OS tests when possible (sanitize in
backend/, call from UI). - Match existing patterns in
explorer.go,platform.go,pluginzip.go.
- Don't hardcode
/tmp,/home, or~in desktop code. - Don't use
path.Joinfor local filesystem paths. - Don't assume Gio
CreateFilealways succeeds without handling write/close errors. - Don't ship download filenames with
:or\from raw URLs. - Don't add
zenity/kdialogrequirements for core features — Windows has no equivalent; use Gio explorer first. - Don't merge file-save or plugin-zip changes without filename safety tests.
| Item | Location |
|---|---|
| Logo | main.go — //go:embed logo.png |
| Version | main.go default + build.sh APP_VERSION; must match release git tag |
Linux .deb |
packaging/nfpm.yaml + build.sh → dist/dback_${VERSION}_amd64.deb |
| Launchpad PPA | debian/ + packaging/build-ppa.sh; CI matrix (noble + jammy): .github/workflows/ppa.yml; see ppa.md |
| Desktop icon (deb) | packaging/dback.desktop + packaging/icons/hicolor/*/apps/dback.png |
| In-app updater | internal/update/ + internal/app/update.go; About → Check for updates |
| Plugin embed | wordpress/dback-db-tools/embed.go |
| Linux build | build.sh — CGO + Gio/EGL + nfpm deb |
| Windows build | GitHub Actions + go build — see README.md |
| CI release | .github/workflows/go.yml — Go 1.22, GOTOOLCHAIN=local; runs on tag push v* |
Go toolchain: go.mod declares go 1.22. CI and Launchpad use GOTOOLCHAIN=local (no auto-download). PPA builds vendor deps at package time (vendor/ is gitignored). Launchpad jammy may install golang-1.22-go without /usr/bin/go, so debian/rules must export /usr/lib/go-1.22/bin in PATH; debian/prepare-go.sh verifies and logs the actual go binary. Do not run go mod tidy with a newer local Go without verifying CI/PPA still pass.
Current app version in repo: 3.8.3 → About screen and local ./build.sh use this until you bump again.
./build.sh linux
# or explicitly:
APP_VERSION=3.8.3 ./build.sh linuxOutputs: dist/dback-linux, dist/dback, dist/dback_3.8.3_amd64.deb.
build.sh prints the release git tag to push when the build succeeds.
Tag must match main.go / build.sh version (3.8.3 → tag v3.8.3). CI strips the v and embeds the version in binaries and the .deb name.
git tag v3.8.3
git push origin v3.8.3GitHub Actions then publishes:
| Asset | Name |
|---|---|
| Linux binary | dback-linux |
| Windows binary | dback-windows.exe |
| Debian package | dback_3.8.3_amd64.deb |
PPA: ppa.yml uploads two source packages per tag — PPA_DIST=noble and PPA_DIST=jammy — via packaging/sync-debian-changelog.sh. See ppa.md.
When you bump the app version, update this table’s examples and the tag commands to the new v{x.y.z}.
Cross-platform: all embedded assets and zip/download logic must work on both targets — see Cross-platform requirement above.
| Concern | Primary symbols | File |
|---|---|---|
| Backup | App.Backup, transfer.BackupSSH, transfer.BackupWordPress |
internal/app/app.go, backend/transfer/ |
| Verify | App.QuickVerify, App.DeepVerify, verify.QuickCheck, verify.CaptureFingerprint |
internal/app/verify.go, backend/verify/ |
| Restore | App.Restore, transfer.RestoreSSH, transfer.RestoreWordPress |
same |
| Query | App.RunImportQuery, db.BuildQueryCommand, wordpress.Client.Query |
internal/app/app.go, backend/db/, backend/wordpress/ |
| Executor | ssh.NewExecutor, ssh.NewClient, LocalClient |
backend/ssh/executor.go |
| Commands | BuildExportCommand, BuildImportStreamCommand, BuildPreflightScript |
backend/db/commands.go |
| Vault | Store.Unlock, Store.SaveProfiles |
internal/store/ |
| Sync | App.SyncPush, sync.Push |
internal/app/sync.go, internal/sync/s3.go |
| UI shell | UI.layout, Section, View |
ui/app.go, ui/state.go |
| About / updates | layoutAbout, runAboutUpdateCheck |
ui/about.go, ui/about_update.go |
| Profile model | models.Profile, ConnectionType |
models/models.go |
- Branch WordPress vs non-WordPress at
internal/appusingprofile.UsesWordPress(). - Use
ssh.NewExecutor(profile)as the single shell entry for SSH/Jump/Localhost. - Run preflight before backup/restore on SSH path.
- Mask commands in logs:
db.MaskCommand. - Substitute query placeholders before execution.
- Respect
context.Context— close SSH on cancel. - Clear legacy
ExportSettings/ImportSettingson save. - Validate minimum backup size (128 bytes).
- Capture SHA256 + fingerprint after every successful backup (
captureBackupMetadata). - Use
TargetDBOverrideonly for deep verify temp DB restores — never point it at production. - Use tmp-file-first for Jump Host backups.
- Keep vault locked checks on all store access.
- Pass
X-DBACK-DATABASEfor WordPress import and query whenTargetDBNameis set. - Sanitize download filenames and handle save dialog errors on Linux and Windows.
- Run
go test ./...; add tests for path/filename/zip logic when touching I/O. - Bump app version in
main.goandbuild.shon every change set (see Versioning).
- Don't call
ssh.NewClientdirectly from app code — useNewExecutor. - Don't write
ExportSettings/ImportSettingson save (migration only). - Don't continue restore if pre-import query fails — abort and show error.
- Don't ship Linux-only desktop code without
GOOShandling or documented exception. - Don't swallow file save errors in
pickSaveBytes/ export flows. - Don't include backup files in app bundles or S3 sync.
- Don't include SyncActivity in remote sync payload.
- Don't skip
ImportProtectedcheck before restore. - Don't interpolate SQL into shell — use base64 pipe in
BuildQueryCommand. - Don't assume streaming works through double SSH tunnel (Jump Host).
- Don't add connection-type logic only in UI — mirror in app + transfer/backend.
- Don't read
php://inputin WordPress plugin for REST import — use$request->get_body(). - Don't trust
wpdb->select()return value on modern WordPress — it returnsnullon success; verify with$wpdb->readyandSELECT DATABASE().
go test ./...
# CI uses Go 1.22 with GOTOOLCHAIN=local — match locally when debugging CI failures:
GOTOOLCHAIN=go1.22.12 go test ./...Key test locations:
| Area | Path |
|---|---|
| WordPress client | backend/wordpress/client_test.go |
| App updater | internal/update/*_test.go |
| Transfer / validate | backend/transfer/*_test.go |
| Dry-Run Verify | backend/verify/*_test.go |
| Store / vault | internal/store/store_test.go |
| UI helpers | ui/helpers_test.go, ui/filters_test.go |
| Document | Scope |
|---|---|
wordpress/dback-db-tools/wordpress_agent.md |
PHP plugin REST API, export/import/query, auth, constraints |
README.md |
User-facing features and setup |
Current app version: 3.8.3
The WordPress plugin has its own version in wordpress/dback-db-tools/ (DBACK_DB_TOOLS_VERSION) — see wordpress_agent.md. Do not confuse the two.
When you modify Go app code, UI, build/CI, updater, or user-visible behavior:
- Bump the app version (patch by default:
3.8.0→3.8.1; minor:3.7.x→3.8.0): - Update examples in
README.mdif they show a pinned version. - Update Current app version here and in Build and embed.
- Tell the user the release git tag to push:
v{same version}(e.g.v3.8.3for app version3.8.3).
git tag v3.8.3
git push origin v3.8.3CI reads the tag (v3.8.3 → APP_VERSION=3.8.3); tag and main.go/build.sh must always match. PPA changelog per Ubuntu series is synced in CI — do not commit jammy/noble-specific changelog entries unless doing a manual PPA upload.
-
main.goappVersionbumped -
build.shdefaultAPP_VERSIONbumped -
agent.mdCurrent app version + Build section tag examples updated - User told:
git tag vX.Y.Zandgit push origin vX.Y.Z
| Change type | Example bump |
|---|---|
| Bug fix, CI/packaging, docs tied to app behavior | 3.8.0 → 3.8.1 |
| New user-facing feature (backward compatible) | 3.7.x → 3.8.0 |
| Breaking profile/vault/sync contract | 3.x → 4.0.0 |
- Leave
main.go/build.shdefaults on an old version after shipping a newer tag. - Commit
dist/binaries or.debfiles (see.gitignore). - Bump only the Git tag without updating
main.goandbuild.shdefaults.
When this doc and code diverge, trust the code and update this file. Last aligned with v3.8.3 — Go 1.22 toolchain, jammy/noble PPA matrix, offline vendor builds, in-app updater, Dry-Run Verify (SHA256 + fingerprint + deep verify), and mandatory app version bumps on changes.