mirror of
https://github.com/sol1/rustguac.git
synced 2026-09-10 09:35:45 +00:00
fb0f965bb2
Logout link moved inside the float-right user-menu-wrapper so both Logout and Settings appear on the far right of the nav bar. Fixed addressbook.html Settings font size to match other pages (1.3em).
553 lines
28 KiB
HTML
553 lines
28 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>rustguac - Recordings</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; --type-vdi-bg: #0e2a2a; --type-vdi-fg: #2dd4bf; --hop-bg: #1b4332; --hop-fg: #52b788; --bg-pattern: none; }
|
|
body { font-family: monospace; background-color: var(--bg); background-image: var(--bg-pattern); background-attachment: fixed; min-height: 100vh; color: var(--text); padding: 2em; font-size: 18px; }
|
|
h1 { color: var(--primary); }
|
|
a { color: var(--accent); }
|
|
nav { margin-top: 0.8em; margin-bottom: 1.5em; padding-bottom: 0.8em; border-bottom: 1px solid var(--border); font-size: 0.95em; }
|
|
nav a { margin-right: 1.5em; text-decoration: none; }
|
|
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); }
|
|
|
|
table { border-collapse: collapse; width: 100%; max-width: 1200px; }
|
|
th, td { text-align: left; padding: 0.4em 0.8em; border-bottom: 1px solid var(--border); }
|
|
th { color: var(--text-muted); font-size: 0.85em; }
|
|
.btn-small {
|
|
background: none; border: none; color: var(--accent);
|
|
cursor: pointer; font-family: monospace; padding: 0; font-size: 0.9em;
|
|
}
|
|
.btn-small:hover { text-decoration: underline; }
|
|
.btn-delete { color: var(--primary); }
|
|
.empty { color: var(--text-dim); margin: 2em 0; }
|
|
|
|
#player-section {
|
|
display: none;
|
|
margin: 1em 0 1.5em 0;
|
|
background: var(--surface);
|
|
border-radius: 6px;
|
|
padding: 1em;
|
|
max-width: 1100px;
|
|
}
|
|
#player-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 0.5em;
|
|
}
|
|
#player-title { color: var(--text-muted); font-size: 0.9em; }
|
|
#player-close {
|
|
background: none; border: none; color: #888; cursor: pointer;
|
|
font-family: monospace; font-size: 1.1em;
|
|
}
|
|
#player-close:hover { color: var(--primary); }
|
|
#player-display {
|
|
background: #000;
|
|
overflow: auto;
|
|
max-height: 70vh;
|
|
}
|
|
#player-controls {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.8em;
|
|
margin-top: 0.5em;
|
|
padding: 0.4em 0;
|
|
}
|
|
#play-btn {
|
|
background: var(--primary); color: #fff; border: none;
|
|
font-family: monospace; font-size: 0.9em; padding: 0.3em 1em;
|
|
border-radius: 3px; cursor: pointer; min-width: 60px;
|
|
}
|
|
#play-btn:hover { background: var(--primary-hover); }
|
|
#seek-container {
|
|
position: relative;
|
|
flex: 1;
|
|
height: 32px;
|
|
}
|
|
#histogram {
|
|
position: absolute;
|
|
top: 0; left: 0;
|
|
width: 100%; height: 100%;
|
|
border-radius: 3px;
|
|
}
|
|
#seek-slider {
|
|
position: absolute;
|
|
top: 0; left: 0;
|
|
width: 100%; height: 100%;
|
|
z-index: 2;
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
background: transparent;
|
|
outline: none;
|
|
margin: 0;
|
|
}
|
|
#seek-slider::-webkit-slider-runnable-track {
|
|
height: 4px;
|
|
background: rgba(255,255,255,0.1);
|
|
border-radius: 2px;
|
|
}
|
|
#seek-slider::-moz-range-track {
|
|
height: 4px;
|
|
background: rgba(255,255,255,0.1);
|
|
border-radius: 2px;
|
|
}
|
|
#seek-slider::-webkit-slider-thumb {
|
|
-webkit-appearance: none;
|
|
width: 12px; height: 12px;
|
|
background: var(--accent); border-radius: 50%; cursor: pointer;
|
|
margin-top: -4px;
|
|
}
|
|
#seek-slider::-moz-range-thumb {
|
|
width: 12px; height: 12px;
|
|
background: var(--accent); border-radius: 50%; cursor: pointer; border: none;
|
|
}
|
|
#player-time { color: var(--text-muted); font-size: 0.85em; white-space: nowrap; }
|
|
</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>
|
|
|
|
<!-- Guacamole JS modules (same as client.html) -->
|
|
<script src="/guac/Namespace.js"></script>
|
|
<script src="/guac/Client.js"></script>
|
|
<script src="/guac/Display.js"></script>
|
|
<script src="/guac/Event.js"></script>
|
|
<script src="/guac/InputSink.js"></script>
|
|
<script src="/guac/IntegerPool.js"></script>
|
|
<script src="/guac/Keyboard.js"></script>
|
|
<script src="/guac/KeyEventInterpreter.js"></script>
|
|
<script src="/guac/Layer.js"></script>
|
|
<script src="/guac/Mouse.js"></script>
|
|
<script src="/guac/Parser.js"></script>
|
|
<script src="/guac/Position.js"></script>
|
|
<script src="/guac/Status.js"></script>
|
|
<script src="/guac/Touch.js"></script>
|
|
<script src="/guac/Tunnel.js"></script>
|
|
<script src="/guac/UTF8Parser.js"></script>
|
|
<script src="/guac/InputStream.js"></script>
|
|
<script src="/guac/OutputStream.js"></script>
|
|
<script src="/guac/StringReader.js"></script>
|
|
<script src="/guac/StringWriter.js"></script>
|
|
<script src="/guac/ArrayBufferReader.js"></script>
|
|
<script src="/guac/ArrayBufferWriter.js"></script>
|
|
<script src="/guac/BlobReader.js"></script>
|
|
<script src="/guac/BlobWriter.js"></script>
|
|
<script src="/guac/DataURIReader.js"></script>
|
|
<script src="/guac/AudioContextFactory.js"></script>
|
|
<script src="/guac/AudioPlayer.js"></script>
|
|
<script src="/guac/AudioRecorder.js"></script>
|
|
<script src="/guac/VideoPlayer.js"></script>
|
|
<script src="/guac/JSONReader.js"></script>
|
|
<script src="/guac/Object.js"></script>
|
|
<script src="/guac/RawAudioFormat.js"></script>
|
|
<script src="/guac/SessionRecording.js"></script>
|
|
</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" class="active">Recordings</a>
|
|
<a href="/reports.html" id="reports-link" style="display:none">Reports</a>
|
|
<a href="/docs.html">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" style="color:var(--text-muted);text-decoration:none;margin-right:1em">Logout</a><span id="user-menu-btn" title="Preferences">⚙ Settings</span><div id="user-menu"><div class="um-section-label">Theme</div><div id="um-theme-list"></div></div></span>
|
|
</nav>
|
|
|
|
<strong>Recordings</strong>
|
|
|
|
<div id="player-section">
|
|
<div id="player-header">
|
|
<span id="player-title"></span>
|
|
<button id="player-close">✕ close</button>
|
|
</div>
|
|
<div id="player-display"></div>
|
|
<div id="player-controls">
|
|
<button id="play-btn">Play</button>
|
|
<div id="seek-container">
|
|
<canvas id="histogram"></canvas>
|
|
<input type="range" id="seek-slider" min="0" max="1000" value="0">
|
|
</div>
|
|
<span id="player-time">0:00 / 0:00</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="max-width:1200px;margin-bottom:1em">
|
|
<input type="text" id="rec-search" placeholder="Search recordings (user, entry, folder...)" style="width:100%;padding:0.5em;background:var(--input);border:1px solid var(--border);color:var(--text);font-family:monospace;font-size:1em;border-radius:3px;box-sizing:border-box">
|
|
</div>
|
|
<div id="recording-list"></div>
|
|
|
|
<script>
|
|
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='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',aurora:'Midnight blue with ambient glow',jaguar:'Racing green & gold'};
|
|
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){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+' - Recordings';document.querySelector('h1').textContent=d.site_title;}
|
|
initTheme(d.theme);
|
|
});
|
|
var apiKey = sessionStorage.getItem('rustguac_api_key');
|
|
|
|
// Check auth: either API key or cookie
|
|
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 = '/'; });
|
|
});
|
|
|
|
function apiHeaders() {
|
|
var h = {};
|
|
if (apiKey) h['Authorization'] = 'Bearer ' + apiKey;
|
|
return h;
|
|
}
|
|
|
|
function formatBytes(bytes) {
|
|
if (bytes < 1024) return bytes + ' B';
|
|
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
|
return (bytes / 1048576).toFixed(1) + ' MB';
|
|
}
|
|
|
|
function formatTime(ms) {
|
|
var secs = Math.floor(ms / 1000);
|
|
var m = Math.floor(secs / 60);
|
|
var s = secs % 60;
|
|
return m + ':' + (s < 10 ? '0' : '') + s;
|
|
}
|
|
|
|
function escapeHtml(s) {
|
|
var d = document.createElement('div');
|
|
d.textContent = s;
|
|
return d.innerHTML;
|
|
}
|
|
|
|
function escapeAttr(s) {
|
|
return s.replace(/&/g,'&').replace(/"/g,'"').replace(/'/g,''').replace(/</g,'<').replace(/>/g,'>');
|
|
}
|
|
|
|
function refreshRecordings() {
|
|
fetch('/api/recordings', { headers: apiHeaders(), credentials: 'same-origin' })
|
|
.then(function(res) {
|
|
if (!res.ok) throw new Error('Failed to load recordings');
|
|
return res.json();
|
|
})
|
|
.then(function(recordings) {
|
|
allRecordings = recordings;
|
|
renderRecordings(recordings);
|
|
});
|
|
}
|
|
|
|
var allRecordings = [];
|
|
var recSortCol = 'modified';
|
|
var recSortAsc = false;
|
|
|
|
document.getElementById('rec-search').addEventListener('input', function() {
|
|
var q = this.value.toLowerCase();
|
|
if (!q) { renderRecordings(allRecordings); return; }
|
|
var filtered = allRecordings.filter(function(r) {
|
|
return (r.name || '').toLowerCase().indexOf(q) >= 0 ||
|
|
(r.user || '').toLowerCase().indexOf(q) >= 0 ||
|
|
(r.entry_display_name || '').toLowerCase().indexOf(q) >= 0 ||
|
|
(r.address_book_entry || '').toLowerCase().indexOf(q) >= 0 ||
|
|
(r.folder || '').toLowerCase().indexOf(q) >= 0 ||
|
|
(r.session_type || '').toLowerCase().indexOf(q) >= 0;
|
|
});
|
|
renderRecordings(filtered);
|
|
});
|
|
|
|
function sortRecordings(recs, col, asc) {
|
|
return recs.slice().sort(function(a, b) {
|
|
var va = a[col] || '', vb = b[col] || '';
|
|
if (col === 'size_bytes') { va = va || 0; vb = vb || 0; }
|
|
if (va < vb) return asc ? -1 : 1;
|
|
if (va > vb) return asc ? 1 : -1;
|
|
return 0;
|
|
});
|
|
}
|
|
|
|
function renderRecordings(recordings) {
|
|
var container = document.getElementById('recording-list');
|
|
if (recordings.length === 0) {
|
|
container.innerHTML = '<p class="empty">No recordings found.</p>';
|
|
return;
|
|
}
|
|
var sorted = sortRecordings(recordings, recSortCol, recSortAsc);
|
|
function sortHeader(label, col) {
|
|
var arrow = recSortCol === col ? (recSortAsc ? ' ▲' : ' ▼') : '';
|
|
return '<th style="cursor:pointer" data-sort-col="' + col + '">' + label + arrow + '</th>';
|
|
}
|
|
var html = '<table><thead><tr>' +
|
|
sortHeader('Entry', 'entry_display_name') +
|
|
sortHeader('User', 'user') +
|
|
sortHeader('Type', 'session_type') +
|
|
sortHeader('Folder', 'folder') +
|
|
sortHeader('Size', 'size_bytes') +
|
|
sortHeader('Date', 'modified') +
|
|
'<th></th><th></th><th></th>' +
|
|
'</tr></thead><tbody>';
|
|
sorted.forEach(function(r) {
|
|
var date = r.modified ? new Date(r.modified).toLocaleString() : (r.created_at ? new Date(r.created_at).toLocaleString() : '');
|
|
var entryName = r.entry_display_name || r.address_book_entry || r.name;
|
|
var typeBadge = r.session_type ? '<span style="color:var(--type-' + r.session_type + '-fg)">' + r.session_type + '</span>' : '';
|
|
html += '<tr>' +
|
|
'<td title="' + escapeAttr(r.name) + '">' + escapeHtml(entryName) + '</td>' +
|
|
'<td>' + escapeHtml(r.user || '') + '</td>' +
|
|
'<td>' + typeBadge + '</td>' +
|
|
'<td>' + escapeHtml(r.folder || '') + '</td>' +
|
|
'<td>' + formatBytes(r.size_bytes) + '</td>' +
|
|
'<td>' + date + '</td>' +
|
|
'<td><button class="btn-small" data-play="' + escapeAttr(r.name) + '">play</button></td>' +
|
|
'<td><a class="btn-small" href="/api/recordings/' + encodeURIComponent(r.name) + '" download="' + escapeAttr(r.name) + '">download</a></td>' +
|
|
'<td><button class="btn-small btn-delete" data-delete-rec="' + escapeAttr(r.name) + '">delete</button></td>' +
|
|
'</tr>';
|
|
});
|
|
html += '</tbody></table>';
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
document.getElementById('recording-list').addEventListener('click', function(e) {
|
|
if (e.target.getAttribute('data-sort-col')) {
|
|
var col = e.target.getAttribute('data-sort-col');
|
|
if (recSortCol === col) { recSortAsc = !recSortAsc; }
|
|
else { recSortCol = col; recSortAsc = true; }
|
|
renderRecordings(allRecordings.filter(function(r) {
|
|
var q = document.getElementById('rec-search').value.toLowerCase();
|
|
if (!q) return true;
|
|
return (r.name || '').toLowerCase().indexOf(q) >= 0 ||
|
|
(r.user || '').toLowerCase().indexOf(q) >= 0 ||
|
|
(r.entry_display_name || '').toLowerCase().indexOf(q) >= 0 ||
|
|
(r.folder || '').toLowerCase().indexOf(q) >= 0;
|
|
}));
|
|
return;
|
|
}
|
|
});
|
|
|
|
// Handle clicks on recording list
|
|
document.getElementById('recording-list').addEventListener('click', function(e) {
|
|
var playName = e.target.getAttribute('data-play');
|
|
if (playName) {
|
|
playRecording(playName);
|
|
return;
|
|
}
|
|
var deleteName = e.target.getAttribute('data-delete-rec');
|
|
if (deleteName) {
|
|
if (!confirm('Delete recording ' + deleteName + '?')) return;
|
|
fetch('/api/recordings/' + encodeURIComponent(deleteName), {
|
|
method: 'DELETE',
|
|
headers: apiHeaders(),
|
|
credentials: 'same-origin'
|
|
}).then(function() {
|
|
refreshRecordings();
|
|
// Close player if this recording is playing
|
|
if (document.getElementById('player-title').textContent === deleteName) {
|
|
closePlayer();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// Download link needs auth header — intercept clicks
|
|
document.getElementById('recording-list').addEventListener('click', function(e) {
|
|
if (e.target.tagName === 'A' && e.target.hasAttribute('download')) {
|
|
e.preventDefault();
|
|
var url = e.target.href;
|
|
var filename = e.target.getAttribute('download');
|
|
fetch(url, { headers: apiHeaders(), credentials: 'same-origin' })
|
|
.then(function(res) { return res.blob(); })
|
|
.then(function(blob) {
|
|
var a = document.createElement('a');
|
|
a.href = URL.createObjectURL(blob);
|
|
a.download = filename;
|
|
a.click();
|
|
URL.revokeObjectURL(a.href);
|
|
});
|
|
}
|
|
});
|
|
|
|
var currentRecording = null;
|
|
|
|
function closePlayer() {
|
|
if (currentRecording) {
|
|
currentRecording.pause();
|
|
currentRecording = null;
|
|
}
|
|
document.getElementById('player-display').innerHTML = '';
|
|
document.getElementById('player-section').style.display = 'none';
|
|
document.getElementById('play-btn').textContent = 'Play';
|
|
document.getElementById('seek-slider').value = 0;
|
|
document.getElementById('player-time').textContent = '0:00 / 0:00';
|
|
}
|
|
|
|
document.getElementById('player-close').addEventListener('click', closePlayer);
|
|
|
|
function playRecording(name) {
|
|
closePlayer();
|
|
|
|
document.getElementById('player-title').textContent = name;
|
|
var playerSection = document.getElementById('player-section');
|
|
playerSection.style.display = 'block';
|
|
playerSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
|
|
var headers = {};
|
|
if (apiKey) headers['Authorization'] = 'Bearer ' + apiKey;
|
|
var tunnel = new Guacamole.StaticHTTPTunnel(
|
|
'/api/recordings/' + encodeURIComponent(name),
|
|
false,
|
|
headers
|
|
);
|
|
var recording = new Guacamole.SessionRecording(tunnel);
|
|
currentRecording = recording;
|
|
|
|
var displayEl = document.getElementById('player-display');
|
|
var playBtn = document.getElementById('play-btn');
|
|
var seekSlider = document.getElementById('seek-slider');
|
|
var timeEl = document.getElementById('player-time');
|
|
|
|
var display = recording.getDisplay();
|
|
displayEl.appendChild(display.getElement());
|
|
|
|
// Auto-scale VNC recordings to fit the player container
|
|
function scaleRecordingDisplay() {
|
|
var w = display.getWidth();
|
|
var h = display.getHeight();
|
|
if (w > 0 && h > 0) {
|
|
var containerWidth = displayEl.clientWidth || 1080;
|
|
var scale = Math.min(containerWidth / w, 1);
|
|
display.scale(scale);
|
|
}
|
|
}
|
|
display.onresize = function() {
|
|
scaleRecordingDisplay();
|
|
};
|
|
|
|
var duration = 0;
|
|
var currentPos = 0;
|
|
var seeking = false;
|
|
|
|
function rebuildHistogram() {
|
|
var frameData = recording.getFrames();
|
|
if (!frameData.length || duration <= 0) return;
|
|
|
|
var canvas = document.getElementById('histogram');
|
|
var rect = canvas.parentElement.getBoundingClientRect();
|
|
canvas.width = rect.width * devicePixelRatio;
|
|
canvas.height = rect.height * devicePixelRatio;
|
|
|
|
var ctx = canvas.getContext('2d');
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
var bucketCount = Math.min(Math.floor(rect.width / 2), 200);
|
|
if (bucketCount <= 0) return;
|
|
var buckets = new Array(bucketCount).fill(0);
|
|
var bucketDuration = duration / bucketCount;
|
|
|
|
for (var i = 0; i < frameData.length; i++) {
|
|
var idx = Math.min(Math.floor(frameData[i].timestamp / bucketDuration), bucketCount - 1);
|
|
if (idx >= 0) buckets[idx] += frameData[i].size;
|
|
}
|
|
|
|
var sorted = buckets.slice().sort(function(a, b) { return a - b; });
|
|
var maxVal = sorted[Math.floor(sorted.length * 0.95)] || 1;
|
|
|
|
var barWidth = canvas.width / bucketCount;
|
|
for (var i = 0; i < bucketCount; i++) {
|
|
var h = Math.min(buckets[i] / maxVal, 1) * canvas.height * 0.85;
|
|
ctx.fillStyle = 'rgba(91, 192, 190, 0.35)';
|
|
ctx.fillRect(i * barWidth, canvas.height - h, barWidth - 0.5, h);
|
|
}
|
|
}
|
|
|
|
recording.onprogress = function(millis) {
|
|
duration = millis;
|
|
timeEl.textContent = formatTime(currentPos) + ' / ' + formatTime(duration);
|
|
rebuildHistogram();
|
|
};
|
|
|
|
recording.onplay = function() {
|
|
playBtn.textContent = 'Pause';
|
|
};
|
|
|
|
recording.onpause = function() {
|
|
playBtn.textContent = 'Play';
|
|
};
|
|
|
|
recording.onseek = function(position) {
|
|
currentPos = position;
|
|
if (!seeking && duration > 0) {
|
|
seekSlider.value = Math.round((position / duration) * 1000);
|
|
}
|
|
timeEl.textContent = formatTime(position) + ' / ' + formatTime(duration);
|
|
};
|
|
|
|
playBtn.onclick = function() {
|
|
if (recording.isPlaying()) {
|
|
recording.pause();
|
|
} else {
|
|
recording.play();
|
|
}
|
|
};
|
|
|
|
seekSlider.addEventListener('mousedown', function() { seeking = true; });
|
|
seekSlider.addEventListener('mouseup', function() { seeking = false; });
|
|
seekSlider.addEventListener('input', function() {
|
|
if (duration > 0) {
|
|
var target = (seekSlider.value / 1000) * duration;
|
|
recording.seek(target);
|
|
}
|
|
});
|
|
|
|
// Connect to load the recording data, then auto-play
|
|
recording.connect();
|
|
}
|
|
|
|
refreshRecordings();
|
|
// Don't auto-refresh recordings as aggressively as sessions
|
|
setInterval(refreshRecordings, 15000);
|
|
</script>
|
|
</body>
|
|
</html>
|