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(