Files
rustguac/static/docs.html
T
Jad Seifeddine be866264bd pages: shared facet filters and one list toolbar on every list page
reports: show entry names as host over a tenant subline

Session history and the most-used table showed "host (Tenant)" names
wrapped across three lines; they now read as the host over a quieter
tenant line, as on Connections and Recordings.

Part of #254

sessions, recordings: show the session type with the shared badge (#254)

The sessions page printed the type as plain text and the recordings page
as a coloured word; connections and reports already used .type-badge
from rustguac.css. All four now use the same badge. The Proxmox badge
colour moves into the shared stylesheet with the others.

Shared facet filters (static/filters.js) on the sessions, recordings,
reports and connections pages: a dropdown per facet (Type, Folder, User,
Owner, Status as each page has them), multi-select with counts that narrow
as other facets apply, All/None, a find box for long lists, a Clear filters
link, and selections kept per page in sessionStorage. They sit beside the
existing text search and combine with it; the buttons are compacted to the
height and padding of the other controls and the dropdown reserves room for
its scrollbar so it no longer sits over the counts.

The list pages share one toolbar: a borderless row with the search box, the
facets and the page action on the right (New ad-hoc session, Export CSV),
placeholders standardised to 'Search <noun> (fields…)'. Sessions gains a
search (user, host, owner, ID, type) and a User facet; Reports moves from
its own .filter-bar to the shared one. The redundant page titles go — the
nav already names and marks the active page — and section headings centre
their heading and action button on one line.

Connections: the folder header is sticky at the top of the entries pane, so
it stays under the nav and the active-sessions strip while a long table
scrolls. All four pages show the session type with the shared .type-badge
(the Proxmox colour moves into rustguac.css), reports and recordings show
"host (Tenant)" names as the host over a quieter tenant line, and the key
glyph before Credentials in the nav is sized to the text.

Part of #254

tests: shared facet filters and the list toolbar on every list page
2026-10-16 16:51:36 +07:00

230 lines
12 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>rustguac - Docs</title>
<link rel="stylesheet" href="/rustguac.css">
<style>
.docs-layout {
display: flex;
min-height: calc(100vh - 180px);
border-top: 1px solid var(--border);
margin-top: var(--s-3);
}
.sidebar {
width: 240px; min-width: 240px;
border-right: 1px solid var(--border);
padding: var(--s-3) 0;
overflow-y: auto;
}
.sidebar a {
display: block;
padding: var(--s-2) var(--s-4);
color: var(--text-muted);
text-decoration: none;
font-size: var(--fz-sm);
border-left: 3px solid transparent;
}
.sidebar a:hover { color: var(--text); background: var(--surface); }
.sidebar a.active {
color: var(--primary);
border-left-color: var(--primary);
background: var(--surface);
font-weight: bold;
}
.content {
flex: 1;
overflow-y: auto;
padding: var(--s-4) var(--s-5);
max-width: 960px;
}
.content h1 { color: var(--primary); font-size: var(--fz-xl); margin-bottom: var(--s-3); }
.content h2 {
color: var(--primary); font-size: var(--fz-lg);
margin-top: var(--s-5); margin-bottom: var(--s-2);
border-bottom: 1px solid var(--border); padding-bottom: var(--s-2);
}
.content h3 { color: var(--accent); font-size: var(--fz-md); margin-top: var(--s-4); margin-bottom: var(--s-1); }
.content h4 { color: var(--accent); font-size: var(--fz-sm); margin-top: var(--s-3); margin-bottom: var(--s-1); }
.content p { margin: var(--s-2) 0; line-height: 1.65; }
.content ul, .content ol { margin: var(--s-2) 0 var(--s-2) var(--s-4); line-height: 1.65; }
.content li { margin: var(--s-1) 0; }
.content a { color: var(--accent); }
.content a:hover { text-decoration: underline; }
.content code {
background: var(--surface);
padding: 2px var(--s-1);
border-radius: var(--radius);
font-size: 0.9em;
}
.content pre {
background: var(--surface);
border: 1px solid var(--border);
padding: var(--s-3);
border-radius: var(--radius);
overflow-x: auto;
line-height: 1.5;
margin: var(--s-2) 0;
}
.content pre code { background: none; padding: 0; border-radius: 0; font-size: var(--fz-sm); }
.content table { border-collapse: collapse; width: 100%; margin: var(--s-2) 0; }
.content th, .content td {
text-align: left; padding: var(--s-2) var(--s-3);
border-bottom: 1px solid var(--border); font-size: var(--fz-sm);
}
.content th { color: var(--text-muted); text-transform: none; letter-spacing: 0; background: transparent; }
.content blockquote {
border-left: 3px solid var(--accent);
margin: var(--s-2) 0;
padding: var(--s-1) var(--s-3);
color: var(--text-muted);
}
.content hr { border: none; border-top: 1px solid var(--border); margin: var(--s-5) 0; }
.loading { padding: var(--s-4); color: var(--text-muted); }
@media (max-width: 700px) {
.sidebar { width: 180px; min-width: 180px; }
.content { padding: var(--s-3); }
}
</style>
<script>(function(){var c=localStorage.getItem('rustguac_theme_colors');if(c){try{var o=JSON.parse(c),r=document.documentElement.style;for(var k in o)r.setProperty('--'+k.replace(/_/g,'-'),o[k]);if(o.bg_pattern&&o.bg_pattern!=='none'){var s=document.createElement('style');s.id='bg-pattern-style';s.textContent='body{background-image:'+o.bg_pattern+';background-attachment:fixed}';document.head.appendChild(s)}}catch(e){}}})();</script>
</head>
<body>
<div class="brand-row">
<img id="site-logo" src="/logo.svg" alt="">
<h1>rustguac</h1>
</div>
<nav>
<a href="/connections.html">Connections</a>
<a href="/sessions.html" id="sessions-link" data-min-role="poweruser">Sessions</a>
<a href="/recordings.html">Recordings</a>
<a href="/reports.html" id="reports-link" data-min-role="poweruser">Reports</a>
<a href="/docs.html" class="active">Docs</a>
<a href="/tokens.html" id="tokens-link" data-min-role="operator">Tokens</a>
<a href="/admin.html" id="admin-link" data-min-role="admin">Admin</a>
<a href="/connections.html#my-credentials" id="my-creds-nav" title="Manage your saved credential variables"><span class="nav-key">&#128273;</span> Credentials</a>
<span id="user-menu-wrapper"><a href="#" id="logout-item">Logout</a><span id="user-menu-btn" title="Preferences">&#9881; Settings</span><div id="user-menu"><div class="um-section-label">Theme</div><div id="um-theme-list"></div></div></span>
</nav>
<div class="docs-layout">
<div class="sidebar" id="sidebar"></div>
<div class="content" id="content">
<div class="loading">Loading documentation...</div>
</div>
</div>
<script>
var docs = [];
var sidebar = document.getElementById('sidebar');
var content = document.getElementById('content');
function showSection(slug) {
var doc = docs.find(function(d) { return d.slug === slug; });
if (!doc && docs.length) doc = docs[0];
if (!doc) return;
content.innerHTML = doc.html;
// Update sidebar active state
var links = sidebar.querySelectorAll('a');
for (var i = 0; i < links.length; i++) {
links[i].classList.toggle('active', links[i].dataset.slug === doc.slug);
}
// Update hash without triggering hashchange
if (location.hash !== '#' + doc.slug) {
history.replaceState(null, '', '#' + doc.slug);
}
content.scrollTop = 0;
}
function getSlugFromHash() {
return location.hash ? location.hash.substring(1) : '';
}
// Load docs from API
fetch('/api/docs')
.then(function(r) { return r.json(); })
.then(function(data) {
docs = data;
// Build sidebar
var html = '';
for (var i = 0; i < docs.length; i++) {
html += '<a href="#' + docs[i].slug + '" data-slug="' + docs[i].slug + '">' + docs[i].title + '</a>';
}
sidebar.innerHTML = html;
// Click handler for sidebar links
sidebar.addEventListener('click', function(e) {
var a = e.target.closest('a');
if (!a) return;
e.preventDefault();
var slug = a.dataset.slug;
history.pushState(null, '', '#' + slug);
showSection(slug);
});
// Show initial section from hash or first
showSection(getSlugFromHash() || docs[0].slug);
})
.catch(function() {
content.innerHTML = '<div class="loading">Failed to load documentation.</div>';
});
// Handle browser back/forward
window.addEventListener('popstate', function() {
showSection(getSlugFromHash());
});
// Theme and auth
function applyThemeColors(colors){var r=document.documentElement.style;for(var k in colors)r.setProperty('--'+k.replace(/_/g,'-'),colors[k]);var s=document.getElementById('bg-pattern-style');if(!s){s=document.createElement('style');s.id='bg-pattern-style';document.head.appendChild(s)}s.textContent=colors.bg_pattern&&colors.bg_pattern!=='none'?'body{background-image:'+colors.bg_pattern+';background-attachment:fixed}':'';localStorage.setItem('rustguac_theme_colors',JSON.stringify(colors))}
var _themePresets={},_adminPreset='aurora';
var _themeDescriptions={dark:'Navy & cyan \u2014 the default',light:'Clean white & blue','high-contrast':'Maximum readability',terminal:'Retro green-on-black',nord:'Arctic, muted blues',corporate:'Slate & steel blue',aurora:'Midnight blue with ambient glow',jaguar:'Racing green & gold'};
function initTheme(t){if(!t)return;_themePresets=t.presets||{};_adminPreset=t.admin_preset||'aurora';var u=localStorage.getItem('rustguac_theme'),active=u&&_themePresets[u]?u:_adminPreset,colors=(active===_adminPreset)?t.admin_colors:_themePresets[active];if(colors)applyThemeColors(colors);if(t.logo_url){var l=document.getElementById('site-logo');if(l){if(l.src!==t.logo_url&&!l.src.endsWith(t.logo_url))l.src=t.logo_url;l.style.display=''}}var menu=document.getElementById('um-theme-list');if(menu){menu.innerHTML='';Object.keys(_themePresets).forEach(function(name){var item=document.createElement('div');item.className='um-item'+(name===active?' active':'');var sw=document.createElement('span');sw.className='um-swatch';var p=_themePresets[name];sw.style.background='linear-gradient(135deg,'+p.primary+' 50%,'+p.accent+' 50%)';item.appendChild(sw);var info=document.createElement('div');info.className='um-theme-info';var nm=document.createElement('span');nm.className='um-theme-name';nm.textContent=name;info.appendChild(nm);var desc=document.createElement('span');desc.className='um-theme-desc';desc.textContent=_themeDescriptions[name]||'';info.appendChild(desc);item.appendChild(info);item.addEventListener('click',function(){localStorage.setItem('rustguac_theme',name);applyThemeColors(_themePresets[name]);menu.querySelectorAll('.um-item').forEach(function(el){el.classList.remove('active')});item.classList.add('active');document.getElementById('user-menu').style.display='none'});menu.appendChild(item)})}}
var _ub=document.getElementById('user-menu-btn');if(_ub)_ub.addEventListener('click',function(e){e.stopPropagation();var m=document.getElementById('user-menu');m.style.display=m.style.display==='block'?'none':'block'});document.addEventListener('click',function(){var m=document.getElementById('user-menu');if(m)m.style.display='none'});
fetch('/api/auth/status').then(function(r){return r.json()}).then(function(d){
if(d.site_title){document.title=d.site_title+' - Docs';document.querySelector('h1').textContent=d.site_title;}
initTheme(d.theme);
});
var apiKey = sessionStorage.getItem('rustguac_api_key');
var roleLevel = { admin: 4, poweruser: 3, operator: 2, viewer: 1 };
if (apiKey) {
// A key is never on the page GET, so the server served the anonymous variant.
document.documentElement.setAttribute('data-role-level', '4');
document.getElementById('admin-link').style.display = '';
document.getElementById('tokens-link').style.display = '';
document.getElementById('reports-link').style.display = '';
} else {
fetch('/api/me', { credentials: 'same-origin' })
.then(function(res) {
if (res.status === 401 || res.status === 403) { window.location.href = '/'; return; }
if (!res.ok) return;
return res.json();
})
.then(function(data) {
if (!data) return;
if (data.role === 'admin') document.getElementById('admin-link').style.display = '';
var level = roleLevel[data.role] || 0;
// Reconcile the server-written nav gate (see rustguac.css): the page
// GET carried no API key, so key users arrive on the anonymous variant.
document.documentElement.setAttribute('data-role-level', String((typeof apiKey !== 'undefined' && apiKey) ? 4 : level));
if (level >= 2) document.getElementById('tokens-link').style.display = '';
if (level >= 3) {
document.getElementById('reports-link').style.display = '';
}
if (level < 3) {
document.getElementById('sessions-link').style.display = 'none';
}
})
.catch(function() {});
}
document.getElementById('logout-item').addEventListener('click', function() {
sessionStorage.removeItem('rustguac_api_key');
fetch('/auth/logout', { credentials: 'same-origin' })
.finally(function() { window.location.href = '/'; });
});
</script>
</body>
</html>