v0.1.59: IoT telemetry pipeline — Postgres storage + REST API + UI table
This commit is contained in:
@@ -267,6 +267,13 @@ const S = {
|
||||
mqttConnecting: false,
|
||||
mqttLog: [],
|
||||
autoTimer: null,
|
||||
|
||||
// Телеметрия
|
||||
telemetryRows: [], // загруженные строки [{id, device_id, ts, payload}]
|
||||
telemetryLoading: false,
|
||||
telemetryAutoRefresh: false,
|
||||
telemetryRefreshTimer: null,
|
||||
telemetryDeviceFilter: '', // '' = все устройства
|
||||
};
|
||||
|
||||
// Сохраняем auth-параметры в localStorage
|
||||
@@ -838,6 +845,12 @@ function switchTab(tab) {
|
||||
// Останавливаем авто-отправку при уходе с вкладки эмулятора
|
||||
mqttStopAuto();
|
||||
}
|
||||
if (tab !== 'telemetry') {
|
||||
// Останавливаем авто-рефреш при уходе с вкладки телеметрии
|
||||
clearInterval(S.telemetryRefreshTimer);
|
||||
S.telemetryRefreshTimer = null;
|
||||
S.telemetryAutoRefresh = false;
|
||||
}
|
||||
S.activeTab = tab;
|
||||
|
||||
// Обновляем active-класс на вкладках
|
||||
@@ -849,6 +862,11 @@ function switchTab(tab) {
|
||||
if (body && S.deviceDetail) {
|
||||
body.innerHTML = tabContent(S.deviceDetail, tab);
|
||||
}
|
||||
|
||||
// При переходе на телеметрию — сразу загружаем данные
|
||||
if (tab === 'telemetry') {
|
||||
loadTelemetry();
|
||||
}
|
||||
}
|
||||
|
||||
function tabContent(d, tab) {
|
||||
@@ -989,6 +1007,10 @@ function emulatorTab(d) {
|
||||
<div class="form-group">
|
||||
<label>Payload (JSON)</label>
|
||||
<textarea id="emu-payload" rows="4">{"temp": 22.5, "humidity": 60}</textarea>
|
||||
<button class="btn btn-ghost btn-sm" style="margin-top:6px;"
|
||||
onclick="document.getElementById('emu-payload').value=generateSensorPayload()">
|
||||
🎲 Случайные данные (температура / влажность)
|
||||
</button>
|
||||
</div>
|
||||
<div class="row" style="margin-bottom:16px;flex-wrap:wrap;">
|
||||
<button class="btn btn-primary" onclick="emuSendOnce()">Отправить</button>
|
||||
@@ -1026,6 +1048,14 @@ function emuSendOnce() {
|
||||
mqttPublish(topic, payload.trim());
|
||||
}
|
||||
|
||||
// generateSensorPayload — случайный payload: температура [18-28°C], влажность [40-80%]
|
||||
// Используется кнопкой «Случайные данные» в эмуляторе
|
||||
function generateSensorPayload() {
|
||||
const temp = parseFloat((18 + Math.random() * 10).toFixed(1));
|
||||
const hum = Math.round(40 + Math.random() * 40);
|
||||
return JSON.stringify({ temperature: temp, humidity: hum, ts: new Date().toISOString() });
|
||||
}
|
||||
|
||||
// toggleAuto — включить/выключить авто-отправку
|
||||
function toggleAuto() {
|
||||
const btn = document.getElementById('auto-btn');
|
||||
@@ -1044,18 +1074,126 @@ function toggleAuto() {
|
||||
// ВКЛАДКА — ТЕЛЕМЕТРИЯ
|
||||
// =============================================================
|
||||
function telemetryTab() {
|
||||
const deviceOptions = S.devices.map(d =>
|
||||
`<option value="${h(d.device_id)}" ${S.telemetryDeviceFilter === d.device_id ? 'selected' : ''}>${h(d.name)}</option>`
|
||||
).join('');
|
||||
|
||||
const rows = S.telemetryRows.length === 0
|
||||
? `<tr><td colspan="3" style="text-align:center;color:#888;padding:24px;">Нет данных — нажмите «↻ Обновить»</td></tr>`
|
||||
: S.telemetryRows.map(r => {
|
||||
const ts = r.ts ? new Date(r.ts).toLocaleString('ru') : '—';
|
||||
const dev = h(r.device_id || '—');
|
||||
let payloadStr = '';
|
||||
try {
|
||||
payloadStr = typeof r.payload === 'object'
|
||||
? JSON.stringify(r.payload)
|
||||
: String(r.payload);
|
||||
} catch (_) { payloadStr = String(r.payload); }
|
||||
return `<tr>
|
||||
<td style="padding:6px 12px;white-space:nowrap;">${h(ts)}</td>
|
||||
<td style="padding:6px 12px;">${dev}</td>
|
||||
<td style="padding:6px 12px;font-family:monospace;font-size:12px;word-break:break-all;">${h(payloadStr)}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
return `<div class="card">
|
||||
<div class="coming-soon">
|
||||
<div class="icon">📊</div>
|
||||
<h3>Телеметрия</h3>
|
||||
<p class="hint" style="margin-top:8px;">Скоро — хранение данных и API чтения</p>
|
||||
<p class="hint" style="margin-top:4px;color:#334155;">
|
||||
В разработке — появится в следующем обновлении
|
||||
</p>
|
||||
<div class="card-title">Телеметрия</div>
|
||||
|
||||
<!-- Панель управления: фильтр, обновление, авто-рефреш -->
|
||||
<div class="row" style="margin-bottom:16px;flex-wrap:wrap;gap:12px;align-items:center;">
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<label class="hint" for="telem-dev-filter">Устройство:</label>
|
||||
<select id="telem-dev-filter" onchange="S.telemetryDeviceFilter=this.value;loadTelemetry()">
|
||||
<option value="">Все</option>
|
||||
${deviceOptions}
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-sm" onclick="loadTelemetry()">↻ Обновить</button>
|
||||
<label style="display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer;">
|
||||
<input type="checkbox" id="telem-auto" ${S.telemetryAutoRefresh ? 'checked' : ''}
|
||||
onchange="toggleTelemetryAutoRefresh(this.checked)">
|
||||
Авто (5 с)
|
||||
</label>
|
||||
<span id="telem-status" class="hint"></span>
|
||||
</div>
|
||||
|
||||
<!-- Таблица телеметрии -->
|
||||
<div style="overflow-x:auto;">
|
||||
<table style="width:100%;border-collapse:collapse;font-size:13px;">
|
||||
<thead>
|
||||
<tr style="background:#f0f4f8;text-align:left;">
|
||||
<th style="padding:8px 12px;white-space:nowrap;">Время</th>
|
||||
<th style="padding:8px 12px;white-space:nowrap;">Устройство</th>
|
||||
<th style="padding:8px 12px;">Данные</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="telem-tbody">${rows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// loadTelemetry — загружает историю телеметрии с API.
|
||||
// URL: GET /v1/namespaces/{ns}/iot/telemetry?limit=100[&device=...]
|
||||
async function loadTelemetry() {
|
||||
if (!S.ns || !S.token) return;
|
||||
const status = document.getElementById('telem-status');
|
||||
if (status) status.textContent = 'Загрузка...';
|
||||
|
||||
// Читаем текущий фильтр из DOM (если вкладка открыта) или из state
|
||||
const sel = document.getElementById('telem-dev-filter');
|
||||
if (sel) S.telemetryDeviceFilter = sel.value;
|
||||
|
||||
try {
|
||||
let url = `${S.apiBase}/v1/namespaces/${encodeURIComponent(S.ns)}/iot/telemetry?limit=100`;
|
||||
if (S.telemetryDeviceFilter) url += `&device=${encodeURIComponent(S.telemetryDeviceFilter)}`;
|
||||
|
||||
const resp = await fetch(url, { headers: { Authorization: 'Bearer ' + S.token } });
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
const data = await resp.json();
|
||||
S.telemetryRows = data.items || [];
|
||||
|
||||
// Обновляем только tbody — не перерисовываем весь таб (сохраняем фильтр/чекбокс)
|
||||
const tbody = document.getElementById('telem-tbody');
|
||||
if (tbody) {
|
||||
if (S.telemetryRows.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="3" style="text-align:center;color:#888;padding:24px;">Нет данных</td></tr>';
|
||||
} else {
|
||||
tbody.innerHTML = S.telemetryRows.map(r => {
|
||||
const ts = r.ts ? new Date(r.ts).toLocaleString('ru') : '—';
|
||||
const dev = h(r.device_id || '—');
|
||||
let payloadStr = '';
|
||||
try {
|
||||
payloadStr = typeof r.payload === 'object'
|
||||
? JSON.stringify(r.payload)
|
||||
: String(r.payload);
|
||||
} catch (_) { payloadStr = String(r.payload); }
|
||||
return `<tr>
|
||||
<td style="padding:6px 12px;white-space:nowrap;">${h(ts)}</td>
|
||||
<td style="padding:6px 12px;">${dev}</td>
|
||||
<td style="padding:6px 12px;font-family:monospace;font-size:12px;word-break:break-all;">${h(payloadStr)}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
}
|
||||
if (status) status.textContent = `Обновлено в ${new Date().toLocaleTimeString('ru')}`;
|
||||
} catch(e) {
|
||||
if (status) status.textContent = 'Ошибка: ' + e.message;
|
||||
}
|
||||
}
|
||||
|
||||
// toggleTelemetryAutoRefresh — включить/выключить авто-обновление каждые 5 секунд
|
||||
function toggleTelemetryAutoRefresh(enabled) {
|
||||
S.telemetryAutoRefresh = enabled;
|
||||
clearInterval(S.telemetryRefreshTimer);
|
||||
S.telemetryRefreshTimer = null;
|
||||
if (enabled) {
|
||||
// Немедленная загрузка + интервал
|
||||
loadTelemetry();
|
||||
S.telemetryRefreshTimer = setInterval(loadTelemetry, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================
|
||||
// NAVBAR
|
||||
// =============================================================
|
||||
|
||||
Reference in New Issue
Block a user