544 lines
22 KiB
HTML
544 lines
22 KiB
HTML
{% extends "admin/layout.html" %}
|
|
{% let active_page = "analytics" %}
|
|
|
|
{% block title %}{{ t.analytics_title }}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="page-head">
|
|
<div>
|
|
<h1>{{ t.analytics_title }}</h1>
|
|
<p>{{ t.analytics_intro }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="analytics-controls"
|
|
data-labels='{
|
|
"landing_views": "{{ t.analytics_metric_landing_views }}",
|
|
"landing_unique": "{{ t.analytics_metric_landing_unique }}",
|
|
"portal_opens": "{{ t.analytics_metric_portal_opens }}",
|
|
"media_views": "{{ t.analytics_metric_media_views }}",
|
|
"no_data": "{{ t.analytics_no_data }}",
|
|
"loading": "{{ t.analytics_loading }}",
|
|
"error": "{{ t.analytics_load_error }}",
|
|
"log_empty": "{{ t.analytics_log_empty }}",
|
|
"log_end": "{{ t.analytics_log_end }}",
|
|
"visitor": "{{ t.analytics_log_visitor }}",
|
|
"landing_view": "{{ t.analytics_ev_landing_view }}",
|
|
"portal_open": "{{ t.analytics_ev_portal_open }}",
|
|
"media_view": "{{ t.analytics_ev_media_view }}"
|
|
}'>
|
|
<div class="analytics-presets">
|
|
<button type="button" class="button is-small preset-btn" data-days="7">{{ t.analytics_preset_7 }}</button>
|
|
<button type="button" class="button is-small preset-btn" data-days="30">{{ t.analytics_preset_30 }}</button>
|
|
<button type="button" class="button is-small preset-btn" data-days="90">{{ t.analytics_preset_90 }}</button>
|
|
<button type="button" class="button is-small preset-btn" data-days="365">{{ t.analytics_preset_365 }}</button>
|
|
</div>
|
|
<div class="analytics-range">
|
|
<label>{{ t.analytics_range_from }}
|
|
<input type="date" id="anFrom" class="input is-small">
|
|
</label>
|
|
<label>{{ t.analytics_range_to }}
|
|
<input type="date" id="anTo" class="input is-small">
|
|
</label>
|
|
<label>{{ t.analytics_granularity }}
|
|
<select id="anGran" class="input is-small">
|
|
<option value="day">{{ t.analytics_gran_day }}</option>
|
|
<option value="week">{{ t.analytics_gran_week }}</option>
|
|
<option value="month">{{ t.analytics_gran_month }}</option>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="analytics-kpis" id="anKpis"></div>
|
|
|
|
<div class="analytics-metric-toggles">
|
|
<span class="toggles-label">{{ t.analytics_metrics_label }}:</span>
|
|
<label><input type="checkbox" class="metric-toggle" data-key="landing_views" checked> {{ t.analytics_metric_landing_views }}</label>
|
|
<label><input type="checkbox" class="metric-toggle" data-key="landing_unique" checked> {{ t.analytics_metric_landing_unique }}</label>
|
|
<label><input type="checkbox" class="metric-toggle" data-key="portal_opens" checked> {{ t.analytics_metric_portal_opens }}</label>
|
|
<label><input type="checkbox" class="metric-toggle" data-key="media_views" checked> {{ t.analytics_metric_media_views }}</label>
|
|
</div>
|
|
|
|
<div class="analytics-chart-wrap">
|
|
<div class="analytics-chart-box">
|
|
<canvas id="anChart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="analytics-tops">
|
|
<div class="analytics-top-card">
|
|
<h2>{{ t.analytics_top_clients }}</h2>
|
|
<table class="analytics-top-table" id="anTopClients">
|
|
<thead><tr><th>{{ t.analytics_top_clients }}</th><th>{{ t.analytics_count }}</th></tr></thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
</div>
|
|
<div class="analytics-top-card">
|
|
<h2>{{ t.analytics_top_media }}</h2>
|
|
<table class="analytics-top-table" id="anTopMedia">
|
|
<thead><tr><th>{{ t.analytics_top_media }}</th><th>{{ t.analytics_count }}</th></tr></thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="analytics-log-card">
|
|
<h2>{{ t.analytics_log_title }}</h2>
|
|
<div class="analytics-log" id="anLog"></div>
|
|
</div>
|
|
|
|
<style>
|
|
.analytics-controls { display:flex; flex-wrap:wrap; gap:16px; align-items:flex-end; margin-bottom:18px; }
|
|
.analytics-presets { display:flex; gap:6px; flex-wrap:wrap; }
|
|
.analytics-range { display:flex; gap:12px; flex-wrap:wrap; }
|
|
.analytics-range label { display:flex; flex-direction:column; font-size:.8rem; color:#666; gap:3px; }
|
|
.analytics-kpis { display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); gap:12px; margin-bottom:18px; }
|
|
.analytics-kpi { background:#fff; border-radius:12px; padding:14px 16px; box-shadow:0 1px 4px rgba(0,0,0,.06); border-left:4px solid var(--kpi-color,#7c6ed4); }
|
|
.analytics-kpi .kpi-value { font-size:1.7rem; font-weight:700; line-height:1.1; }
|
|
.analytics-kpi .kpi-label { font-size:.8rem; color:#777; margin-top:2px; }
|
|
.analytics-metric-toggles { display:flex; flex-wrap:wrap; gap:14px; align-items:center; margin-bottom:12px; font-size:.85rem; }
|
|
.analytics-metric-toggles .toggles-label { color:#777; font-weight:600; }
|
|
.analytics-metric-toggles label { display:flex; align-items:center; gap:5px; cursor:pointer; }
|
|
.analytics-chart-wrap { background:#fff; border-radius:12px; padding:16px; box-shadow:0 1px 4px rgba(0,0,0,.06); margin-bottom:20px; }
|
|
.analytics-chart-box { position:relative; height:340px; width:100%; }
|
|
@media (max-width:600px) { .analytics-chart-box { height:260px; } }
|
|
.analytics-tops { display:grid; grid-template-columns:repeat(auto-fit,minmax(280px,1fr)); gap:16px; }
|
|
.analytics-top-card { background:#fff; border-radius:12px; padding:16px; box-shadow:0 1px 4px rgba(0,0,0,.06); }
|
|
.analytics-top-card h2 { font-size:1rem; margin:0 0 10px; }
|
|
.analytics-top-table { width:100%; border-collapse:collapse; font-size:.9rem; }
|
|
.analytics-top-table th { text-align:left; color:#888; font-weight:600; font-size:.78rem; border-bottom:1px solid #eee; padding:6px 4px; }
|
|
.analytics-top-table td { padding:6px 4px; border-bottom:1px solid #f4f4f4; }
|
|
.analytics-top-table td:last-child, .analytics-top-table th:last-child { text-align:right; width:70px; }
|
|
.analytics-top-empty { color:#aaa; padding:10px 4px; }
|
|
.an-media-link { display:flex; align-items:center; gap:8px; color:inherit; text-decoration:none; }
|
|
.an-media-link:hover { text-decoration:underline; }
|
|
.an-media-thumb { width:40px; height:40px; border-radius:6px; object-fit:cover; background:#eceaf5; flex:0 0 auto; }
|
|
.an-media-missing { width:40px; height:40px; border-radius:6px; background:#f0f0f3; display:inline-flex; align-items:center; justify-content:center; color:#bbb; flex:0 0 auto; }
|
|
|
|
.analytics-log-card { background:#fff; border-radius:12px; padding:16px; box-shadow:0 1px 4px rgba(0,0,0,.06); margin-top:20px; }
|
|
.analytics-log-card h2 { font-size:1rem; margin:0 0 10px; }
|
|
.analytics-log { max-height:460px; overflow-y:auto; border:1px solid #f0f0f0; border-radius:8px; }
|
|
.an-log-row { display:flex; gap:12px; align-items:flex-start; padding:10px 12px; border-bottom:1px solid #f5f5f5; font-size:.85rem; }
|
|
.an-log-row:last-child { border-bottom:none; }
|
|
.an-log-icon { font-size:1.1rem; line-height:1.4; flex:0 0 auto; }
|
|
.an-log-main { flex:1 1 auto; min-width:0; }
|
|
.an-log-head { display:flex; flex-wrap:wrap; gap:8px; align-items:baseline; }
|
|
.an-log-event { font-weight:600; }
|
|
.an-log-client { color:#7c6ed4; }
|
|
.an-log-time { color:#999; font-size:.78rem; margin-left:auto; white-space:nowrap; }
|
|
.an-log-meta { color:#888; font-size:.78rem; margin-top:3px; word-break:break-word; }
|
|
.an-log-meta span { margin-right:12px; }
|
|
.an-log-meta code { background:rgba(0,0,0,.04); padding:1px 4px; border-radius:3px; }
|
|
.an-log-thumb { width:44px; height:44px; border-radius:6px; object-fit:cover; background:#eceaf5; flex:0 0 auto; }
|
|
.an-log-status { text-align:center; color:#aaa; padding:14px; font-size:.85rem; }
|
|
</style>
|
|
|
|
<script src="/static/chart.min.js?v={{ t.app_version() }}"></script>
|
|
<script>
|
|
(function () {
|
|
var controls = document.querySelector('.analytics-controls');
|
|
var L = JSON.parse(controls.getAttribute('data-labels'));
|
|
|
|
var META = [
|
|
{ key: 'landing_views', color: '#7c6ed4' },
|
|
{ key: 'landing_unique', color: '#00b496' },
|
|
{ key: 'portal_opens', color: '#ff8c26' },
|
|
{ key: 'media_views', color: '#ff5287' }
|
|
];
|
|
|
|
var fromInput = document.getElementById('anFrom');
|
|
var toInput = document.getElementById('anTo');
|
|
var granSelect = document.getElementById('anGran');
|
|
var kpisEl = document.getElementById('anKpis');
|
|
var chart = null;
|
|
|
|
// Draws a centered status message ("loading", "no data", error) directly on
|
|
// the chart canvas, so state changes never shift the page layout.
|
|
var messageOverlay = {
|
|
id: 'messageOverlay',
|
|
afterDraw: function (c) {
|
|
var msg = c.$message;
|
|
if (!msg) { return; }
|
|
var ctx = c.ctx;
|
|
var a = c.chartArea;
|
|
ctx.save();
|
|
ctx.fillStyle = 'rgba(255,255,255,0.72)';
|
|
ctx.fillRect(a.left, a.top, a.right - a.left, a.bottom - a.top);
|
|
ctx.fillStyle = '#8a8a8a';
|
|
ctx.font = '600 14px system-ui, sans-serif';
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillText(msg, (a.left + a.right) / 2, (a.top + a.bottom) / 2);
|
|
ctx.restore();
|
|
}
|
|
};
|
|
|
|
function setMessage(msg) {
|
|
if (!chart) { return; }
|
|
chart.$message = msg;
|
|
chart.update('none');
|
|
}
|
|
|
|
function fmt(d) {
|
|
return d.getFullYear() + '-' +
|
|
String(d.getMonth() + 1).padStart(2, '0') + '-' +
|
|
String(d.getDate()).padStart(2, '0');
|
|
}
|
|
|
|
function setRange(days) {
|
|
var to = new Date();
|
|
var from = new Date();
|
|
from.setDate(from.getDate() - (days - 1));
|
|
fromInput.value = fmt(from);
|
|
toInput.value = fmt(to);
|
|
}
|
|
|
|
function metricLabel(key) { return L[key] || key; }
|
|
|
|
function hexToRgba(hex, a) {
|
|
var n = parseInt(hex.slice(1), 16);
|
|
return 'rgba(' + ((n >> 16) & 255) + ',' + ((n >> 8) & 255) + ',' + (n & 255) + ',' + a + ')';
|
|
}
|
|
|
|
function renderKpis(report) {
|
|
kpisEl.replaceChildren();
|
|
META.forEach(function (m) {
|
|
var metric = report.metrics.find(function (x) { return x.key === m.key; });
|
|
var total = metric ? metric.total : 0;
|
|
var card = document.createElement('div');
|
|
card.className = 'analytics-kpi';
|
|
card.style.setProperty('--kpi-color', m.color);
|
|
var v = document.createElement('div');
|
|
v.className = 'kpi-value';
|
|
v.textContent = total.toLocaleString();
|
|
var lbl = document.createElement('div');
|
|
lbl.className = 'kpi-label';
|
|
lbl.textContent = metricLabel(m.key);
|
|
card.appendChild(v);
|
|
card.appendChild(lbl);
|
|
kpisEl.appendChild(card);
|
|
});
|
|
}
|
|
|
|
function initChart() {
|
|
var ctx = document.getElementById('anChart').getContext('2d');
|
|
var datasets = META.map(function (m) {
|
|
return {
|
|
label: metricLabel(m.key),
|
|
data: [],
|
|
borderColor: m.color,
|
|
backgroundColor: hexToRgba(m.color, 0.12),
|
|
borderWidth: 2,
|
|
pointRadius: 2,
|
|
tension: 0.25,
|
|
fill: true,
|
|
hidden: false,
|
|
_key: m.key
|
|
};
|
|
});
|
|
chart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: { labels: [], datasets: datasets },
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
animation: false,
|
|
interaction: { mode: 'index', intersect: false },
|
|
plugins: { legend: { display: false } },
|
|
scales: { y: { beginAtZero: true, ticks: { precision: 0 } } }
|
|
},
|
|
plugins: [messageOverlay]
|
|
});
|
|
}
|
|
|
|
function applyData(report) {
|
|
chart.data.labels = report.labels;
|
|
chart.data.datasets.forEach(function (ds) {
|
|
var metric = report.metrics.find(function (x) { return x.key === ds._key; });
|
|
ds.data = metric ? metric.values : [];
|
|
});
|
|
chart.update('none');
|
|
}
|
|
|
|
function renderTop(tableId, rows) {
|
|
var tbody = document.querySelector('#' + tableId + ' tbody');
|
|
tbody.replaceChildren();
|
|
if (!rows || rows.length === 0) {
|
|
var tr = document.createElement('tr');
|
|
var td = document.createElement('td');
|
|
td.colSpan = 2;
|
|
td.className = 'analytics-top-empty';
|
|
td.textContent = L.no_data;
|
|
tr.appendChild(td);
|
|
tbody.appendChild(tr);
|
|
return;
|
|
}
|
|
rows.forEach(function (row) {
|
|
var tr = document.createElement('tr');
|
|
var name = document.createElement('td');
|
|
name.textContent = row.label;
|
|
var cnt = document.createElement('td');
|
|
cnt.textContent = row.count.toLocaleString();
|
|
tr.appendChild(name);
|
|
tr.appendChild(cnt);
|
|
tbody.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
function mediaLabel(m) {
|
|
if (!m.exists) { return '#' + m.media_id; }
|
|
var base = m.client_name || '';
|
|
if (m.caption) { return base ? base + ': ' + m.caption : m.caption; }
|
|
return base ? base + ' #' + m.media_id : '#' + m.media_id;
|
|
}
|
|
|
|
// Builds a clickable preview (opens the shared lightbox) for a media ref,
|
|
// or a placeholder if the file was deleted.
|
|
function mediaLink(m, thumbClass) {
|
|
if (m && m.exists && m.url) {
|
|
var a = document.createElement('a');
|
|
a.href = m.url;
|
|
a.setAttribute('data-lightbox', m.file_type === 'video' ? 'video' : 'photo');
|
|
a.className = 'an-media-link';
|
|
var img = document.createElement('img');
|
|
img.className = thumbClass;
|
|
img.src = m.thumbnail_url;
|
|
img.alt = '';
|
|
img.loading = 'lazy';
|
|
a.appendChild(img);
|
|
var span = document.createElement('span');
|
|
span.textContent = mediaLabel(m);
|
|
a.appendChild(span);
|
|
return a;
|
|
}
|
|
var wrap = document.createElement('span');
|
|
wrap.className = 'an-media-link';
|
|
var ph = document.createElement('span');
|
|
ph.className = 'an-media-missing';
|
|
ph.textContent = '🗑';
|
|
wrap.appendChild(ph);
|
|
var lbl = document.createElement('span');
|
|
lbl.textContent = mediaLabel(m);
|
|
wrap.appendChild(lbl);
|
|
return wrap;
|
|
}
|
|
|
|
function renderTopMedia(rows) {
|
|
var tbody = document.querySelector('#anTopMedia tbody');
|
|
tbody.replaceChildren();
|
|
if (!rows || rows.length === 0) {
|
|
var tr = document.createElement('tr');
|
|
var td = document.createElement('td');
|
|
td.colSpan = 2;
|
|
td.className = 'analytics-top-empty';
|
|
td.textContent = L.no_data;
|
|
tr.appendChild(td);
|
|
tbody.appendChild(tr);
|
|
return;
|
|
}
|
|
rows.forEach(function (m) {
|
|
var tr = document.createElement('tr');
|
|
var td1 = document.createElement('td');
|
|
td1.appendChild(mediaLink(m, 'an-media-thumb'));
|
|
var td2 = document.createElement('td');
|
|
td2.textContent = (m.count || 0).toLocaleString();
|
|
tr.appendChild(td1);
|
|
tr.appendChild(td2);
|
|
tbody.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
function hasData(report) {
|
|
return report.metrics.some(function (m) { return m.total > 0; });
|
|
}
|
|
|
|
function load() {
|
|
var params = new URLSearchParams({
|
|
from: fromInput.value,
|
|
to: toInput.value,
|
|
granularity: granSelect.value
|
|
});
|
|
setMessage(L.loading);
|
|
fetch('/admin/analytics/data?' + params.toString(), { credentials: 'same-origin' })
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (report) {
|
|
renderKpis(report);
|
|
applyData(report);
|
|
renderTop('anTopClients', report.top_clients);
|
|
renderTopMedia(report.top_media);
|
|
setMessage(hasData(report) ? '' : L.no_data);
|
|
})
|
|
.catch(function () { setMessage(L.error); });
|
|
}
|
|
|
|
document.querySelectorAll('.preset-btn').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
setRange(parseInt(btn.getAttribute('data-days'), 10));
|
|
load();
|
|
});
|
|
});
|
|
fromInput.addEventListener('change', load);
|
|
toInput.addEventListener('change', load);
|
|
granSelect.addEventListener('change', load);
|
|
document.querySelectorAll('.metric-toggle').forEach(function (cb) {
|
|
cb.addEventListener('change', function () {
|
|
if (!chart) { return; }
|
|
chart.data.datasets.forEach(function (ds) {
|
|
if (ds._key === cb.getAttribute('data-key')) { ds.hidden = !cb.checked; }
|
|
});
|
|
chart.update();
|
|
});
|
|
});
|
|
|
|
// --- Event log (newest first, infinite scroll of older events) ---------
|
|
var logEl = document.getElementById('anLog');
|
|
var logCursor = null;
|
|
var logLoading = false;
|
|
var logDone = false;
|
|
|
|
function logStatus(text) {
|
|
var s = document.createElement('div');
|
|
s.className = 'an-log-status';
|
|
s.textContent = text;
|
|
return s;
|
|
}
|
|
|
|
function eventIcon(type) {
|
|
if (type === 'landing_view') { return '🌐'; }
|
|
if (type === 'portal_open') { return '👤'; }
|
|
if (type === 'media_view') { return '🖼️'; }
|
|
return '•';
|
|
}
|
|
|
|
function shortUa(ua) {
|
|
if (!ua) { return ''; }
|
|
var browser = '';
|
|
if (/Edg\//.test(ua)) { browser = 'Edge'; }
|
|
else if (/OPR\/|Opera/.test(ua)) { browser = 'Opera'; }
|
|
else if (/Chrome\//.test(ua)) { browser = 'Chrome'; }
|
|
else if (/Firefox\//.test(ua)) { browser = 'Firefox'; }
|
|
else if (/Safari\//.test(ua)) { browser = 'Safari'; }
|
|
var os = '';
|
|
if (/Windows/.test(ua)) { os = 'Windows'; }
|
|
else if (/iPhone|iPad|iPod/.test(ua)) { os = 'iOS'; }
|
|
else if (/Android/.test(ua)) { os = 'Android'; }
|
|
else if (/Mac OS X|Macintosh/.test(ua)) { os = 'macOS'; }
|
|
else if (/Linux/.test(ua)) { os = 'Linux'; }
|
|
return [browser, os].filter(Boolean).join(' · ');
|
|
}
|
|
|
|
function metaSpan(text) {
|
|
var s = document.createElement('span');
|
|
s.textContent = text;
|
|
return s;
|
|
}
|
|
|
|
function renderLogRow(e) {
|
|
var row = document.createElement('div');
|
|
row.className = 'an-log-row';
|
|
|
|
var icon = document.createElement('div');
|
|
icon.className = 'an-log-icon';
|
|
icon.textContent = eventIcon(e.event_type);
|
|
row.appendChild(icon);
|
|
|
|
if (e.media && e.media.exists && e.media.url) {
|
|
var a = document.createElement('a');
|
|
a.href = e.media.url;
|
|
a.setAttribute('data-lightbox', e.media.file_type === 'video' ? 'video' : 'photo');
|
|
var img = document.createElement('img');
|
|
img.className = 'an-log-thumb';
|
|
img.src = e.media.thumbnail_url;
|
|
img.alt = '';
|
|
img.loading = 'lazy';
|
|
a.appendChild(img);
|
|
row.appendChild(a);
|
|
}
|
|
|
|
var main = document.createElement('div');
|
|
main.className = 'an-log-main';
|
|
|
|
var head = document.createElement('div');
|
|
head.className = 'an-log-head';
|
|
var ev = document.createElement('span');
|
|
ev.className = 'an-log-event';
|
|
ev.textContent = L[e.event_type] || e.event_type;
|
|
head.appendChild(ev);
|
|
if (e.client_name) {
|
|
var cl = document.createElement('span');
|
|
cl.className = 'an-log-client';
|
|
cl.textContent = e.client_name;
|
|
head.appendChild(cl);
|
|
}
|
|
var tm = document.createElement('span');
|
|
tm.className = 'an-log-time';
|
|
tm.textContent = e.created_at;
|
|
head.appendChild(tm);
|
|
main.appendChild(head);
|
|
|
|
var meta = document.createElement('div');
|
|
meta.className = 'an-log-meta';
|
|
if (e.visitor) { meta.appendChild(metaSpan(L.visitor + ': ' + e.visitor)); }
|
|
var ua = shortUa(e.user_agent);
|
|
if (ua) { meta.appendChild(metaSpan(ua)); }
|
|
if (e.path && e.path !== '/') {
|
|
var p = document.createElement('span');
|
|
var code = document.createElement('code');
|
|
code.textContent = e.path;
|
|
p.appendChild(code);
|
|
meta.appendChild(p);
|
|
}
|
|
if (e.referer) { meta.appendChild(metaSpan('← ' + e.referer)); }
|
|
if (meta.childNodes.length) { main.appendChild(meta); }
|
|
|
|
row.appendChild(main);
|
|
return row;
|
|
}
|
|
|
|
function loadLog() {
|
|
if (logLoading || logDone) { return; }
|
|
logLoading = true;
|
|
var busy = logStatus(L.loading);
|
|
logEl.appendChild(busy);
|
|
var params = new URLSearchParams({ limit: '40' });
|
|
if (logCursor) { params.set('before', logCursor); }
|
|
fetch('/admin/analytics/events?' + params.toString(), { credentials: 'same-origin' })
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (data) {
|
|
busy.remove();
|
|
(data.events || []).forEach(function (e) { logEl.appendChild(renderLogRow(e)); });
|
|
logCursor = data.next_cursor;
|
|
logLoading = false;
|
|
if (!logEl.querySelector('.an-log-row')) {
|
|
logEl.appendChild(logStatus(L.log_empty));
|
|
logDone = true;
|
|
return;
|
|
}
|
|
if (!logCursor) {
|
|
logDone = true;
|
|
logEl.appendChild(logStatus(L.log_end));
|
|
return;
|
|
}
|
|
// Keep loading until the scroll container is actually scrollable.
|
|
if (logEl.scrollHeight <= logEl.clientHeight + 20) { loadLog(); }
|
|
})
|
|
.catch(function () {
|
|
busy.remove();
|
|
logLoading = false;
|
|
logEl.appendChild(logStatus(L.error));
|
|
});
|
|
}
|
|
|
|
logEl.addEventListener('scroll', function () {
|
|
if (logEl.scrollTop + logEl.clientHeight >= logEl.scrollHeight - 40) { loadLog(); }
|
|
});
|
|
|
|
initChart();
|
|
setRange(30);
|
|
load();
|
|
loadLog();
|
|
})();
|
|
</script>
|
|
{% endblock %}
|