Files
rustguac/static/docs.html
T
Dave Kempe ea72c52a31 v0.5.0: Theme system, ARM64 builds, Docker fixes, dependency updates
Theme system:
- 6 built-in presets (dark, light, high-contrast, terminal, nord, corporate)
- Admin configures preset + per-color overrides in [theme] config section
- Client-side theme switching via localStorage (flash-free)
- All static pages updated with 28 CSS custom properties

Proxy telemetry:
- Track which side terminated connection (guacd/browser/cancelled)
- Timing-based log levels (guacd close <5s = warning)
- Clamp session dimensions to safe ranges (width 640-8192, height 480-8192, DPI 16-384)

Docker fixes (#37):
- Fix port mismatch: Dockerfile now uses 8089 consistently
- Auto-generate admin API key on first run
- Add API key setup docs and recordings volume to compose example

ARM64 support:
- Multi-platform Docker builds (linux/amd64 + linux/arm64)
- Native ARM64 .deb and tarball builds via ubuntu-24.04-arm runner

Dependency updates:
- rustls 0.23.37, chrono 0.4.44, clap 4.5.60, toml 1.0.3
- futures-util 0.3.32, uuid 1.21.0, pulldown-cmark 0.13.1
- actions/upload-artifact v7, actions/download-artifact v8

Also: FreeRDP 3.x NULL deref patch (003), .gitignore for .playwright-mcp/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 15:05:41 +11:00

239 lines
13 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>rustguac - Docs</title>
<style>
:root { --primary: #e94560; --primary-hover: #c73652; --accent: #5bc0be; --accent-hover: #4aa3a1; --bg: #1a1a2e; --surface: #16213e; --input: #0f3460; --text: #e0e0e0; --text-muted: #aaa; --border: #333; --text-dim: #666; --text-on-primary: #fff; --btn-disabled: #555; --status-pending: #f0c040; --status-active: #5bc0be; --status-completed: #888; --status-error: #e94560; --status-expired: #666; --type-ssh-bg: #1b4332; --type-ssh-fg: #52b788; --type-rdp-bg: #3d1f00; --type-rdp-fg: #f0a050; --type-vnc-bg: #2d1b4e; --type-vnc-fg: #b07ff0; --type-web-bg: #1a1a4e; --type-web-fg: #7b8ff0; --hop-bg: #1b4332; --hop-fg: #52b788; }
* { box-sizing: border-box; }
body { font-family: monospace; background: var(--bg); color: var(--text); padding: 2em; font-size: 18px; margin: 0; }
h1 { color: var(--primary); }
nav { margin-bottom: 1.5em; font-size: 0.95em; }
nav a { margin-right: 1.5em; text-decoration: none; color: var(--accent); }
nav a:hover { text-decoration: underline; }
nav .active { color: var(--primary); font-weight: bold; }
#user-menu-wrapper { position: relative; float: right; }
#user-menu-btn { cursor: pointer; font-size: 1.3em; color: var(--text-muted); }
#user-menu-btn:hover { color: var(--text); }
#user-menu { display:none; position:absolute; right:0; top:1.8em; background:var(--surface); border:1px solid var(--border); border-radius:6px; min-width:220px; z-index:50; padding:0.4em 0; }
.um-section-label { padding:0.4em 0.9em; font-size:0.8em; color:var(--text-dim); text-transform:uppercase; letter-spacing:0.05em; }
.um-item { padding:0.5em 0.9em; cursor:pointer; display:flex; align-items:center; gap:0.6em; font-size:0.9em; }
.um-item:hover { background:var(--input); }
.um-item.active { color:var(--accent); }
.um-theme-info { display:flex; flex-direction:column; }
.um-theme-name { font-weight:bold; }
.um-theme-desc { font-size:0.8em; color:var(--text-dim); }
.um-swatch { display:inline-block; width:16px; height:16px; border-radius:50%; border:1px solid var(--border); flex-shrink:0; }
.um-divider { height:1px; background:var(--border); margin:0.3em 0; }
.um-logout { color:var(--text-muted); }
.um-logout:hover { color:var(--primary); background:var(--input); }
/* Layout */
.docs-layout { display: flex; min-height: calc(100vh - 140px); border-top: 1px solid var(--border); margin-top: 1em; }
/* Sidebar */
.sidebar {
width: 220px;
min-width: 220px;
border-right: 1px solid var(--border);
padding: 1em 0;
overflow-y: auto;
}
.sidebar a {
display: block;
padding: 0.5em 1.2em;
color: var(--text-muted);
text-decoration: none;
font-size: 0.9em;
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 area */
.content {
flex: 1;
overflow-y: auto;
padding: 2em 2.5em;
max-width: 900px;
}
.content h1 { color: var(--primary); font-size: 1.5em; margin-bottom: 0.8em; }
.content h2 { color: var(--primary); font-size: 1.15em; margin-top: 2em; margin-bottom: 0.5em; border-bottom: 1px solid var(--border); padding-bottom: 0.3em; }
.content h3 { color: var(--accent); font-size: 1em; margin-top: 1.5em; margin-bottom: 0.4em; }
.content h4 { color: var(--accent); font-size: 0.95em; margin-top: 1.2em; margin-bottom: 0.3em; }
.content p { margin: 0.6em 0; line-height: 1.6; }
.content ul, .content ol { margin: 0.5em 0 0.5em 1.5em; line-height: 1.6; }
.content li { margin: 0.2em 0; }
.content a { color: var(--accent); }
.content a:hover { text-decoration: underline; }
.content code {
background: var(--surface);
padding: 0.15em 0.4em;
border-radius: 3px;
font-size: 0.9em;
}
.content pre {
background: var(--surface);
border: 1px solid var(--border);
padding: 1em;
border-radius: 6px;
overflow-x: auto;
line-height: 1.5;
margin: 0.8em 0;
}
.content pre code {
background: none;
padding: 0;
border-radius: 0;
font-size: 0.85em;
}
.content table { border-collapse: collapse; width: 100%; margin: 0.8em 0; }
.content th, .content td { text-align: left; padding: 0.4em 0.8em; border-bottom: 1px solid var(--border); }
.content th { color: var(--text-muted); font-size: 0.85em; }
.content blockquote {
border-left: 3px solid var(--accent);
margin: 0.8em 0;
padding: 0.4em 1em;
color: var(--text-muted);
}
.content hr { border: none; border-top: 1px solid var(--border); margin: 2em 0; }
.loading { padding: 2em; color: var(--text-muted); }
@media (max-width: 700px) {
.sidebar { width: 160px; min-width: 160px; }
.content { padding: 1em; }
}
</style>
</head>
<body>
<img id="site-logo" src="/logo.svg" style="max-height:40px;vertical-align:middle;margin-right:0.5em" alt=""><h1 style="display:inline;vertical-align:middle">rustguac</h1>
<nav>
<a href="/addressbook.html">Address Book</a>
<a href="/sessions.html" id="sessions-link">Sessions</a>
<a href="/recordings.html">Recordings</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"><span id="user-menu-btn" title="Preferences">&#9881;</span><div id="user-menu"><div class="um-section-label">Theme</div><div id="um-theme-list"></div><div class="um-divider"></div><div class="um-item um-logout" id="logout-item">Logout</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(){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])}catch(e){}}})();
function applyThemeColors(colors){var r=document.documentElement.style;for(var k in colors)r.setProperty('--'+k.replace(/_/g,'-'),colors[k]);localStorage.setItem('rustguac_theme_colors',JSON.stringify(colors))}
var _themePresets={},_adminPreset='dark';
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',avocado:'Natural greens & warm brown'};
function initTheme(t){if(!t)return;_themePresets=t.presets||{};_adminPreset=t.admin_preset||'dark';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){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 = '';
} else {
fetch('/api/me', { credentials: 'same-origin' })
.then(function(res) {
if (!res.ok) { window.location.href = '/'; 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('sessions-link').style.display = 'none';
}
})
.catch(function() { window.location.href = '/'; });
}
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>