mobile-connector: merge pairing+devices into one self-service Mobile App page
Nightly Build / build (push) Successful in 7m3s

The two admin-only console pages become a single "Mobile App" page
visible to every logged-in user: connection status (with the last
connection error for troubleshooting), the device list (admin sees all,
others only their own), a pairing dialog with the QR, and — admin-only —
a settings dialog hosting the plugin config, including a relay picker
(official grayed out, test, custom URL). The generic plugin-detail
config form defers to it via the new Plugin::config_in_detail_page flag.

Pairing is now self-service: any user opens a window and the device
auto-binds to them; revocation is admin-for-anyone, owner-for-self;
(re)binding to another user stays admin-only. Binding-managed plugins
(manages_own_access) now expose their non-admin pages to all users and
self-scope per caller (web_pages_for). The relay client records the
error that ends a WS session and clears it on reconnect.
This commit is contained in:
2026-07-27 23:49:50 +01:00
parent a78259551e
commit 50e1333d99
18 changed files with 834 additions and 411 deletions
+10 -5
View File
@@ -57,7 +57,7 @@ pub struct RelayApp {
/// Per-user debounced notifiers, created on demand by the forwarders.
pub(crate) notifiers: Mutex<HashMap<String, Arc<DelayedNotifier>>>,
/// The user a device paired *during the current window* auto-binds to — set
/// by the web pairing console (the admin who opened the window). `None` for
/// by the web pairing dialog (the user who opened the window). `None` for
/// the agent-tool flow (`mobile_start_pairing`), which leaves the device
/// Pending for an explicit `mobile_bind_device`. Cleared on stop-pairing.
pending_owner: Mutex<Option<String>>,
@@ -91,7 +91,7 @@ impl RelayApp {
}
/// Set (or clear) the user that devices paired during the current window
/// auto-bind to. Called by the web pairing endpoint with the admin's id.
/// auto-bind to. Called by the web pairing endpoint with the caller's id.
pub(crate) async fn set_pending_owner(&self, user_id: Option<String>) {
*self.pending_owner.lock().await = user_id;
}
@@ -107,6 +107,11 @@ impl RelayApp {
&self.client
}
/// The relay URL this run is configured with ("" = not configured).
pub(crate) fn relay_url(&self) -> String {
self.client.relay_url()
}
/// Backend localizer — the router resolves its error strings to the caller's
/// language through this (`app.i18n().for_user(user_id, key, &[])`).
pub(crate) fn i18n(&self) -> &Arc<dyn I18nApi> {
@@ -363,16 +368,16 @@ impl RelayApp {
self.apply_client_payload(&from, &payload).await;
}
Ok(RelayEvent::ClientPaired { ed25519_pub, .. }) => {
// Web-console pairing: the admin who opened the window is
// Web-dialog pairing: the user who opened the window is
// the pending owner, so bind (and thereby authorize) the
// device to them straight away — usable on the phone at
// once, reassignable later from the Devices page.
// once, reassignable later from the Mobile App page.
if let Some(owner) = self.pending_owner().await {
match self.bind_device(ed25519_pub, owner.clone(), None).await {
Ok(()) => info!(
plugin = PLUGIN_ID, user_id = %owner,
device = %hex::encode(ed25519_pub),
"new device paired — auto-bound to pairing admin"
"new device paired — auto-bound to pairing user"
),
Err(e) => warn!(plugin = PLUGIN_ID, error = %e, "auto-bind on pair failed"),
}