Repository navigation
feat(user-role): persona/capability-gating — role model, callsite reclassification, and role-picker UI #879
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0521ef0
e795129
ecaa46f
736c3bc
eeb65e3
b336dd8
d8e3c55
5386d82
d3e6b05
a47542d
94309ff
1b8d2bf
daa474c
3a9e583
18caa36
dc6747e
1e27cd9
9a73dc2
dc6cd92
fe9b2be
f0f0c84
3271149
5826b85
61a6fc9
6bf8291
80fd1d2
b2ac420
7a69b92
3be88f0
14b07cf
908744f
7508da5
ce7096d
8377889
279d383
2edbc18
a37c78d
e235f88
c15173b
a7cf0a4
ad558d5
9646771
c81db44
7a2df9b
2965731
79914cf
116cc9c
fc3317c
f34d8d2
60fd091
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| # User Roles (Interface Mode) | ||
|
|
||
| Dash Evo Tool adapts its interface to how you use it. Instead of a single | ||
| "Expert mode" on/off switch, the app offers three interface modes, each | ||
| revealing more advanced controls than the one before it: | ||
|
|
||
| | Mode | What it shows | | ||
| | - | - | | ||
| | **Default view** | Your balance, send and receive, and usernames. | | ||
| | **Detailed view** | Adds account details, address tables, and masternode tools. | | ||
| | **Developer tools** | Adds raw protocol data, Devnet, and signing overrides. | | ||
|
|
||
| Each mode includes everything the one before it does — Developer tools has | ||
| everything Detailed view has, plus more. **Detailed view** is the starting | ||
| point until you pick a mode yourself; switch down to Default view for a | ||
| simpler screen, or up to Developer tools when you need raw protocol data. | ||
|
|
||
| ## Where to set it | ||
|
|
||
| The three modes are chosen the same way, with the same descriptions, in two | ||
| places: | ||
|
|
||
| - **Network Settings** — open the network chooser screen. An **Interface | ||
| mode** card sits above Advanced Settings, always visible (it is not tucked | ||
| inside a collapsed section). | ||
| - **Welcome screen** — on first launch, the onboarding screen shows a | ||
| "Choose your experience level" row with the same three options before you | ||
| create or load a wallet. | ||
|
|
||
| Picking a mode in either place updates the other — there is only one active | ||
| mode, shared across the whole app. | ||
|
|
||
| **This choice is reversible.** You are not locking yourself into a mode by | ||
| picking one at onboarding: open **Network Settings** at any time and change | ||
| it from the **Interface mode** card. The app applies the change immediately, | ||
| with no restart required. | ||
|
|
||
| ## Upgrading from an older install | ||
|
|
||
| Earlier versions had a **Developer mode** on/off switch instead of the three | ||
| interface modes. If you are upgrading, and you have not picked an interface | ||
| mode yet, the app starts you in **Detailed view** — the mode that matches the | ||
| account details, address tables and masternode tools those builds exposed. | ||
| Nothing you had is hidden, and you can move to another mode at any time from | ||
| **Network Settings**. | ||
|
|
||
| The interface mode is chosen in the app, not in a file: it is stored with your | ||
| other settings, and it is the same on every network. | ||
|
|
||
| ## Migration note: obsolete `.env` entries | ||
|
|
||
| Older configurations may still contain a global `DEVELOPER_MODE=true|false` | ||
| entry, or per-network entries such as `MAINNET_developer_mode=true`. Neither has | ||
| any effect on the interface mode — that is chosen in the app and stored with your | ||
| settings. New installs no longer get a `DEVELOPER_MODE` line at all. | ||
|
|
||
| Leave the leftover entries or delete them, as you prefer. The one place | ||
| `DEVELOPER_MODE` is still consulted is the one-time database upgrade that moved | ||
| existing installs onto the built-in light client (SPV): an install that had both | ||
| `DEVELOPER_MODE=true` and a Dash Core RPC password configured keeps talking to | ||
| its local Dash Core node. That upgrade runs once, and only for installs that | ||
| predate it. | ||
|
|
||
| The application directory holding `.env` is: | ||
|
|
||
| | Operating System | Path | | ||
| | - | - | | ||
| | macOS | `~/Library/Application Support/Dash-Evo-Tool/.env` | | ||
| | Windows | `C:\Users\<User>\AppData\Roaming\Dash-Evo-Tool\config\.env` | | ||
| | Linux | `~/.config/dash-evo-tool/.env` | |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,7 @@ use crate::backend_task::error::TaskError; | |
| use crate::backend_task::{BackendTask, BackendTaskSuccessResult}; | ||
| use crate::context::AppContext; | ||
| use crate::context::connection_status::{ConnectionStatus, OverallConnectionState}; | ||
| use crate::context::feature_gate::FeatureGate; | ||
| use crate::context::migration_status::MigrationStep; | ||
| use crate::database::Database; | ||
| use crate::model::settings::AppSettings; | ||
|
|
@@ -563,15 +564,11 @@ impl AppState { | |
|
|
||
| let saved_network = settings.network; | ||
|
|
||
| // App-global Expert Mode flag: read once from config and shared into | ||
| // every per-network context (including any created later by a network | ||
| // switch), so a live toggle is observed everywhere without a restart. | ||
| let developer_mode = Arc::new(std::sync::atomic::AtomicBool::new( | ||
| crate::config::Config::load_from(&data_dir) | ||
| .ok() | ||
| .and_then(|c| c.developer_mode) | ||
| .unwrap_or(false), | ||
| )); | ||
| // App-global user role, shared into every per-network context (including | ||
| // any created later by a network switch) so a live change is observed | ||
| // everywhere without a restart. Seeded below from `get_app_settings()` | ||
| // once the active context exists — the single persisted source of truth. | ||
| let user_role = crate::model::user_role::UserRoleCell::default(); | ||
|
|
||
| // Build a helper to create AppContext for a given network. | ||
| let make_context = |network: Network| -> Option<Arc<AppContext>> { | ||
|
|
@@ -584,7 +581,7 @@ impl AppState { | |
| ctx.clone(), | ||
| Arc::clone(&app_kv), | ||
| Arc::clone(&secret_store), | ||
| Arc::clone(&developer_mode), | ||
| user_role.clone(), | ||
| ) | ||
| }; | ||
|
|
||
|
|
@@ -636,6 +633,17 @@ impl AppState { | |
| .expect("invariant: chosen_network was just taken from network_contexts") | ||
| .clone(); | ||
|
|
||
| // Seed the shared role cell from AppSettings — the single source of truth. | ||
| // `get_app_settings` resolves an account that never chose a role to | ||
| // `UserRole::WHEN_UNSET`. `set_user_role` publishes the value to every | ||
| // context holding the cell and syncs animation gating. | ||
| active_context.set_user_role( | ||
| active_context | ||
| .get_app_settings() | ||
| .user_role | ||
| .unwrap_or(crate::model::user_role::UserRole::WHEN_UNSET), | ||
| ); | ||
|
Comment on lines
+640
to
+645
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💬 Nitpick: Dead .unwrap_or(UserRole::WHEN_UNSET) fallback after the settings fix I traced every path through source: ['sonnet5-rust-quality'] |
||
|
|
||
| // load fonts | ||
| ctx.set_fonts(crate::bundled::fonts().expect("failed to load fonts")); | ||
|
|
||
|
|
@@ -1057,12 +1065,12 @@ impl AppState { | |
| } | ||
|
|
||
| pub fn active_root_screen_mut(&mut self) -> &mut Screen { | ||
| // Live de-gating (§10.11): if Expert Mode flipped off while the | ||
| // Live de-gating (§10.11): if the role dropped below Power while the | ||
| // Masternodes tab was active, fall back to the neutral Identities tab so | ||
| // the Expert-gated screen is never shown without its gate. Identities is | ||
| // always registered, so the subsequent lookup cannot fail. | ||
| // the gated screen is never shown without its gate. Identities is always | ||
| // registered, so the subsequent lookup cannot fail. | ||
| if self.selected_main_screen == RootScreenType::RootScreenMasternodes | ||
| && !self.current_app_context().is_developer_mode() | ||
| && !FeatureGate::Masternodes.is_available(self.current_app_context()) | ||
| { | ||
| self.selected_main_screen = RootScreenType::RootScreenIdentities; | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 Blocking: A transient settings read still elevates the runtime role for the session
get_app_settings()now correctly returns an uncached fallback on error, but this boot call immediately publishes that fallback. The error fallback iswith_default_user_role(AppSettings::default()), so its role isPower. If the persisted role isEverydayand this read transiently fails, the sharedUserRoleCellbecomesPower; after the store recovers, later settings reads returnEveryday, but nothing reseeds the cell, so UI and feature gates remain at Power until restart. The MCP boot path has the same sequence. This leaves the runtime role inconsistent with the recovered settings and can expose Power-only operations after a transient read failure.Keep the least-privileged pre-seed role on read errors, or make the boot seed distinguish a successful/absent read from an error instead of publishing the fallback. Please add a regression that stores
Everyday, fails the boot role read once, recovers the store, and asserts the runtime role is not left atPower.