Skip to content

feat: multi-profile config storage (desktop + Android)#1057

Open
dazzling-no-more wants to merge 1 commit into
therealaleph:mainfrom
dazzling-no-more:feature/profile
Open

feat: multi-profile config storage (desktop + Android)#1057
dazzling-no-more wants to merge 1 commit into
therealaleph:mainfrom
dazzling-no-more:feature/profile

Conversation

@dazzling-no-more
Copy link
Copy Markdown
Contributor

Summary

Adds a multi-profile system so users can keep several complete config snapshots side by side (e.g. one Apps Script setup + one Full tunnel setup) and switch between them without re-typing deployment IDs, auth keys, or tuning knobs. Available on both the desktop egui UI and the Android Compose UI; the on-disk format is identical so a profiles.json from desktop is bit-for-bit importable into Android (and vice versa).

The runtime is unchanged — the proxy server, tunnel client, and MITM still read a single config.json. Profiles live in a separate
profiles.json next to it, and switching a profile rewrites config.json to the chosen snapshot before reloading the form.

Originally requested in a community feature ask: #1044

What's in it

Storage layer (src/profiles.rs,
ProfileStore.kt)

  • New profiles.json with an active pointer and a list of
    {name, config} snapshots. Atomic temp-file + rename writes on
    every mutation; no pre-delete window where the previous file could
    be lost to a failed rename.
  • Identical on-disk schema on Rust and Kotlin sides.

Desktop UI (src/bin/ui.rs)

  • Profile bar above the Mode section: selector dropdown, "Save as
    profile…", "Manage…".
  • Manage window has rename / duplicate / delete with an inline
    "Confirm delete?" row — profile data may be the user's only saved
    copy, so deletion isn't one-click.
  • Selector + Save-as both disabled while the proxy is running (the
    running task holds a cloned Config; rewriting config.json under
    it would cause runtime/disk drift).

Android UI (ProfileBar.kt)

  • Same selector / save / manage surfaces, wired into HomeScreen.
  • Honours ui_lang on switch: a profile that changes the locale
    fires the same onLangChange path as the top-bar toggle, so RTL/LTR
    flips correctly.
  • All new user-visible strings localised in both values/strings.xml
    and values-fa/strings.xml.

Design invariants

These are documented at the top of both profiles.rs and ProfileStore.kt and pinned by tests on both sides:

  1. Raw snapshot preservation. A profile's config is written to
    config.json byte-for-byte. Unknown fields (fronting_groups,
    exit_node, future Rust-side keys) survive the round-trip.
    Config::extras (Rust, via #[serde(flatten)]) and
    MhrvConfig.extrasJson (Android, via a MODELLED_KEYS filter)
    carry the bytes through the in-memory form too — so even after a
    user edits a field, the unknown keys are re-emitted on the next
    save.
  2. active == "name" means profiles[name].config equals the
    live config.json.
    Save-as writes both files (config first,
    profiles second). Deleting the active profile clears active = ""
    instead of jumping to a different profile. Regular form edits
    also clear active since the form no longer matches any saved
    snapshot.
  3. Persist before in-memory state changes. All mutations clone,
    save, and only commit the clone back to in-memory state on
    success. A failed disk write leaves both the live UI and the
    on-disk file unchanged.
  4. Load failure is loud. A corrupt profiles.json is renamed
    aside to profiles.json.corrupt-<nanos> and surfaces a startup
    toast. If the backup rename also fails, all profile writes are
    disabled for the session so we can't clobber the recoverable
    bytes. Read I/O failures (permissions, locked) get the same
    write-gate treatment.

@github-actions github-actions Bot added the type: feature feat: PR — auto-applied by release-drafter label May 11, 2026
dazzling-no-more added a commit to dazzling-no-more/rahgozar that referenced this pull request May 15, 2026
dazzling-no-more added a commit to dazzling-no-more/rahgozar that referenced this pull request May 15, 2026
- ConfigStore.toJson(): drop a duplicate `fronting_groups` put left
  behind when therealaleph#1033 (fronting groups) + therealaleph#1057 (extras passthrough) +
  therealaleph#1189 (draft-drop filter) were squash-merged in sequence. The
  later filtered put was being overwritten by the older unfiltered
  one, breaking the ConfigStoreFrontingGroupsTest expectation that
  draft groups never reach disk.
- ProfileStoreTest.applyProfile_*_returns_partial: the snapshot
  config was missing a deployment ID, so validateRuntimeShape
  rejected it as 'apps_script mode requires script_id' before the
  test's injected write failure could be exercised. Pre-existing
  bug in therealaleph#1057's tests; only surfaced now that we run the full
  suite on the integrated branch.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: feature feat: PR — auto-applied by release-drafter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant