Add recorder-dashboard-standalone.html
This commit is contained in:
parent
4ac0f1c1f8
commit
a69e7abff8
1 changed files with 304 additions and 0 deletions
304
recorder-dashboard-standalone.html
Normal file
304
recorder-dashboard-standalone.html
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
<!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>
|
||||
Loading…
Reference in a new issue