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
+18 -8
View File
@@ -42,6 +42,9 @@ pub struct PluginInfo {
/// Whether the plugin gates access through its own binding lifecycle — the
/// admin UI hides the "User access" checklist when true (see the trait).
pub manages_own_access: bool,
/// Whether the plugin-detail page shows the generic `config_schema` form
/// (`false` = the plugin hosts its own config UI in one of its pages).
pub config_in_detail_page: bool,
pub runtime_status: Option<Value>,
}
@@ -404,6 +407,7 @@ impl PluginManager {
user_config_schema: plugin.user_config_schema(),
has_router: plugin.http_router().is_some(),
manages_own_access: plugin.manages_own_access(),
config_in_detail_page: plugin.config_in_detail_page(),
runtime_status: plugin.runtime_status(),
});
}
@@ -463,8 +467,9 @@ impl PluginManager {
/// entry of every **enabled** plugin, filtered by audience — `admin_only`
/// pages go to the admin role only; the others require the `plugin_access`
/// grant (admins see all). Binding-managed plugins (`manages_own_access`)
/// keep their pages admin-only unless the page says otherwise, mirroring
/// `list_accessible`.
/// own their access model (e.g. the device↔user binding), so their
/// non-`admin_only` pages are visible to every logged-in user and the page
/// itself scopes what each caller sees.
pub async fn web_pages_for(&self, user_id: &str, is_admin: bool) -> Result<Vec<PluginPageInfo>> {
let granted: std::collections::HashSet<String> = if is_admin {
std::collections::HashSet::new()
@@ -484,8 +489,10 @@ impl PluginManager {
for page in pages {
let visible = if is_admin {
true
} else if page.admin_only || owns_access {
} else if page.admin_only {
false
} else if owns_access {
true
} else {
granted.contains(plugin.id())
};
@@ -640,15 +647,18 @@ mod tests {
assert_eq!(admin[0].api_version, 1);
// Non-admin: only the non-admin_only page of a granted, enabled,
// non-binding-managed plugin — beta is disabled, gamma manages its own
// access, alpha's admin console is admin_only.
// non-binding-managed plugin — beta is disabled, alpha's admin console
// is admin_only. gamma manages its own access, so its page is visible
// to everyone and self-scopes per caller.
let user = mgr.web_pages_for("u1", false).await.unwrap();
let got: Vec<(&str, &str)> = user.iter()
.map(|p| (p.plugin_id.as_str(), p.page_id.as_str())).collect();
assert_eq!(got, vec![("alpha", "user-dash")]);
assert_eq!(got, vec![("gamma", "pairing"), ("alpha", "user-dash")]);
// A user with no grants sees nothing.
// A user with no grants sees only the binding-managed page.
let stranger = mgr.web_pages_for("u2", false).await.unwrap();
assert!(stranger.is_empty());
let got: Vec<(&str, &str)> = stranger.iter()
.map(|p| (p.plugin_id.as_str(), p.page_id.as_str())).collect();
assert_eq!(got, vec![("gamma", "pairing")]);
}
}