rov-autonomy/widgets/w0_data_lake_inspector.html

345 lines
10 KiB
HTML

<!DOCTYPE html>
<!--
============================================================
ARGONAUT 3 — DATA LAKE INSPECTOR
Widget 0 — Diagnostic tool (not a production widget)
============================================================
Purpose : Show all Cockpit data lake variables with live
updating values. Used during development to
discover what MAVLink data is available.
Import : Cockpit → Edit mode → Add DIY Widget →
gear icon → Import → select this file
Version : 1.1 (added scrolling, compact layout, fix)
============================================================
-->
<style>
/* ── Reset and root variables ──────────────────────────── */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg0: #09111a; /* darkest background */
--bg1: #0e1d2e; /* table row background */
--bg2: #162336; /* header / input background */
--border: #1e3050; /* subtle border */
--text0: #d8eeff; /* primary text */
--text1: #6a9bbf; /* secondary / dim text */
--accent: #00c8f0; /* highlight colour */
--green: #00e09a; /* value flash on update */
--amber: #ffb830; /* status warning */
--red: #ff3a5a; /* status error / stale */
--mono: 'Courier New', monospace;
}
/* ── Outer wrapper — fills the widget area ────────────── */
body {
background: var(--bg0);
color: var(--text0);
font-family: var(--mono);
font-size: 12px;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden; /* prevent body scroll — table scrolls */
}
/* ── Sticky header bar ─────────────────────────────────── */
#header {
background: var(--bg2);
border-bottom: 1px solid var(--border);
padding: 5px 8px;
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0; /* never shrink — always visible */
}
#header .title {
font-size: 11px;
font-weight: bold;
color: var(--accent);
white-space: nowrap;
}
/* Search / filter input */
#search {
flex: 1;
background: var(--bg0);
border: 1px solid var(--border);
border-radius: 3px;
color: var(--text0);
font-family: var(--mono);
font-size: 11px;
padding: 2px 6px;
outline: none;
}
#search:focus {
border-color: var(--accent);
}
/* Live variable count */
#count {
font-size: 10px;
color: var(--text1);
white-space: nowrap;
}
/* Connection status dot */
#status {
font-size: 10px;
color: var(--amber);
white-space: nowrap;
}
#status.ok { color: var(--green); }
#status.err { color: var(--red); }
/* ── Scrollable table container ────────────────────────── */
#table-wrap {
flex: 1; /* take remaining height */
overflow-y: auto; /* THIS is what enables scrolling */
overflow-x: hidden;
}
/* Thin scrollbar styling */
#table-wrap::-webkit-scrollbar { width: 5px; }
#table-wrap::-webkit-scrollbar-track { background: var(--bg0); }
#table-wrap::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
/* ── Table ─────────────────────────────────────────────── */
table {
width: 100%;
border-collapse: collapse;
}
/* Sticky column headers scroll with table — not the page */
thead th {
position: sticky;
top: 0;
background: var(--bg2);
border-bottom: 1px solid var(--border);
color: var(--text1);
font-size: 10px;
font-weight: normal;
padding: 3px 6px;
text-align: left;
text-transform: uppercase;
letter-spacing: 0.05em;
z-index: 1;
}
/* Column widths */
thead th:nth-child(1) { width: 55%; } /* Variable name */
thead th:nth-child(2) { width: 12%; } /* Type */
thead th:nth-child(3) { width: 33%; } /* Value */
/* Rows */
tbody tr {
border-bottom: 1px solid var(--border);
transition: background 0.1s;
}
tbody tr:hover { background: var(--bg2); }
/* Flash green when a value changes */
tbody tr.flash td { color: var(--green); }
td {
padding: 3px 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Variable name column */
td:nth-child(1) {
color: var(--accent);
font-size: 11px;
}
/* Type column */
td:nth-child(2) {
color: var(--text1);
font-size: 10px;
}
/* Value column */
td:nth-child(3) {
color: var(--text0);
font-size: 11px;
}
/* Empty state message */
#empty {
padding: 20px;
text-align: center;
color: var(--text1);
font-size: 11px;
}
</style>
<body>
<!-- ── Header bar ──────────────────────────────────────── -->
<div id="header">
<span class="title">⬡ Data Lake</span>
<input id="search" type="text" placeholder="filter variables…" />
<span id="count">0 vars</span>
<span id="status">connecting…</span>
</div>
<!-- ── Scrollable table area ───────────────────────────── -->
<div id="table-wrap">
<table>
<thead>
<tr>
<th>Variable</th>
<th>Type</th>
<th>Value</th>
</tr>
</thead>
<tbody id="tbody">
<!-- Rows injected by JS -->
</tbody>
</table>
<div id="empty" style="display:none">No variables match the filter.</div>
</div>
</body>
<script>
// ── State ─────────────────────────────────────────────────
let vars = {}; // { variableName: { type, value } }
let filter = ''; // current search string (lowercased)
// ── DOM references ────────────────────────────────────────
const tbody = document.getElementById('tbody');
const countEl = document.getElementById('count');
const statusEl= document.getElementById('status');
const searchEl= document.getElementById('search');
const emptyEl = document.getElementById('empty');
// ── Format a raw value for display ───────────────────────
function fmt(v) {
if (v === null || v === undefined) return '—';
if (typeof v === 'number') {
// Integers shown as-is; floats to 4 decimal places
return Number.isInteger(v) ? String(v) : v.toFixed(4);
}
if (typeof v === 'boolean') return v ? 'true' : 'false';
if (typeof v === 'object') return JSON.stringify(v).slice(0, 60);
return String(v).slice(0, 80);
}
// ── Render the table ─────────────────────────────────────
// Only re-renders rows that changed (avoids scroll reset)
function render() {
const keys = Object.keys(vars).sort();
const filtered = keys.filter(k =>
!filter || k.toLowerCase().includes(filter)
);
// Show/hide empty state
emptyEl.style.display = filtered.length === 0 ? '' : 'none';
// Update count
countEl.textContent = `${filtered.length} / ${keys.length} vars`;
// Build a map of existing rows by variable name
const existingRows = {};
tbody.querySelectorAll('tr[data-key]').forEach(row => {
existingRows[row.dataset.key] = row;
});
// Remove rows no longer in filtered set
Object.keys(existingRows).forEach(k => {
if (!filtered.includes(k)) {
existingRows[k].remove();
delete existingRows[k];
}
});
// Add or update rows (in sorted order)
filtered.forEach((key, idx) => {
const { type, value } = vars[key];
let row = existingRows[key];
if (!row) {
// Create new row
row = document.createElement('tr');
row.dataset.key = key;
row.innerHTML = `<td title="${key}">${key}</td><td></td><td></td>`;
tbody.appendChild(row);
}
// Update type and value cells
const cells = row.cells;
const newType = type || '—';
const newValue = fmt(value);
// Flash green on value change
if (cells[2].textContent !== newValue) {
row.classList.add('flash');
setTimeout(() => row.classList.remove('flash'), 400);
}
cells[1].textContent = newType;
cells[2].textContent = newValue;
});
}
// ── Poll the data lake every 500ms ───────────────────────
// getAllDataLakeVariablesInfo() returns {name: {type, value}}
function poll() {
try {
// Guard: API may not be ready immediately on load
if (typeof window.cockpit === 'undefined' ||
typeof window.cockpit.getAllDataLakeVariablesInfo !== 'function') {
statusEl.className = 'err';
statusEl.textContent = 'API not ready';
return;
}
const info = window.cockpit.getAllDataLakeVariablesInfo();
if (!info || Object.keys(info).length === 0) {
statusEl.className = '';
statusEl.textContent = 'no data';
return;
}
// Update our state from the fresh snapshot
Object.entries(info).forEach(([key, meta]) => {
vars[key] = {
type: meta?.type ?? typeof meta?.value ?? '?',
value: meta?.value ?? meta,
};
});
statusEl.className = 'ok';
statusEl.textContent = 'live';
render();
} catch (err) {
statusEl.className = 'err';
statusEl.textContent = 'error';
console.error('[DataLake Inspector]', err);
}
}
// ── Search / filter handler ───────────────────────────────
searchEl.addEventListener('input', () => {
filter = searchEl.value.trim().toLowerCase();
render();
});
// ── Start polling ─────────────────────────────────────────
// Initial poll after 300ms (allows Cockpit API to initialise)
setTimeout(poll, 300);
setInterval(poll, 500);
</script>