196 lines
9.2 KiB
JavaScript
196 lines
9.2 KiB
JavaScript
// data_sources.js — logic for /data-sources page
|
||
|
||
function getDateRange() {
|
||
const to = document.getElementById('date_to').value;
|
||
const from = document.getElementById('date_from').value;
|
||
return { date_from: from, date_to: to };
|
||
}
|
||
|
||
function qs(params) {
|
||
return Object.entries(params).filter(([, v]) => v).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&');
|
||
}
|
||
|
||
async function fetchJSON(url) {
|
||
const r = await fetch(url);
|
||
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||
return r.json();
|
||
}
|
||
|
||
function pct(n, total) {
|
||
if (!total) return '—';
|
||
return (100 * n / total).toFixed(1) + '%';
|
||
}
|
||
|
||
// ── Coverage cards ────────────────────────────────────────────
|
||
|
||
async function loadCoverage() {
|
||
const el = document.getElementById('coverage-cards');
|
||
const chart = document.getElementById('coverage-chart');
|
||
try {
|
||
const data = await fetchJSON('/api/data-sources/coverage?' + qs(getDateRange()));
|
||
const rows = data.rows || [];
|
||
const totals = data.totals || {};
|
||
|
||
// Summary cards
|
||
const total = totals.total_schedule || 0;
|
||
el.innerHTML = `
|
||
<div class="card">
|
||
<h3>Расписание <span class="badge badge-sched">Яндекс</span></h3>
|
||
<div class="stat-row"><span>Всего рейсов</span><span class="stat-val">${total.toLocaleString()}</span></div>
|
||
<div class="stat-row"><span>Дней</span><span class="stat-val">${totals.days || 0}</span></div>
|
||
</div>
|
||
<div class="card">
|
||
<h3>RTL-SDR <span class="badge badge-rtlsdr">Локальный</span></h3>
|
||
<div class="stat-row"><span>Рейсов с треком</span><span class="stat-val">${(totals.with_rtlsdr||0).toLocaleString()}<span class="pct">${pct(totals.with_rtlsdr, total)}</span></span></div>
|
||
<div class="bar-wrap" style="margin-top:8px"><div class="bar bar-rtlsdr" style="width:${pct(totals.with_rtlsdr, total)}"></div></div>
|
||
</div>
|
||
<div class="card">
|
||
<h3>FR24 API <span class="badge badge-fr24">Платный</span></h3>
|
||
<div class="stat-row"><span>Рейсов с треком</span><span class="stat-val">${(totals.with_fr24||0).toLocaleString()}<span class="pct">${pct(totals.with_fr24, total)}</span></span></div>
|
||
<div class="bar-wrap" style="margin-top:8px"><div class="bar bar-fr24" style="width:${pct(totals.with_fr24, total)}"></div></div>
|
||
</div>
|
||
<div class="card">
|
||
<h3>FlightAware <span class="badge badge-fa">AeroAPI</span></h3>
|
||
<div class="stat-row"><span>Рейсов с треком</span><span class="stat-val">${(totals.with_fa||0).toLocaleString()}<span class="pct">${pct(totals.with_fa, total)}</span></span></div>
|
||
<div class="bar-wrap" style="margin-top:8px"><div class="bar bar-fa" style="width:${pct(totals.with_fa, total)}"></div></div>
|
||
</div>
|
||
`;
|
||
|
||
// Stacked bar chart by day
|
||
if (!rows.length) { chart.innerHTML = '<div class="loading">Нет данных</div>'; return; }
|
||
const maxTotal = Math.max(...rows.map(r => r.total_schedule || 0), 1);
|
||
chart.innerHTML = rows.map(r => {
|
||
const t = r.total_schedule || 1;
|
||
const wRtl = ((r.with_rtlsdr || 0) / t * 100).toFixed(1);
|
||
const wFr = ((r.with_fr24 || 0) / t * 100).toFixed(1);
|
||
const wFa = ((r.with_fa || 0) / t * 100).toFixed(1);
|
||
const wSch = (100 - parseFloat(wRtl) - parseFloat(wFr) - parseFloat(wFa)).toFixed(1);
|
||
return `<div class="chart-row">
|
||
<span class="chart-label">${r.coverage_date}</span>
|
||
<div class="chart-bars">
|
||
<span style="width:${wRtl}%;background:#40c057" title="RTL-SDR ${wRtl}%"></span>
|
||
<span style="width:${wFr}%;background:#4dabf7" title="FR24 ${wFr}%"></span>
|
||
<span style="width:${wFa}%;background:#cc5de8" title="FA ${wFa}%"></span>
|
||
<span style="width:${Math.max(0,wSch)}%;background:#3a3a1c" title="Только расписание"></span>
|
||
</div>
|
||
<span style="font-size:11px;color:#8b8fa8;width:40px">${t}</span>
|
||
</div>`;
|
||
}).join('');
|
||
} catch (e) {
|
||
el.innerHTML = `<div class="error">Ошибка: ${e.message}</div>`;
|
||
}
|
||
}
|
||
|
||
// ── Quality ───────────────────────────────────────────────────
|
||
|
||
async function loadQuality() {
|
||
const el = document.getElementById('quality-table');
|
||
try {
|
||
const data = await fetchJSON('/api/data-sources/quality?' + qs(getDateRange()));
|
||
const q = data.quality || {};
|
||
el.innerHTML = `<table>
|
||
<tr><th>Метрика</th><th>Значение</th></tr>
|
||
<tr><td>Рейсов с маршрутом</td><td>${pct(q.with_route, q.total)}</td></tr>
|
||
<tr><td>Рейсов с треком</td><td>${pct(q.with_track, q.total)}</td></tr>
|
||
<tr><td>Рейсов с факт. временем</td><td>${pct(q.with_actual_time, q.total)}</td></tr>
|
||
<tr><td>Рейсов с типом ВС</td><td>${pct(q.with_aircraft_type, q.total)}</td></tr>
|
||
<tr><td>Медиана точек (RTL-SDR)</td><td>${q.median_points_rtlsdr || '—'}</td></tr>
|
||
<tr><td>Медиана точек (FR24)</td><td>${q.median_points_fr24 || '—'}</td></tr>
|
||
<tr><td>Медиана точек (FA)</td><td>${q.median_points_fa || '—'}</td></tr>
|
||
</table>`;
|
||
} catch (e) {
|
||
el.innerHTML = `<div class="error">Ошибка: ${e.message}</div>`;
|
||
}
|
||
}
|
||
|
||
// ── Airport load ──────────────────────────────────────────────
|
||
|
||
async function loadAirportLoad() {
|
||
const el = document.getElementById('airport-load');
|
||
try {
|
||
const data = await fetchJSON('/api/data-sources/airport-load?' + qs(getDateRange()));
|
||
const rows = data.rows || [];
|
||
if (!rows.length) { el.innerHTML = '<div class="loading">Нет данных</div>'; return; }
|
||
const maxCount = Math.max(...rows.map(r => r.flight_count || 0), 1);
|
||
el.innerHTML = `<table>
|
||
<tr><th>Аэропорт</th><th>Час (UTC)</th><th>Рейсов</th><th></th></tr>
|
||
${rows.map(r => `<tr>
|
||
<td>${r.airport_iata}</td>
|
||
<td>${String(r.hour).padStart(2,'0')}:00</td>
|
||
<td>${r.flight_count}</td>
|
||
<td><div class="bar-wrap" style="width:80px"><div class="bar bar-fr24" style="width:${(r.flight_count/maxCount*100).toFixed(0)}%"></div></div></td>
|
||
</tr>`).join('')}
|
||
</table>`;
|
||
} catch (e) {
|
||
el.innerHTML = `<div class="error">Ошибка: ${e.message}</div>`;
|
||
}
|
||
}
|
||
|
||
// ── Top airlines ──────────────────────────────────────────────
|
||
|
||
async function loadTopAirlines() {
|
||
const el = document.getElementById('top-airlines');
|
||
try {
|
||
const data = await fetchJSON('/api/data-sources/top-airlines?' + qs(getDateRange()));
|
||
const rows = data.rows || [];
|
||
if (!rows.length) { el.innerHTML = '<div class="loading">Нет данных</div>'; return; }
|
||
const max = rows[0].flight_count || 1;
|
||
el.innerHTML = `<table>
|
||
<tr><th>#</th><th>Авиакомпания</th><th>Рейсов</th><th></th></tr>
|
||
${rows.map((r, i) => `<tr>
|
||
<td style="color:#8b8fa8">${i+1}</td>
|
||
<td>${r.airline_iata || '—'}</td>
|
||
<td>${r.flight_count}</td>
|
||
<td><div class="bar-wrap" style="width:80px"><div class="bar bar-rtlsdr" style="width:${(r.flight_count/max*100).toFixed(0)}%"></div></div></td>
|
||
</tr>`).join('')}
|
||
</table>`;
|
||
} catch (e) {
|
||
el.innerHTML = `<div class="error">Ошибка: ${e.message}</div>`;
|
||
}
|
||
}
|
||
|
||
// ── Top routes ────────────────────────────────────────────────
|
||
|
||
async function loadTopRoutes() {
|
||
const el = document.getElementById('top-routes');
|
||
try {
|
||
const data = await fetchJSON('/api/data-sources/top-routes?' + qs(getDateRange()));
|
||
const rows = data.rows || [];
|
||
if (!rows.length) { el.innerHTML = '<div class="loading">Нет данных</div>'; return; }
|
||
const max = rows[0].flight_count || 1;
|
||
el.innerHTML = `<table>
|
||
<tr><th>#</th><th>Маршрут</th><th>Рейсов</th><th></th></tr>
|
||
${rows.map((r, i) => `<tr>
|
||
<td style="color:#8b8fa8">${i+1}</td>
|
||
<td>${r.origin_iata || '?'} → ${r.destination_iata || '?'}</td>
|
||
<td>${r.flight_count}</td>
|
||
<td><div class="bar-wrap" style="width:80px"><div class="bar bar-fa" style="width:${(r.flight_count/max*100).toFixed(0)}%"></div></div></td>
|
||
</tr>`).join('')}
|
||
</table>`;
|
||
} catch (e) {
|
||
el.innerHTML = `<div class="error">Ошибка: ${e.message}</div>`;
|
||
}
|
||
}
|
||
|
||
// ── Init ──────────────────────────────────────────────────────
|
||
|
||
function loadAll() {
|
||
loadCoverage();
|
||
loadQuality();
|
||
loadAirportLoad();
|
||
loadTopAirlines();
|
||
loadTopRoutes();
|
||
}
|
||
|
||
// Set default date range: last 7 days
|
||
(function initDates() {
|
||
const today = new Date();
|
||
const to = today.toISOString().slice(0, 10);
|
||
today.setDate(today.getDate() - 7);
|
||
const from = today.toISOString().slice(0, 10);
|
||
document.getElementById('date_from').value = from;
|
||
document.getElementById('date_to').value = to;
|
||
})();
|
||
|
||
loadAll();
|