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
+11
View File
@@ -235,4 +235,15 @@ impl RelayClient {
pub fn is_connected(&self) -> bool {
self.state.is_connected()
}
/// The configured relay URL ("" when not configured — the loop stays idle).
pub fn relay_url(&self) -> String {
self.state.relay_url()
}
/// The error that ended the last WS session, if any (UI troubleshooting).
/// Cleared on the next successful connect.
pub fn last_error(&self) -> Option<String> {
self.state.last_error()
}
}
+18
View File
@@ -58,6 +58,9 @@ pub(crate) struct RelayState {
/// Derived from the seed + the client's x25519 pubkey; never persisted.
aes_cache: Mutex<HashMap<[u8; 32], [u8; 32]>>,
connected: AtomicBool,
/// Last connection error that ended a WS session, for UI troubleshooting.
/// Cleared on the next successful connect.
last_error: Mutex<Option<String>>,
/// Broadcast sink for [`RelayEvent`]s consumed by the application layer.
events_tx: broadcast::Sender<RelayEvent>,
/// Pending `open_pipe` waiters: connection_id → accept/reject delivery
@@ -84,6 +87,7 @@ impl RelayState {
outbound: Mutex::new(None),
aes_cache: Mutex::new(HashMap::new()),
connected: AtomicBool::new(false),
last_error: Mutex::new(None),
events_tx,
pipe_waiters: Mutex::new(HashMap::new()),
incoming_pipes_tx,
@@ -115,6 +119,10 @@ impl RelayState {
pub(crate) fn set_connected(&self, v: bool) {
let was = self.connected.swap(v, Ordering::Relaxed);
if v {
// A live connection means the previous error is resolved.
*self.last_error.lock().unwrap() = None;
}
if was != v {
self.emit(if v { RelayEvent::Connected } else { RelayEvent::Disconnected });
}
@@ -124,6 +132,16 @@ impl RelayState {
self.connected.load(Ordering::Relaxed)
}
/// Record the error that ended a WS session (surfaced to the UI).
pub(crate) fn set_last_error(&self, msg: String) {
*self.last_error.lock().unwrap() = Some(msg);
}
/// The last recorded connection error, if any.
pub(crate) fn last_error(&self) -> Option<String> {
self.last_error.lock().unwrap().clone()
}
pub(crate) fn set_outbound(&self, tx: mpsc::UnboundedSender<Vec<u8>>) {
*self.outbound.lock().unwrap() = Some(tx);
}
+1
View File
@@ -46,6 +46,7 @@ pub(crate) async fn run_loop(
}
Err(e) => {
warn!(crate_name = "skald-relay-client", error = %e, "relay connection ended");
state.set_last_error(e.to_string());
}
}