304 lines
12 KiB
JavaScript
304 lines
12 KiB
JavaScript
let currentTab = 'dashboard';
|
||
|
||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
document.getElementById(btn.dataset.tab).classList.add('active');
|
||
currentTab = btn.dataset.tab;
|
||
if (btn.dataset.tab === 'dashboard') loadDashboard();
|
||
if (btn.dataset.tab === 'alerts') loadAlerts();
|
||
if (btn.dataset.tab === 'ams') loadAms();
|
||
if (btn.dataset.tab === 'edges') loadEdges();
|
||
if (btn.dataset.tab === 'simulate') loadSimulate();
|
||
});
|
||
});
|
||
|
||
async function api(url) {
|
||
const r = await fetch(url);
|
||
if (!r.ok) throw new Error(r.status);
|
||
return r.json();
|
||
}
|
||
|
||
async function loadStats() {
|
||
try {
|
||
const stats = await api('/api/stats');
|
||
document.getElementById('statAlerts').textContent = stats.alerts_today;
|
||
document.getElementById('statOnline').textContent = `${stats.online_ams}/${stats.total_ams}`;
|
||
document.getElementById('statEdges').textContent = `${stats.online_edges}/${stats.total_edges}`;
|
||
} catch (e) { console.error('Stats error:', e); }
|
||
}
|
||
|
||
async function loadDashboard() {
|
||
await loadStats();
|
||
await loadAmsMap();
|
||
await loadRecentAlerts();
|
||
await loadStatsChart();
|
||
}
|
||
|
||
async function loadAmsMap() {
|
||
try {
|
||
const amsList = await api('/api/ams');
|
||
const container = document.getElementById('amsMap');
|
||
if (!amsList.length) {
|
||
container.innerHTML = '<p style="text-align:center;color:white;">Нет АМС. Добавьте на вкладке АМС.</p>';
|
||
return;
|
||
}
|
||
container.style.position = 'relative';
|
||
container.innerHTML = amsList.map((a, i) => {
|
||
const x = 10 + (i % 3) * 35;
|
||
const y = 20 + Math.floor(i / 3) * 25;
|
||
return `<div class="ams-point ${a.status === 'online' ? 'online' : 'offline'}"
|
||
style="left:${x}%;top:${y}%"
|
||
title="${a.name} (${a.status})"></div>`;
|
||
}).join('');
|
||
} catch (e) { console.error('Map error:', e); }
|
||
}
|
||
|
||
async function loadRecentAlerts() {
|
||
try {
|
||
const alerts = await api('/api/alerts');
|
||
const container = document.getElementById('recentAlerts');
|
||
if (!alerts.length) {
|
||
container.innerHTML = '<p style="text-align:center;color:var(--gray-500);">Тревог нет</p>';
|
||
return;
|
||
}
|
||
container.innerHTML = alerts.slice(0, 5).map(a => `
|
||
<div class="alert-item ${a.status}">
|
||
<div class="alert-header">
|
||
<span class="alert-title">${a.amc_name || 'АМС #' + a.amc_id}</span>
|
||
<span class="alert-time">${new Date(a.created_at).toLocaleString('kk-KZ')}</span>
|
||
</div>
|
||
<div class="alert-details">📹 ${a.camera_role} • 🎯 ${a.event_type}</div>
|
||
</div>
|
||
`).join('');
|
||
} catch (e) { console.error('Alerts error:', e); }
|
||
}
|
||
|
||
async function loadStatsChart() {
|
||
try {
|
||
const stats = await api('/api/stats');
|
||
document.getElementById('statsChart').innerHTML = `
|
||
<div class="stat-row"><span class="label">Всего АМС</span><span class="value">${stats.total_ams}</span></div>
|
||
<div class="stat-row"><span class="label">АМС онлайн</span><span class="value">${stats.online_ams}</span></div>
|
||
<div class="stat-row"><span class="label">Edge устройств</span><span class="value">${stats.total_edges}</span></div>
|
||
<div class="stat-row"><span class="label">Edge онлайн</span><span class="value">${stats.online_edges}</span></div>
|
||
<div class="stat-row"><span class="label">Событий сегодня</span><span class="value">${stats.events_today}</span></div>
|
||
<div class="stat-row"><span class="label">Тревог сегодня</span><span class="value">${stats.alerts_today}</span></div>
|
||
<div class="stat-row"><span class="label">Всего тревог</span><span class="value">${stats.total_alerts}</span></div>
|
||
`;
|
||
} catch (e) { console.error('Stats error:', e); }
|
||
}
|
||
|
||
async function loadAlerts() {
|
||
try {
|
||
const alerts = await api('/api/alerts');
|
||
const statusFilter = document.getElementById('alertStatusFilter').value;
|
||
const search = document.getElementById('alertSearch').value.toLowerCase();
|
||
|
||
let filtered = alerts;
|
||
if (statusFilter !== 'all') filtered = alerts.filter(a => a.status === statusFilter);
|
||
if (search) filtered = alerts.filter(a => (a.amc_name || '').toLowerCase().includes(search));
|
||
|
||
const container = document.getElementById('alertsTable');
|
||
if (!filtered.length) {
|
||
container.innerHTML = '<p style="padding:20px;text-align:center;color:var(--gray-500);">Нет данных</p>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = `<table>
|
||
<thead>
|
||
<tr><th>ID</th><th>АМС</th><th>Камера</th><th>Тип</th><th>Время</th><th>Статус</th><th>Действия</th></tr>
|
||
</thead>
|
||
<tbody>${filtered.map(a => `
|
||
<tr>
|
||
<td>${a.id}</td>
|
||
<td>${a.amc_name || 'АМС #' + a.amc_id}</td>
|
||
<td>${a.camera_role}</td>
|
||
<td>${a.event_type}</td>
|
||
<td>${new Date(a.created_at).toLocaleString('kk-KZ')}</td>
|
||
<td><span class="status-badge status-${a.status}">${a.status}</span></td>
|
||
<td>
|
||
${a.status === 'new' ? `<button onclick="updateAlert(${a.id},'confirmed')" class="btn-primary" style="padding:6px 12px;font-size:12px;">✓</button>` : ''}
|
||
${a.status !== 'closed' ? `<button onclick="updateAlert(${a.id},'closed')" class="btn-secondary" style="padding:6px 12px;font-size:12px;">✕</button>` : ''}
|
||
</td>
|
||
</tr>
|
||
`).join('')}</tbody>
|
||
</table>`;
|
||
} catch (e) { console.error('Alerts error:', e); }
|
||
}
|
||
|
||
document.getElementById('alertStatusFilter').addEventListener('change', loadAlerts);
|
||
document.getElementById('alertSearch').addEventListener('input', loadAlerts);
|
||
|
||
async function updateAlert(id, status) {
|
||
try {
|
||
await fetch(`/api/alerts/${id}`, {
|
||
method: 'PATCH',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ status })
|
||
});
|
||
loadAlerts();
|
||
} catch (e) { console.error('Update error:', e); }
|
||
}
|
||
|
||
async function loadAms() {
|
||
try {
|
||
const amsList = await api('/api/ams');
|
||
const search = document.getElementById('amsSearch').value.toLowerCase();
|
||
let filtered = amsList;
|
||
if (search) filtered = amsList.filter(a => (a.name || '').toLowerCase().includes(search));
|
||
|
||
const container = document.getElementById('amsTable');
|
||
if (!filtered.length) {
|
||
container.innerHTML = '<p style="padding:20px;text-align:center;color:var(--gray-500);">Нет АМС. Добавьте первую!</p>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = `<table>
|
||
<thead>
|
||
<tr><th>ID</th><th>Название</th><th>Адрес</th><th>Регион</th><th>Статус</th><th>Наряд</th></tr>
|
||
</thead>
|
||
<tbody>${filtered.map(a => `
|
||
<tr>
|
||
<td>${a.id}</td>
|
||
<td><strong>${a.name}</strong></td>
|
||
<td>${a.address || '-'}</td>
|
||
<td>${a.region || '-'}</td>
|
||
<td><span class="status-badge status-${a.status}">${a.status}</span></td>
|
||
<td>${a.has_permit ? '✅' : '❌'}</td>
|
||
</tr>
|
||
`).join('')}</tbody>
|
||
</table>`;
|
||
} catch (e) { console.error('AMS error:', e); }
|
||
}
|
||
|
||
document.getElementById('amsSearch').addEventListener('input', loadAms);
|
||
|
||
function showAddAmsModal() {
|
||
document.getElementById('addAmsModal').classList.add('active');
|
||
}
|
||
|
||
function closeAddAmsModal() {
|
||
document.getElementById('addAmsModal').classList.remove('active');
|
||
document.getElementById('addAmsForm').reset();
|
||
}
|
||
|
||
document.getElementById('addAmsForm').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const form = e.target;
|
||
const ams = {
|
||
name: form.name.value,
|
||
address: form.address.value,
|
||
region: form.region.value,
|
||
lat: form.lat.value,
|
||
lon: form.lon.value,
|
||
has_permit: false
|
||
};
|
||
try {
|
||
await fetch('/api/ams', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(ams)
|
||
});
|
||
closeAddAmsModal();
|
||
loadAms();
|
||
loadAmsMap();
|
||
} catch (e) { console.error('Add AMS error:', e); }
|
||
});
|
||
|
||
async function loadEdges() {
|
||
try {
|
||
const edges = await api('/api/edges');
|
||
const container = document.getElementById('edgesTable');
|
||
if (!edges.length) {
|
||
container.innerHTML = '<p style="padding:20px;text-align:center;color:var(--gray-500);">Edge-устройств нет. Запустите симуляцию.</p>';
|
||
return;
|
||
}
|
||
container.innerHTML = `<table>
|
||
<thead>
|
||
<tr><th>Edge ID</th><th>АМС</th><th>Статус</th><th>Последний heartbeat</th></tr>
|
||
</thead>
|
||
<tbody>${edges.map(e => `
|
||
<tr>
|
||
<td><code>${e.edge_id}</code></td>
|
||
<td>${e.amc_name || 'АМС #' + e.amc_id}</td>
|
||
<td><span class="status-badge status-${e.status}">${e.status}</span></td>
|
||
<td>${new Date(e.last_heartbeat).toLocaleString('kk-KZ')}</td>
|
||
</tr>
|
||
`).join('')}</tbody>
|
||
</table>`;
|
||
} catch (e) { console.error('Edges error:', e); }
|
||
}
|
||
|
||
async function loadSimulate() {
|
||
try {
|
||
const amsList = await api('/api/ams');
|
||
const select = document.getElementById('simAms');
|
||
if (!amsList.length) {
|
||
document.getElementById('simulateForm').innerHTML = '<p>Сначала добавьте АМС на вкладке "АМС"</p>';
|
||
return;
|
||
}
|
||
select.innerHTML = amsList.map(a => `<option value="${a.id}" data-name="${a.name}">${a.name}</option>`).join('');
|
||
} catch (e) { console.error('Simulate load error:', e); }
|
||
}
|
||
|
||
document.getElementById('simulateForm').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const resultBox = document.getElementById('simulateResult');
|
||
try {
|
||
const amsSelect = document.getElementById('simAms');
|
||
const amsName = amsSelect.options[amsSelect.selectedIndex].dataset.name;
|
||
const amsId = parseInt(amsSelect.value);
|
||
|
||
const event = {
|
||
event_id: 'evt-' + Date.now(),
|
||
amc_id: amsId,
|
||
amc_name: amsName,
|
||
camera_id: document.getElementById('simCamera').value,
|
||
camera_role: document.getElementById('simCamera').value === 'ladder' ? 'ladder' : 'perimeter',
|
||
event_type: document.getElementById('simEventType').value,
|
||
confidence: 0.75 + Math.random() * 0.2,
|
||
detected_at: new Date().toISOString(),
|
||
snapshot_url: null,
|
||
clip_url: null
|
||
};
|
||
|
||
const hasPermit = document.getElementById('simPermit').value === 'true';
|
||
if (hasPermit) {
|
||
const data = await fetch('/api/ams').then(r => r.json());
|
||
const ams = data.find(a => a.id === amsId);
|
||
if (ams) {
|
||
ams.has_permit = true;
|
||
await fetch('/api/ams', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: ams });
|
||
}
|
||
}
|
||
|
||
const r = await fetch('/api/events', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(event)
|
||
});
|
||
const result = await r.json();
|
||
|
||
resultBox.className = 'result-box success';
|
||
resultBox.innerHTML = `
|
||
<strong>✅ Событие отправлено!</strong><br>
|
||
${result.alert_created ? '🚨 <strong>ТРЕВОГА создана!</strong> (нет наряда-допуска)' : '✅ Наряд-допуск есть, тревоги нет'}
|
||
`;
|
||
} catch (e) {
|
||
resultBox.className = 'result-box error';
|
||
resultBox.innerHTML = `<strong>❌ Ошибка:</strong> ${e.message}`;
|
||
}
|
||
});
|
||
|
||
setInterval(() => {
|
||
if (currentTab === 'dashboard') loadDashboard();
|
||
if (currentTab === 'alerts') loadAlerts();
|
||
if (currentTab === 'ams') loadAms();
|
||
if (currentTab === 'edges') loadEdges();
|
||
}, 10000);
|
||
|
||
loadDashboard();
|