recorder-dashboard-clean/recorder-dashboard-standalone.html

305 lines
12 KiB
HTML
Raw Permalink Normal View History

2026-03-31 15:29:51 -04:00
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Elastic Recorder Control Dashboard</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #0f172a; color: #fff; }
.container { max-width: 1200px; margin: 0 auto; padding: 24px; }
.header { margin-bottom: 32px; }
h1 { font-size: 32px; font-weight: bold; margin-bottom: 8px; }
.subtitle { color: #94a3b8; }
.login-form { max-width: 500px; background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 32px; }
.form-group { margin-bottom: 16px; }
label { display: block; font-size: 14px; font-weight: 500; color: #cbd5e1; margin-bottom: 8px; text-transform: uppercase; }
textarea, input { width: 100%; padding: 8px 12px; background: #0f172a; border: 1px solid #475569; border-radius: 4px; color: #fff; font-family: monospace; }
textarea:focus, input:focus { outline: none; border-color: #3b82f6; }
textarea { height: 100px; resize: vertical; }
button { width: 100%; padding: 10px; background: #3b82f6; color: #fff; font-weight: 500; border: none; border-radius: 4px; cursor: pointer; transition: background 0.2s; }
button:hover { background: #2563eb; }
button:disabled { background: #475569; cursor: not-allowed; opacity: 0.5; }
.alert { padding: 12px 16px; border-radius: 4px; margin-bottom: 16px; font-size: 14px; display: flex; gap: 12px; }
.alert-error { background: rgba(239, 68, 68, 0.1); border: 1px solid #dc2626; color: #fca5a5; }
.alert-success { background: rgba(34, 197, 94, 0.1); border: 1px solid #16a34a; color: #86efac; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; margin-top: 32px; }
.card { background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 24px; }
.card h2 { font-size: 18px; margin-bottom: 12px; }
.badge { display: inline-block; padding: 4px 8px; background: #1e3a8a; border: 1px solid #3b82f6; border-radius: 4px; font-size: 12px; color: #93c5fd; margin-bottom: 12px; }
.field { margin-bottom: 12px; }
.field-label { font-size: 12px; color: #64748b; text-transform: uppercase; }
.field-value { color: #e2e8f0; word-break: break-all; margin-top: 4px; }
.card-actions { display: flex; gap: 8px; margin-top: 16px; }
.btn-sm { padding: 8px 12px; font-size: 14px; flex: 1; }
.btn-edit { background: #475569; }
.btn-edit:hover { background: #64748b; }
.btn-save { background: #16a34a; }
.btn-save:hover { background: #15803d; }
.btn-cancel { background: #475569; }
.btn-cancel:hover { background: #64748b; }
.btn-logout { background: #64748b; padding: 8px 16px; }
.btn-logout:hover { background: #78909c; }
.btn-refresh { background: #64748b; padding: 8px 16px; }
.btn-refresh:hover { background: #78909c; }
.header-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
.hidden { display: none; }
.empty { text-align: center; color: #64748b; padding: 48px 0; }
input[type="text"] { font-family: sans-serif; }
</style>
</head>
<body>
<div class="container">
<div id="login" class="hidden">
<div class="header">
<h1>Elastic Recorder Control</h1>
<p class="subtitle">Manage recording settings and folder assignments</p>
</div>
<div class="login-form">
<form id="loginForm">
<div class="form-group">
<label>API Key (Base64)</label>
<textarea id="apiKey" placeholder="Paste your base64-encoded API key"></textarea>
</div>
<div class="form-group">
<label>Platform URL</label>
<input type="text" id="baseUrl" value="https://us-east-1.gvampp.com">
</div>
<div id="error" class="alert alert-error hidden"></div>
<button type="submit">Connect</button>
</form>
</div>
</div>
<div id="dashboard" class="hidden">
<div class="header">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<h1>Elastic Recorder Control</h1>
<p class="subtitle" id="channelCount">Managing channels...</p>
</div>
<div class="header-actions">
<button class="btn-refresh" onclick="refreshChannels()">↻ Refresh</button>
<button class="btn-logout" onclick="logout()">Logout</button>
</div>
</div>
</div>
<div id="alert" class="hidden"></div>
<div class="grid" id="channelsGrid">
<div class="empty">Loading channels...</div>
</div>
</div>
</div>
<script>
let token = null;
let channels = [];
let baseUrl = 'https://us-east-1.gvampp.com';
let apiKey = '';
let editing = null;
async function getToken() {
try {
const response = await fetch(`${baseUrl}/identity/connect/token`, {
method: 'POST',
headers: {
'Authorization': `Basic ${apiKey}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'grant_type=client_credentials&scope=platform'
});
if (!response.ok) throw new Error(`Auth failed: ${response.statusText}`);
const data = await response.json();
return data.access_token;
} catch (err) {
showError(`Token error: ${err.message}`);
throw err;
}
}
async function fetchChannels() {
try {
token = await getToken();
const response = await fetch(`${baseUrl}/api/store/channel/v1/channels`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!response.ok) throw new Error(`Fetch failed: ${response.statusText}`);
channels = await response.json();
document.getElementById('login').classList.add('hidden');
document.getElementById('dashboard').classList.remove('hidden');
document.getElementById('channelCount').textContent = `Managing ${channels.length} recorders`;
renderChannels();
showSuccess(`Loaded ${channels.length} channels`);
} catch (err) {
showError(err.message);
}
}
function renderChannels() {
const grid = document.getElementById('channelsGrid');
if (channels.length === 0) {
grid.innerHTML = '<div class="empty">No channels found</div>';
return;
}
grid.innerHTML = channels.map(ch => {
const isEditing = editing === ch['channel:id'];
if (isEditing) {
return `
<div class="card">
<h2>${ch['name:text']}</h2>
<p style="font-size: 12px; color: #64748b; margin-bottom: 12px;">${ch['channel:id'].substring(0, 20)}...</p>
<div class="form-group">
<label>Recording Store</label>
<input type="text" id="edit-source-${ch['channel:id']}" value="${ch['source:text'] || ''}" placeholder="/archive/store1">
</div>
<div class="form-group">
<label>Framelight Folder ID</label>
<input type="text" id="edit-dest-${ch['channel:id']}" value="${ch['destinationId:int'] || ''}" placeholder="Folder ID">
</div>
<div class="card-actions">
<button class="btn-sm btn-save" onclick="saveChannel('${ch['channel:id']}')">💾 Save</button>
<button class="btn-sm btn-cancel" onclick="cancelEdit()">Cancel</button>
</div>
</div>
`;
}
return `
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 12px;">
<h2>${ch['name:text']}</h2>
${ch['elasticRecorder'] ? '<div class="badge">Elastic Recorder</div>' : ''}
</div>
<p style="font-size: 12px; color: #64748b; margin-bottom: 12px;">${ch['channel:id'].substring(0, 20)}...</p>
<div class="field">
<div class="field-label">Recording Store</div>
<div class="field-value">${ch['source:text'] || '—'}</div>
</div>
<div class="field">
<div class="field-label">Framelight Folder</div>
<div class="field-value">${ch['destinationId:int'] || '—'}</div>
</div>
${ch['elasticRecorder'] ? `
<div class="field">
<div class="field-label">Workload ID</div>
<div class="field-value">${ch['elasticRecorder']['workloadId:text'].substring(0, 20)}...</div>
</div>
` : ''}
<div class="card-actions">
<button class="btn-sm btn-edit" onclick="startEdit('${ch['channel:id']}')">⚙️ Edit</button>
</div>
</div>
`;
}).join('');
}
function startEdit(id) {
editing = id;
renderChannels();
}
function cancelEdit() {
editing = null;
renderChannels();
}
async function saveChannel(id) {
try {
const source = document.getElementById(`edit-source-${id}`).value;
const dest = document.getElementById(`edit-dest-${id}`).value;
const response = await fetch(`${baseUrl}/api/store/channel/v1/channels/${id}`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
'source:text': source,
'destinationId:int': dest ? parseInt(dest) : null
})
});
if (!response.ok) throw new Error(`Update failed: ${response.statusText}`);
editing = null;
showSuccess('Channel updated successfully');
setTimeout(() => fetchChannels(), 500);
} catch (err) {
showError(err.message);
}
}
function refreshChannels() {
fetchChannels();
}
function logout() {
token = null;
channels = [];
editing = null;
document.getElementById('login').classList.remove('hidden');
document.getElementById('dashboard').classList.add('hidden');
document.getElementById('apiKey').value = '';
}
function showError(msg) {
const el = document.getElementById('error') || document.getElementById('alert');
el.className = 'alert alert-error';
el.textContent = '✕ ' + msg;
el.classList.remove('hidden');
setTimeout(() => el.classList.add('hidden'), 4000);
}
function showSuccess(msg) {
const el = document.getElementById('alert');
el.className = 'alert alert-success';
el.textContent = '✓ ' + msg;
el.classList.remove('hidden');
setTimeout(() => el.classList.add('hidden'), 3000);
}
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
apiKey = document.getElementById('apiKey').value;
baseUrl = document.getElementById('baseUrl').value;
if (!apiKey) {
showError('Please enter API key');
return;
}
await fetchChannels();
});
// Show login on load
document.getElementById('login').classList.remove('hidden');
</script>
</body>
</html>