From 7b6526a2e8a21e901e2922bc0c9581fd1cfdef08 Mon Sep 17 00:00:00 2001 From: Mike Mueller Date: Fri, 22 May 2026 23:03:06 +0200 Subject: [PATCH] Improve device detail view --- admin_ui/index.html | 40 ++++++++++++++++++++++++++++++++++ src/api/admin/pages/devices.rs | 11 +++++++--- src/api/admin/pages/exec.rs | 3 ++- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/admin_ui/index.html b/admin_ui/index.html index c4c82c5..221c830 100644 --- a/admin_ui/index.html +++ b/admin_ui/index.html @@ -101,8 +101,48 @@ a.classList.toggle('active', a === active); }); } + // Dynamic deep-links: `#devices/` and `#devices//exec`. + // The detail / exec fragments are designed to swap into the + // devices index page's `#devices-region` section, so when we + // land here from a page refresh we have to chain two ajax calls: + // first render the parent page (which provides `#devices-region`), + // then swap the fragment into it. htmx.ajax has returned a Promise + // since 1.9.4, so the `.then` chain is safe at our pinned 1.9.10. + const DEEP_LINK_PATTERNS = [ + { + re: /^#devices\/([^/]+)\/exec$/, + parent: '/admin/pages/devices', + fragment: id => `/admin/pages/devices/${encodeURIComponent(id)}/exec`, + }, + { + re: /^#devices\/([^/]+)$/, + parent: '/admin/pages/devices', + fragment: id => `/admin/pages/devices/${encodeURIComponent(id)}/detail`, + }, + ]; + function loadDeepLink(hash) { + for (const p of DEEP_LINK_PATTERNS) { + const m = hash.match(p.re); + if (!m) continue; + const id = decodeURIComponent(m[1]); + Promise.resolve( + htmx.ajax('GET', p.parent, { target: '#main', swap: 'innerHTML' }) + ).then(() => + htmx.ajax('GET', p.fragment(id), { + target: '#devices-region', + swap: 'innerHTML', + }) + ); + return true; + } + return false; + } function loadFromHash() { const hash = location.hash || '#users'; + if (loadDeepLink(hash)) { + refreshActive(); + return; + } const subUrl = SUB_ROUTES[hash]; if (subUrl) { htmx.ajax('GET', subUrl, { target: '#main', swap: 'innerHTML' }); diff --git a/src/api/admin/pages/devices.rs b/src/api/admin/pages/devices.rs index b33f85b..e82d4ac 100644 --- a/src/api/admin/pages/devices.rs +++ b/src/api/admin/pages/devices.rs @@ -1074,12 +1074,14 @@ fn render_device_row(
@@ -1146,12 +1148,15 @@ pub async fn list_fragment( /// "Back to devices" — refetches the devices table fragment via HTMX /// and swaps it back into `#devices-region`. Used by the detail page. +/// `hx-push-url="#devices"` resets the address bar to the list-level +/// hash so a subsequent refresh lands on the list, not the detail. fn back_button(lang: Lang) -> String { format!( r##""##, + hx-swap="innerHTML" + hx-push-url="#devices">{label}"##, label = t(lang, "devices.back"), ) } diff --git a/src/api/admin/pages/exec.rs b/src/api/admin/pages/exec.rs index 6144a7a..9251ac9 100644 --- a/src/api/admin/pages/exec.rs +++ b/src/api/admin/pages/exec.rs @@ -227,7 +227,8 @@ async fn render_page(

{heading} {id}

+ hx-target="#devices-region" hx-swap="innerHTML" + hx-push-url="#devices">{back} "##, heading = t(lang, "exec.heading"), id = html_escape(peer_id),