Files
rustguac/static/docs.html
T
Dave Kempe 0d69e8fed4 Rename Address Book → Connections; allowed_groups picker; session privacy (#102)
Three pieces of v1.6.0 work that happened together and are easier to
review as one save point.

Rename: Address Book → Connections
- static/addressbook.html renamed to static/connections.html
- Nav links, page titles, empty states, onboarding, and prose updated
  across all 8 static pages (connections, admin, docs, index,
  recordings, reports, sessions, tokens).
- README, CLAUDE.md, and every file under docs/ updated.
- src/main.rs: connections.html added to the branded-page map and
  route list; /addressbook.html returns a 308 permanent redirect so
  existing bookmarks keep working.
- Backend API paths, Rust types, and Vault storage paths are
  deliberately unchanged — internal only.

Folder allowed_groups picker
- New SQLite table `seen_groups` tracks OIDC groups observed in any
  user login; OIDC callback upserts after extracting groups.
- `GET /api/auth/known-groups` (admin-only) returns the union of
  group_role_mappings and seen_groups.
- `GET /api/addressbook/folders/{scope}/{folder}/config` adds the
  missing endpoint the frontend was already calling — existing
  allowed_groups now prefill the edit-folder modal.
- Folder modal swaps the free-text comma-separated input for a chip
  picker with a themed combobox dropdown: autocomplete over known
  groups, keyboard nav, "+ add custom" row for unlisted groups.

Active session visibility (GitHub #102)
- `GET /api/sessions` scopes to the caller's own sessions by default;
  `?all=true` lets admins opt in (used by the Sessions page).
- `GET /api/sessions/{id}` and the thumbnail GET/PUT endpoints are
  now owner-or-admin, returning 404 for other callers so session
  existence isn't leaked.
- Connections' Active Sessions strip is now always owner-scoped —
  admins still manage everyone via the Sessions page.
2026-04-18 21:56:20 +10:00

224 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">Sessions</a>
<a href="/recordings.html">Recordings</a>
<a href="/reports.html" id="reports-link" style="display:none">Reports</a>
<a href="/docs.html" class="active">Docs</a>
<a href="/tokens.html" id="tokens-link" style="display:none">Tokens</a>
<a href="/admin.html" id="admin-link" style="display:none">Admin</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) {
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;
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>