Files
rustdesk-server/admin_ui/index.html
T
mike c1eaac1cb3
build / build-linux-amd64 (push) Successful in 2m1s
web UI and web client improvements.
2026-05-08 08:42:59 +02:00

113 lines
5.5 KiB
HTML

<!doctype html>
<html lang="en" class="h-full">
<head>
<meta charset="utf-8" />
<title>RustDesk Admin</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="/admin/assets/tailwindcss.js"></script>
<script src="/admin/assets/htmx.min.js"></script>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; }
.nav-link.active { background: rgb(15 23 42); color: rgb(125 211 252); }
</style>
</head>
<body class="h-full bg-slate-950 text-slate-100">
<!--
Single-page shell. The sidebar drives navigation via HTMX:
each link does an `hx-get` of an HTML fragment URL that returns the
body of the page. The fragments live under /admin/pages/ and are
server-rendered Rust handlers that return Html<String>.
This keeps the UI a flat directory of static files plus a small
set of fragment endpoints — no SPA, no Node, no build step.
-->
<div class="min-h-full flex">
<aside class="w-56 shrink-0 bg-slate-900 border-r border-slate-800 flex flex-col">
<div class="px-4 py-5 border-b border-slate-800">
<h1 class="text-base font-semibold">RustDesk Admin</h1>
<p id="me-display" class="text-xs text-slate-500 mt-1" hx-get="/admin/me" hx-trigger="load" hx-swap="innerHTML"></p>
</div>
<nav class="flex-1 px-2 py-3 space-y-1">
<a class="nav-link block px-3 py-1.5 text-sm rounded text-slate-300 hover:bg-slate-800"
hx-get="/admin/pages/users" hx-target="#main" hx-push-url="#users">Users</a>
<a class="nav-link block px-3 py-1.5 text-sm rounded text-slate-300 hover:bg-slate-800"
hx-get="/admin/pages/devices" hx-target="#main" hx-push-url="#devices">Devices</a>
<a class="nav-link block px-3 py-1.5 text-sm rounded text-slate-300 hover:bg-slate-800"
hx-get="/admin/pages/groups" hx-target="#main" hx-push-url="#groups">Device groups</a>
<a class="nav-link block px-3 py-1.5 text-sm rounded text-slate-300 hover:bg-slate-800"
hx-get="/admin/pages/strategies" hx-target="#main" hx-push-url="#strategies">Strategies</a>
<a class="nav-link block px-3 py-1.5 text-sm rounded text-slate-300 hover:bg-slate-800"
hx-get="/admin/pages/address-books" hx-target="#main" hx-push-url="#address-books">Address books</a>
<a class="nav-link block px-3 py-1.5 text-sm rounded text-slate-300 hover:bg-slate-800"
hx-get="/admin/pages/oidc" hx-target="#main" hx-push-url="#oidc">OIDC providers</a>
<a class="nav-link block px-3 py-1.5 text-sm rounded text-slate-300 hover:bg-slate-800"
hx-get="/admin/pages/audit" hx-target="#main" hx-push-url="#audit">Audit log</a>
<a class="nav-link block px-3 py-1.5 text-sm rounded text-slate-300 hover:bg-slate-800"
hx-get="/admin/pages/recordings" hx-target="#main" hx-push-url="#recordings">Recordings</a>
<a class="nav-link block px-3 py-1.5 text-sm rounded text-slate-300 hover:bg-slate-800"
hx-get="/admin/pages/deploy" hx-target="#main" hx-push-url="#deploy">Deploy</a>
</nav>
<div class="px-2 py-3 border-t border-slate-800 space-y-1">
<a class="nav-link block px-3 py-1.5 text-sm rounded text-slate-400 hover:bg-slate-800"
hx-get="/admin/pages/profile" hx-target="#main" hx-push-url="#profile">My profile</a>
<button
class="w-full text-left px-3 py-1.5 text-sm rounded text-slate-400 hover:bg-slate-800"
hx-post="/admin/logout"
hx-on::after-request="window.location.href = '/admin/login.html'"
>Sign out</button>
</div>
</aside>
<main id="main" class="flex-1 p-6 overflow-x-hidden">
<div class="text-slate-500 text-sm">Loading…</div>
</main>
</div>
<!-- Toast container used by all admin handlers via hx-trigger="load delay:1s" -->
<div id="toast"
class="fixed bottom-4 right-4 max-w-sm space-y-2 pointer-events-none"></div>
<!-- Load fragment + highlight active link based on the URL hash. -->
<script>
function linkForHash() {
const hash = location.hash || '#users';
return document.querySelector('.nav-link[hx-push-url="' + hash + '"]')
|| document.querySelector('.nav-link[hx-push-url="#users"]');
}
function refreshActive() {
const active = linkForHash();
document.querySelectorAll('.nav-link').forEach(a => {
a.classList.toggle('active', a === active);
});
}
function loadFromHash() {
const link = linkForHash();
if (link) {
htmx.ajax('GET', link.getAttribute('hx-get'),
{ target: '#main', swap: 'innerHTML' });
}
refreshActive();
}
window.addEventListener('hashchange', loadFromHash);
document.body.addEventListener('htmx:afterSwap', refreshActive);
loadFromHash();
// Bounce to login if any HTMX request comes back 401.
document.body.addEventListener('htmx:responseError', (evt) => {
if (evt.detail.xhr.status === 401) {
window.location.href = '/admin/login.html';
}
});
// Close any open per-row action popover when a click happens outside it.
// The action dropdowns are <details class="... relative"> with an
// absolutely-positioned panel; the deploy page uses <details> too but
// without `relative`, so the selector is specific to the popover style.
document.addEventListener('click', (e) => {
document.querySelectorAll('details.relative[open]').forEach(d => {
if (!d.contains(e.target)) d.removeAttribute('open');
});
});
</script>
</body>
</html>