Skip to content

[WIP-Feat] Heroic Backup System and Wizard#5522

Draft
flavioislima wants to merge 21 commits intomainfrom
feat/backup_heroic
Draft

[WIP-Feat] Heroic Backup System and Wizard#5522
flavioislima wants to merge 21 commits intomainfrom
feat/backup_heroic

Conversation

@flavioislima
Copy link
Copy Markdown
Member

A pretty complete backup system for Heroic that backups Logins, Settings, Game Settings, checks for missing paths for Games and Wineprefixes, check for missing wine versions, and more. With options to override and fix missing locations.
Should also convert from Flatpak to other linux installs and vice-versa.

Not planned for this MVP: Proper Game files backup.


Use the following Checklist if you have changed something on the Backend or Frontend:

  • Tested the feature and it's working on a current and clean install.
  • Tested the main App features and they are still working on a current and clean install. (Login, Install, Play, Uninstall, Move games, etc.)
  • Created / Updated Tests (If necessary)
  • Created / Updated documentation (If necessary)

Add the skeleton for the backup import/export feature. Declares the
manifest + stage types, registers stub async IPC handlers
(exportHeroicBackup / validateHeroicBackup / applyHeroicBackup /
getRollbackSnapshot / rollbackHeroicBackup / restartHeroic), wires the
preload API, adds LogPrefix.ImportExport, and provisions the
importExportRollbackStore electron-store. Also adds the adm-zip
dependency used by the later export/apply commits.
Implement exportHeroicBackup — writes a zip archive with verbatim byte
copies of every source file (global config, per-game settings, fixes,
themes, credentials, library caches + installed manifests, sideload
library, wine downloader store) plus a manifest.json describing the
format version, heroic version, platform and counts. Only stages the
caller selects are packaged; missing files are skipped without failing
the export. Backed by adm-zip so the same zip API is available for the
later apply-path verbatim reads.

Unit-tested against a tmp-dir fixture to lock in the byte-for-byte
invariant for config.json and per-game files.
Implement validateHeroicBackup — opens the zip, reads the manifest,
cross-references the included stages against the local machine and
returns a structured ValidationReport the wizard consumes. Surfaces
format-version / platform mismatches, broken install + prefix paths
per-game, missing wine versions (split into downloadable vs. not by
consulting the local wineDownloaderInfoStore), and credential presence
with a display name parsed from each store's user file.

Game titles are read from the bundled store_cache libraries (installed
manifests don't carry titles), matching the per-game list UI in the
wizard.
Implement the write-side of the backup flow:

- applyHeroicBackup opens the archive, writes a pre-apply snapshot of the
  same stages to a persistent rollback archive (path recorded in
  importExportRollbackStore so the rollback button survives restarts),
  then applies each selected stage.
- Per-game settings, installed.json files and the sideload library are
  patched conservatively — never via Object.entries().map() — so
  sibling keys like version and explicit are preserved. Only the
  appName block or install_path/path is rewritten, and only when the
  user explicitly overrode it in the wizard (Browse / Skip /
  Install-after / Use-default-prefix).
- After writing credential files, Legendary and Nile are mirrored into
  configStore via getUserInfo/getUserData so the frontend login state
  updates without a restart. GOG and Zoom keep their own kv stores.
- Wine stage refreshes wineDownloaderInfoStore via
  updateWineVersionInfos(true) before matching and fires background
  installs for missing versions when the user keeps the default toggle.
- rollbackHeroicBackup replays the snapshot archive with
  overwriteGlobalSettings=true and clears the snapshot entry.
- restartHeroic calls app.relaunch() + app.exit(0).

Apply semantics are locked in by unit tests against the
"version"/"explicit" preservation invariant, per-runner skip/queue/patch
branches, and the rollback snapshot write.
Add a new Settings > Import/Export section and a 7-step import wizard.

Settings section:
- Collapsible Export card with per-stage toggles, "N of M included"
  summary and a date+time-stamped default filename
  (heroic-backup-YYYY-MM-DD_HH-MM-SS.zip).
- Collapsible Import card that launches the wizard and surfaces a
  Rollback button only when a persistent rollback snapshot exists.

Wizard:
- 820px dialog, 760px body, 520px min-height, horizontal stepper with
  past steps clickable and a step counter ("Step X of 7").
- Uses existing design-system tokens (--text-*, --space-*,
  --status-success/warning/danger, --accent, --modal-background,
  --input-background) — no hardcoded colors.
- Reuses existing UI primitives: ToggleSwitch, InfoIcon, StoreLogos,
  PathSelectionBox, Dialog/DialogHeader/DialogContent/DialogFooter.
- Steps: pick file -> summary + manifest + platform check ->
  global settings (with opt-in overwrite toggle defaulting off) ->
  per-game settings (titles from store_cache libraries, filter +
  select-all/deselect-all) -> credentials (always importable, shown
  with status pill only) -> library & system (collapsible details,
  per-game Browse/Keep/Skip/Download actions, prefix row also offers
  Use-default-prefix, wine versions with Download default on) ->
  Done (stage list with exclusion labels, rollback hint and a restart
  banner that calls the new restartHeroic IPC).
- Fade-in transition on each step, disabled for users with
  prefers-reduced-motion.

Registered under /settings/importexport in the sidebar submenu and
plumbed through pnpm i18n so the new keys land in en/translation.json.
Address review feedback on the import/export UI:

- InfoIcon is a hover-only tooltip component (its span is hidden by
  default CSS) — it's meant to sit next to a label, not stand in as
  visible hint text. Replace every standalone InfoIcon block in the
  Settings section and the wizard steps 1, 3, 5, 6 and 7 with a
  proper .ImportExportSettings__hint / .ImportExportWizard__hint
  paragraph styled secondary (var(--text-secondary), --text-sm,
  max-width 65ch).
- Export card now pre-fills the output folder with the user's home
  directory on mount via a new getHomeDir async IPC
  (app.getPath('home')) so the Export button is usable without first
  picking a folder.
- Summary step now shows user-facing store names ("Epic: 1, GOG: 2,
  Amazon: 1") instead of internal runner ids.
- Per-game settings step and Store logins step no longer render the
  StoreLogos SVG — the store is indicated inline as "(Epic)" /
  "(Amazon)" text next to the title and name, which is cleaner in a
  dense list.
When the library is genuinely empty (no stores logged in, no sideloaded
games), add a secondary line under the existing "log in" message that
links to Settings > Import/Export so a user on a clean install can
restore their previous state instead of starting from scratch. The hint
is visually subdued (secondary text color, separator rule) so it does
not compete with the primary login call to action.
Coming from the library's "Restore from backup" link, opening the Export
card first is counter-intuitive — the user is there to import, not
export. Pass openImport=true via react-router's NavLink state; the
Settings section reads location.state on mount, flips the cards so the
Import card is the one expanded (Export is collapsed), and auto-opens
the import wizard so the user lands on "Pick a backup file" in a
single click.
Symmetric treatment for both subsections on step 6:

- Missing Wine / Proton versions: replace the all-or-nothing master
  toggle with a per-version checkbox list and Select all / Deselect all
  buttons, matching the per-game settings pattern. Versions that aren't
  downloadable on this machine have the checkbox disabled; the status
  pill stays next to the name for context. Only the ticked versions are
  queued at apply time.
- Missing install paths: same approach as above — master toggle,
  per-game checkbox, select-all/deselect-all, no filter.

Backend contract: replace HeroicApplyOptions.downloadMissingWine:
boolean with includedWineVersions: string[]. The wine apply stage now
intersects that list with what's currently available locally and fires
only those downloads. Rollback calls pass an empty array.

Existing apply tests were updated to the new shape; all 138 backend
tests still pass.
Add an installedOK: InstalledGameSummary[] field to
HeroicBackupValidationReport, emitted by the validator for any
installed game in the backup whose install_path still exists on the
local filesystem. Rendered on step 6 as a collapsible section
(closed by default) that lists each game with a green checkmark, the
runner as a parenthetical, and the install path as secondary text.
This gives the user reassurance about what is being re-registered
as-is in addition to the already-shown lists of things that need
attention. Validator test case added to cover the split between OK
games and games with broken paths.
Break the monolithic 1341-line index.tsx into:

- steps/StepPickFile.tsx
- steps/StepSummary.tsx
- steps/StepGlobalSettings.tsx
- steps/StepPerGame.tsx
- steps/StepCredentials.tsx
- steps/StepLibrarySystem.tsx
- steps/StepDone.tsx
- steps/index.ts (barrel)
- shared.tsx — PathActionRow, StatusPill, runnerLabel(),
  stageFriendlyLabel() and the PathChoice / PathAction types used
  across multiple steps

index.tsx shrinks to 379 lines and now owns only the wizard
orchestration (state, navigation, the apply call) plus the dialog
chrome. No behavioural change; codecheck, lint, full test suite and
electron-vite build all clean.
…bar buttons

- Step 1 drops the explicit "Choose file" button and wires the
  PathSelectionBox folder icon straight to the validation handler —
  picking a file via onPathChange triggers pickBackupFile. The box is
  disabled while validation is running, and a small "Validating..."
  hint appears instead of the old in-button label.
- Fix the Back button flow: navigating from step 2 back to step 1
  preserved filePath + validation state, but the Next button was
  disabled by an extra step === 0 guard that forced the user to
  re-pick the file. Drop that condition; the !validation check alone
  already covers the real "no file yet" case.
- Shrink the Select all / Deselect all buttons on step 4 and step 6 to
  the same compact sizing as the per-row path action buttons
  (var(--text-sm), var(--space-3xs) var(--space-sm)) so the toolbar
  no longer looks heavier than the list rows beneath it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant