{% extends "console/layout.html" %}
{% block title %}{{ "dashboard.title" | t }} ยท {{site_name|default('RustPBX')}}{% endblock %}
{% block content %}
<div class="min-h-[70vh] py-6">
<div class="mx-auto max-w-7xl px-4">
<div class="mb-6 rounded-2xl bg-white p-5 shadow-sm ring-1 ring-black/5">
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-end">
<div class="flex items-center gap-3">
<label for="time-range" class="text-xs font-medium uppercase tracking-wide text-slate-500">{{ "dashboard.time_range" | t }}</label>
<div class="relative w-48">
<select id="time-range" name="time-range"
class="block w-full appearance-none rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm text-slate-700 focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200">
<option value="10m" {% if range.key=='10m' %} selected{% endif %}>{{ "dashboard.last_10_minutes" | t }}</option>
<option value="today" {% if range.key=='today' %} selected{% endif %}>{{ "dashboard.today" | t }}</option>
<option value="yesterday" {% if range.key=='yesterday' %} selected{% endif %}>{{ "dashboard.yesterday" | t }}
</option>
<option value="week" {% if range.key=='week' %} selected{% endif %}>{{ "dashboard.this_week" | t }}</option>
<option value="7days" {% if range.key=='7days' %} selected{% endif %}>{{ "dashboard.last_7_days" | t }}</option>
<option value="30days" {% if range.key=='30days' %} selected{% endif %}>{{ "dashboard.last_30_days" | t }}
</option>
</select>
<span class="pointer-events-none absolute inset-y-0 right-3 flex items-center text-slate-400">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"
class="h-4 w-4">
<path fill-rule="evenodd"
d="M5.23 7.21a.75.75 0 011.06.02L10 10.939l3.71-3.71a.75.75 0 111.06 1.062l-4.24 4.242a.75.75 0 01-1.06 0L5.21 8.29a.75.75 0 01.02-1.08z"
clip-rule="evenodd" />
</svg>
</span>
</div>
</div>
</div>
</div>
<section class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<div class="rounded-xl bg-white p-5 ring-1 ring-black/5">
<div class="text-xs text-slate-500">{{ "dashboard.calls_in" | t }} <span data-range-label>{{ range.period_label_short |
default("dashboard.last_10_minutes" | t) }}</span></div>
<div class="mt-2 flex items-end justify-between">
<div id="metric-total" class="text-3xl font-semibold">{{ metrics.recent10.total | default(0) }}
</div>
<div id="metric-trend" class="text-xs text-emerald-600">{{ metrics.recent10.trend | default('+0%')
}}</div>
</div>
<div class="mt-3 h-2 w-full overflow-hidden rounded bg-slate-100">
<div id="metric-util-bar" class="h-full bg-sky-500"
style="--v: {{ metrics.recent10.util | default(0) }}; width: calc(var(--v) * 1%);"></div>
</div>
</div>
<div class="rounded-xl bg-white p-5 ring-1 ring-black/5">
<div class="text-xs text-slate-500">{{ "dashboard.answered_in" | t }} <span data-range-label>{{ range.period_label_short |
default("dashboard.last_10_minutes" | t) }}</span></div>
<div class="mt-2 flex items-end justify-between">
<div id="metric-answered" class="text-3xl font-semibold">{{ metrics.recent10.answered | default(0)
}}</div>
<div id="metric-asr" class="text-xs text-emerald-600">{{ metrics.recent10.asr | default('โ') }}
</div>
</div>
<div class="mt-3 h-2 w-full overflow-hidden rounded bg-slate-100">
<div id="metric-ans-util-bar" class="h-full bg-emerald-500"
style="--v: {{ metrics.recent10.ans_util | default(0) }}; width: calc(var(--v) * 1%);"></div>
</div>
</div>
<div class="rounded-xl bg-white p-5 ring-1 ring-black/5">
<div class="text-xs text-slate-500">{{ "dashboard.avg_call_duration" | t }} (<span data-range-label>{{
range.period_label_short | default("dashboard.last_10_minutes" | t) }}</span>)</div>
<div class="mt-2 flex items-end justify-between">
<div id="metric-acd" class="text-3xl font-semibold">{{ metrics.recent10.acd | default('0s') }}</div>
<div class="text-xs text-slate-500"><span id="metric-today-acd">{{ metrics.today.acd | default('0s')
}}</span> {{ "dashboard.today" | t }}</div>
</div>
<div class="mt-3 h-2 w-full overflow-hidden rounded bg-slate-100">
<div id="metric-acd-util-bar" class="h-full bg-indigo-500"
style="--v: {{ metrics.recent10.acd_util | default(0) }}; width: calc(var(--v) * 1%);"></div>
</div>
</div>
<div class="rounded-xl bg-white p-5 ring-1 ring-black/5">
<div class="text-xs text-slate-500">{{ "dashboard.active" | t }} {{ "dashboard.calls_in" | t }}</div>
<div class="mt-2 flex items-end justify-between">
<div id="metric-active" class="text-3xl font-semibold">{{ metrics.active | default(0) }}</div>
<div class="text-xs text-slate-500">{{ "dashboard.capacity" | t }} <span id="metric-capacity">{{ metrics.capacity |
default(0) }}</span></div>
</div>
<div class="mt-3 h-2 w-full overflow-hidden rounded bg-slate-100">
<div id="metric-active-util-bar" class="h-full bg-amber-500"
style="--v: {{ metrics.active_util | default(0) }}; width: calc(var(--v) * 1%);"></div>
</div>
</div>
</section>
<section class="mt-4 rounded-xl bg-white p-5 ring-1 ring-black/5">
<div class="flex items-center justify-between">
<h2 class="text-xs font-semibold uppercase tracking-wide text-slate-500">Transaction Pressure</h2>
<span class="text-xs text-slate-400" id="tx-pressure-updated"></span>
</div>
<div class="mt-3 grid gap-3 sm:grid-cols-3">
<div class="rounded-lg bg-slate-50 p-4">
<div class="text-xs text-slate-500">Running Transactions</div>
<div class="mt-1 flex items-baseline gap-1">
<span id="tx-running" class="text-2xl font-semibold text-slate-700">{{ metrics.transaction_running | default(0) }}</span>
{% if metrics.transaction_capacity > 0 %}
<span id="tx-capacity" class="text-xs text-slate-400">/ {{ metrics.transaction_capacity }}</span>
{% endif %}
</div>
<div class="mt-2 h-1.5 w-full overflow-hidden rounded bg-slate-200">
<div id="tx-running-bar" class="h-full bg-purple-500"
style="width: {% if metrics.transaction_capacity > 0 %}{{ (metrics.transaction_running / metrics.transaction_capacity * 100) | round(0) }}{% else %}0{% endif %}%"></div>
</div>
</div>
<div class="rounded-lg bg-slate-50 p-4">
<div class="text-xs text-slate-500">Active Calls</div>
<div class="mt-1 flex items-baseline gap-1">
<span id="tx-active-calls" class="text-2xl font-semibold text-slate-700">{{ metrics.active | default(0) }}</span>
{% if metrics.capacity > 0 %}
<span id="tx-call-capacity" class="text-xs text-slate-400">/ {{ metrics.capacity }}</span>
{% endif %}
</div>
<div class="mt-2 h-1.5 w-full overflow-hidden rounded bg-slate-200">
<div id="tx-active-calls-bar" class="h-full bg-amber-500"
style="width: {% if metrics.capacity > 0 %}{{ (metrics.active / metrics.capacity * 100) | round(0) }}{% else %}0{% endif %}%"></div>
</div>
</div>
<div class="rounded-lg bg-slate-50 p-4">
<div class="text-xs text-slate-500">Concurrency Utilization</div>
<div class="mt-1">
<span id="tx-utilization" class="text-2xl font-semibold text-slate-700">
{% if metrics.transaction_running > 0 and metrics.transaction_capacity > 0 %}
{{ (metrics.transaction_running / metrics.transaction_capacity * 100) | round(0) }}%
{% elif metrics.active_util > 0 %}{{ metrics.active_util }}%
{% else %}0%
{% endif %}
</span>
</div>
<div class="mt-2 flex items-center gap-3 text-xs text-slate-500">
<span>Trans: <span id="tx-util-pct">{% if metrics.transaction_capacity > 0 %}{{ (metrics.transaction_running / metrics.transaction_capacity * 100) | round(0) }}{% else %}0{% endif %}%</span></span>
<span>Call: {{ metrics.active_util | default(0) }}%</span>
</div>
</div>
</div>
</section>
<section class="mt-6 grid gap-6 lg:grid-cols-3">
<div class="lg:col-span-2 rounded-2xl bg-white p-6 ring-1 ring-black/5">
<div class="flex items-center justify-between">
<h2 id="timeline-title" class="text-sm font-semibold text-slate-600">{{ range.timeline_title |
default("dashboard.last_10_minutes_timeline" | t) }}</h2>
</div>
<div class="mt-4">
<canvas id="timelineChart" class="h-40 w-full"></canvas>
</div>
</div>
<div class="rounded-2xl bg-white p-6 ring-1 ring-black/5">
<div class="flex items-center justify-between">
<h2 class="text-sm font-semibold text-slate-600">{{ "dashboard.call_direction" | t }}</h2>
</div>
<div class="mt-4">
<canvas id="callDirectionChart" class="h-44 w-full"></canvas>
</div>
</div>
</section>
{% set active_calls_preview = active_calls | default([]) %}
<section class="mt-6 rounded-2xl bg-white p-6 shadow-sm ring-1 ring-black/5">
<div class="flex flex-col gap-2 sm:flex-row sm:items-baseline sm:justify-between">
<div>
<h2 class="text-sm font-semibold text-slate-600">{{ "dashboard.active_calls_title" | t }} ยท {{ "dashboard.latest_10" | t }}</h2>
<p class="text-xs text-slate-500">{{ "dashboard.live_preview" | t }}</p>
</div>
<span id="active-updated" class="text-xs text-slate-400">{{ "dashboard.updated_just_now" | t }}</span>
</div>
<div class="mt-4 overflow-x-auto">
<table class="min-w-full divide-y divide-slate-100 text-sm">
<thead class="bg-slate-50">
<tr>
<th scope="col" class="px-3 py-2 text-left font-medium text-slate-500">{{ "dashboard.caller" | t }}</th>
<th scope="col" class="px-3 py-2 text-left font-medium text-slate-500">{{ "dashboard.callee" | t }}</th>
<th scope="col" class="px-3 py-2 text-left font-medium text-slate-500">{{ "dashboard.status" | t }}</th>
<th scope="col" class="px-3 py-2 text-left font-medium text-slate-500">{{ "dashboard.started" | t }}</th>
<th scope="col" class="px-3 py-2 text-left font-medium text-slate-500">{{ "dashboard.duration" | t }}</th>
<th scope="col" class="px-3 py-2 text-left font-medium text-slate-500">{{ "dashboard.action" | t }}</th>
</tr>
</thead>
<tbody id="active-calls-body" class="divide-y divide-slate-100">
{% for call in active_calls_preview %}
<tr class="hover:bg-slate-50">
<td class="px-3 py-2 font-medium text-slate-700">{{ call.caller }}</td>
<td class="px-3 py-2 text-slate-600">{{ call.callee }}</td>
<td class="px-3 py-2">
<span
class="inline-flex items-center gap-1 rounded-full bg-slate-100 px-2 py-1 text-xs font-medium text-slate-600">
<span class="h-1.5 w-1.5 rounded-full bg-emerald-500"></span>
{{ call.status }}
</span>
</td>
<td class="px-3 py-2 text-slate-500">{{ call.started_at }}</td>
<td class="px-3 py-2 text-slate-500">{{ call.duration }}</td>
<td class="px-3 py-2 text-slate-500">
<button onclick="hangupCall('{{ call.session_id }}')"
class="rounded bg-red-50 px-2 py-1 text-xs font-medium text-red-600 hover:bg-red-100">
{{ "dashboard.hangup" | t }}
</button>
</td>
</tr>
{% else %}
<tr>
<td colspan="6" class="px-3 py-6 text-center text-sm text-slate-400">{{ "dashboard.no_active_calls" | t }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
</div>
</div>
{% set timeline_labels = metrics.recent10.timeline_labels | default([]) %}
{% set timeline_series = metrics.recent10.timeline | default([2, 5, 7, 4, 9, 6, 3, 5, 8, 4]) %}
{% set call_direction_series = call_direction | default({"Inbound": 28, "Outbound": 17, "Internal": 9}) %}
<div id="dashboard-bootstrap" class="hidden"
data-payload='{{ {"range": range, "metrics": metrics, "call_direction": call_direction, "active_calls": active_calls_preview} | tojson | safe }}'
data-endpoint="{{ api_prefix | safe }}/dashboard/data"></div>
<script defer
src="{{chart_js|default('https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.5.0/chart.umd.min.js')|safe}}"></script>
<script type="application/json" id="__dashboardT">{{ (t.dashboard | default({})) | tojson | safe }}</script>
<script>
function initDashboardPage() {
const dashboardT = JSON.parse(document.getElementById('__dashboardT').textContent || '{}');
const bootstrapNode = document.getElementById('dashboard-bootstrap');
let payload = null;
let endpoint = null;
if (bootstrapNode) {
endpoint = bootstrapNode.dataset.endpoint || null;
try {
payload = JSON.parse(bootstrapNode.dataset.payload || '{}');
} catch (err) {
payload = null;
}
}
const bootstrap = { payload, endpoint };
const rangeSelect = document.getElementById('time-range');
const elements = {
total: document.getElementById('metric-total'),
trend: document.getElementById('metric-trend'),
utilBar: document.getElementById('metric-util-bar'),
answered: document.getElementById('metric-answered'),
asr: document.getElementById('metric-asr'),
ansUtilBar: document.getElementById('metric-ans-util-bar'),
acd: document.getElementById('metric-acd'),
todayAcd: document.getElementById('metric-today-acd'),
acdUtilBar: document.getElementById('metric-acd-util-bar'),
active: document.getElementById('metric-active'),
capacity: document.getElementById('metric-capacity'),
activeUtilBar: document.getElementById('metric-active-util-bar'),
timelineTitle: document.getElementById('timeline-title'),
activeBody: document.getElementById('active-calls-body'),
activeUpdated: document.getElementById('active-updated'),
txRunning: document.getElementById('tx-running'),
txCapacity: document.getElementById('tx-capacity'),
txRunningBar: document.getElementById('tx-running-bar'),
txActiveCalls: document.getElementById('tx-active-calls'),
txCallCapacity: document.getElementById('tx-call-capacity'),
txActiveCallsBar: document.getElementById('tx-active-calls-bar'),
txUtilization: document.getElementById('tx-utilization'),
txUtilPct: document.getElementById('tx-util-pct'),
};
const rangeLabels = document.querySelectorAll('[data-range-label]');
const ctxTimeline = document.getElementById('timelineChart');
const ctxDirection = document.getElementById('callDirectionChart');
let timelineChart = null;
let directionChart = null;
function localizedRangeLabel(range) {
switch (range?.key) {
case 'today':
return dashboardT.today || range?.period_label_short || 'Today';
case 'yesterday':
return dashboardT.yesterday || range?.period_label_short || 'Yesterday';
case 'week':
return dashboardT.this_week || range?.period_label_short || 'This week';
case '7days':
return dashboardT.last_7_days || range?.period_label_short || 'Last 7 days';
case '30days':
return dashboardT.last_30_days || range?.period_label_short || 'Last 30 days';
default:
return dashboardT.last_10_minutes || range?.period_label_short || 'Last 10 minutes';
}
}
function localizedTimelineTitle(range) {
switch (range?.key) {
case 'today':
return `${dashboardT.today || 'Today'} ${dashboardT.timeline || 'Timeline'}`;
case 'yesterday':
return `${dashboardT.yesterday || 'Yesterday'} ${dashboardT.timeline || 'Timeline'}`;
case 'week':
return `${dashboardT.this_week || 'This week'} ${dashboardT.timeline || 'Timeline'}`;
case '7days':
return `${dashboardT.last_7_days || 'Last 7 days'} ${dashboardT.timeline || 'Timeline'}`;
case '30days':
return `${dashboardT.last_30_days || 'Last 30 days'} ${dashboardT.timeline || 'Timeline'}`;
default:
return dashboardT.last_10_minutes_timeline || `${dashboardT.last_10_minutes || 'Last 10 minutes'} ${dashboardT.timeline || 'Timeline'}`;
}
}
function initCharts(initialData) {
if (!window.Chart) {
return;
}
const { metrics, call_direction } = initialData;
const timelineLabels = metrics?.recent10?.timeline_labels || [];
const timelineSeries = metrics?.recent10?.timeline || [];
if (ctxTimeline) {
timelineChart = new Chart(ctxTimeline, {
type: 'line',
data: {
labels: timelineLabels,
datasets: [{
label: 'Calls',
data: timelineSeries,
fill: true,
tension: 0.35,
borderColor: '#0284c7',
backgroundColor: 'rgba(2, 132, 199, 0.15)',
pointRadius: 3,
pointBackgroundColor: '#0284c7',
pointBorderWidth: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: {
color: '#64748b'
},
grid: {
color: 'rgba(148, 163, 184, 0.2)'
}
},
x: {
ticks: {
color: '#64748b'
},
grid: {
display: false
}
}
},
plugins: {
legend: {
display: false
}
}
}
});
}
if (ctxDirection) {
const directionLabels = Object.keys(call_direction || {});
const directionValues = directionLabels.map((key) => call_direction[key]);
directionChart = new Chart(ctxDirection, {
type: 'bar',
data: {
labels: directionLabels,
datasets: [{
label: 'Calls',
data: directionValues,
backgroundColor: ['#0ea5e9', '#22c55e', '#a855f7'],
borderRadius: 6,
barPercentage: 0.6
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
ticks: {
precision: 0,
color: '#64748b'
},
grid: {
color: 'rgba(148, 163, 184, 0.2)'
}
},
x: {
ticks: {
color: '#64748b'
},
grid: {
display: false
}
}
},
plugins: {
legend: {
display: false
}
}
}
});
}
}
function updateCharts(data) {
if (!timelineChart || !directionChart) {
initCharts(data);
return;
}
const timelineLabels = data.metrics?.recent10?.timeline_labels || [];
const timelineSeries = data.metrics?.recent10?.timeline || [];
timelineChart.data.labels = timelineLabels;
timelineChart.data.datasets[0].data = timelineSeries;
timelineChart.update();
const directionLabels = Object.keys(data.call_direction || {});
directionChart.data.labels = directionLabels;
directionChart.data.datasets[0].data = directionLabels.map((key) => data.call_direction[key]);
directionChart.update();
}
function updateMetrics(data) {
const metrics = data.metrics?.recent10 || {};
const today = data.metrics?.today || {};
const active = data.metrics || {};
const rangeLabel = localizedRangeLabel(data.range);
rangeLabels.forEach((node) => {
node.textContent = rangeLabel;
});
if (elements.total) {
elements.total.textContent = metrics.total ?? 0;
}
if (elements.trend) {
elements.trend.textContent = metrics.trend ?? '+0%';
elements.trend.classList.toggle('text-emerald-600', (metrics.trend || '+').startsWith('+'));
elements.trend.classList.toggle('text-rose-600', (metrics.trend || '+').startsWith('-'));
}
if (elements.utilBar) {
elements.utilBar.style.setProperty('--v', metrics.util ?? 0);
}
if (elements.answered) {
elements.answered.textContent = metrics.answered ?? 0;
}
if (elements.asr) {
elements.asr.textContent = metrics.asr ?? 'โ';
}
if (elements.ansUtilBar) {
elements.ansUtilBar.style.setProperty('--v', metrics.ans_util ?? 0);
}
if (elements.acd) {
elements.acd.textContent = metrics.acd ?? '0s';
}
if (elements.todayAcd) {
elements.todayAcd.textContent = today.acd ?? '0s';
}
if (elements.acdUtilBar) {
elements.acdUtilBar.style.setProperty('--v', metrics.acd_util ?? 0);
}
if (elements.active) {
elements.active.textContent = active.active ?? 0;
}
if (elements.capacity) {
elements.capacity.textContent = active.capacity ?? 0;
}
if (elements.activeUtilBar) {
elements.activeUtilBar.style.setProperty('--v', active.active_util ?? 0);
}
const txRunning = active.transaction_running ?? 0;
const txCapacity = active.transaction_capacity ?? 0;
if (elements.txRunning) elements.txRunning.textContent = txRunning;
if (elements.txActiveCalls) elements.txActiveCalls.textContent = active.active ?? 0;
if (elements.txCapacity && txCapacity > 0) {
elements.txCapacity.textContent = '/ ' + txCapacity;
elements.txCapacity.style.display = '';
} else if (elements.txCapacity) {
elements.txCapacity.style.display = 'none';
}
if (elements.txCallCapacity && active.capacity > 0) {
elements.txCallCapacity.textContent = '/ ' + (active.capacity ?? 0);
elements.txCallCapacity.style.display = '';
} else if (elements.txCallCapacity) {
elements.txCallCapacity.style.display = 'none';
}
const txUtilPct = txCapacity > 0 ? Math.round(txRunning / txCapacity * 100) : 0;
if (elements.txRunningBar) elements.txRunningBar.style.width = txUtilPct + '%';
if (elements.txActiveCallsBar) elements.txActiveCallsBar.style.width = (active.active_util ?? 0) + '%';
if (elements.txUtilization) {
elements.txUtilization.textContent = (txRunning > 0 && txCapacity > 0) ? txUtilPct + '%' : ((active.active_util ?? 0) + '%');
}
if (elements.txUtilPct) elements.txUtilPct.textContent = txUtilPct + '%';
if (elements.timelineTitle) {
elements.timelineTitle.textContent = localizedTimelineTitle(data.range);
}
if (elements.activeUpdated) {
const ts = new Date();
elements.activeUpdated.textContent = `Updated ${ts.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
}
}
window.hangupCall = function (sessionId) {
window.dispatchEvent(new CustomEvent('console:confirm', {
detail: {
title: 'Hangup Call',
message: 'Are you sure you want to hangup this call?',
confirmLabel: 'Hangup',
destructive: true,
onConfirm: async () => {
try {
const resp = await fetch(`{{ api_prefix | safe }}/calls/active/${sessionId}/commands`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
action: 'hangup',
reason: 'admin_dashboard'
})
});
if (resp.ok) {
window.dispatchEvent(new CustomEvent('toast', {
detail: { title: 'Success', message: 'Hangup command sent' }
}));
if (rangeSelect) {
rangeSelect.dispatchEvent(new Event('change'));
}
} else {
const err = await resp.json();
throw new Error(err.message || 'Failed to hangup call');
}
} catch (err) {
console.error(err);
window.dispatchEvent(new CustomEvent('toast', {
detail: { title: 'Error', message: err.message }
}));
}
}, }, }));
};
function renderActiveCalls(calls) {
if (!elements.activeBody) {
return;
}
const rows = Array.isArray(calls) ? calls : [];
if (!rows.length) {
elements.activeBody.innerHTML = '<tr><td colspan="6" class="px-3 py-6 text-center text-sm text-slate-400">No active calls at the moment.</td></tr>';
return;
}
const fragments = rows.map((call) => {
return `<tr class="hover:bg-slate-50">
<td class="px-3 py-2 font-medium text-slate-700">${call.caller}</td>
<td class="px-3 py-2 text-slate-600">${call.callee}</td>
<td class="px-3 py-2"><span class="inline-flex items-center gap-1 rounded-full bg-slate-100 px-2 py-1 text-xs font-medium text-slate-600"><span class="h-1.5 w-1.5 rounded-full bg-emerald-500"></span>${call.status}</span></td>
<td class="px-3 py-2 text-slate-500">${call.started_at}</td>
<td class="px-3 py-2 text-slate-500">${call.duration}</td>
<td class="px-3 py-2 text-slate-500">
<button onclick="hangupCall('${call.session_id}')" class="rounded bg-red-50 px-2 py-1 text-xs font-medium text-red-600 hover:bg-red-100">
Hangup
</button>
</td>
</tr>`;
});
elements.activeBody.innerHTML = fragments.join('');
}
function applyPayload(payload) {
if (!payload) {
return;
}
updateMetrics(payload);
updateCharts(payload);
renderActiveCalls(payload.active_calls);
}
applyPayload(bootstrap.payload);
if (rangeSelect && endpoint) {
rangeSelect.addEventListener('change', async (event) => {
const key = event.target.value;
try {
const resp = await fetch(`${endpoint}?range=${encodeURIComponent(key)}`, {
headers: { 'Accept': 'application/json' }
});
if (!resp.ok) {
throw new Error(`Request failed: ${resp.status}`);
}
const data = await resp.json();
applyPayload(data);
} catch (err) {
console.error('Failed to refresh dashboard data', err);
}
});
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initDashboardPage, { once: true });
} else {
initDashboardPage();
}
</script>
{% endblock %}