Compare commits

...

2 commits

Author SHA1 Message Date
eb883f0801 Merge pull request 'fix: hydrate camera popup from GET /api/cameras/{id}' (#9) from fix/camera-popup-hydrate into master
Some checks failed
build-and-deploy / build-push-deploy (push) Failing after 3m14s
Reviewed-on: #9
2026-08-28 20:09:30 -04:00
Sirius DevOps
700f56ef51 fix: hydrate camera popup from GET /api/cameras/{id}
List payload is slim (no URLs). YouTube thumbs need source_url from the
detail endpoint, so the popup fetches the full row before rendering.
2026-08-28 20:04:53 -04:00
2 changed files with 51 additions and 14 deletions

View file

@ -1905,9 +1905,7 @@ async function initMap() {
L.control.zoom({ position: 'topright' }).addTo(map);
map.attributionControl.setPrefix('');
let activeHls = null;
map.on('popupopen', (e) => {
if (activeHls) { try { activeHls.destroy(); } catch (_) {} activeHls = null; }
const root = e.popup.getElement();
const startHlsFrom = (root) => {
const v = root && root.querySelector('video[data-hls]');
if (!v) return;
const fallback = () => {
@ -1935,6 +1933,25 @@ async function initMap() {
};
if (window.Hls) startHls();
else loadHls().then(startHls).catch(fallback);
};
map.on('popupopen', (e) => {
if (activeHls) { try { activeHls.destroy(); } catch (_) {} activeHls = null; }
const root = e.popup.getElement();
const box = root && root.querySelector('.cam-pop[data-cam-id]');
if (box && box.dataset.loaded !== '1') {
fetch(`${API}/api/cameras/${box.dataset.camId}`)
.then(r => { if (!r.ok) throw new Error('camera detail'); return r.json(); })
.then(c => {
box.outerHTML = camPopupHtml(c);
startHlsFrom(e.popup.getElement());
})
.catch(() => {
const ph = box.querySelector('.thumb.placeholder');
if (ph) ph.textContent = 'preview unavailable';
});
return;
}
startHlsFrom(root);
});
map.on('popupclose', () => {
if (activeHls) { try { activeHls.destroy(); } catch (_) {} activeHls = null; }
@ -2305,6 +2322,20 @@ function camThumb(c) {
);
return `<img class="thumb" src="${first}" alt="camera preview" loading="lazy" onerror="${onerr}">`;
}
function camPopupHtml(c) {
return `<div class="cam-pop" data-cam-id="${esc(c.id)}" data-loaded="1">` +
`<b>${esc(c.location_name || 'Open camera')}</b>` +
`${c.id ? camThumb(c) : '<div class="thumb placeholder">no snapshot</div>'}` +
`<table>` +
`<tr><td class="k">Vendor</td><td>${esc(c.vendor || '?')} ${esc(c.device_type || '')}</td></tr>` +
`<tr><td class="k">Source</td><td>${esc(c.discovery_source || '?')}</td></tr>` +
`<tr><td class="k">First seen</td><td>${esc((c.first_seen||'').slice(0,16).replace('T',' '))}</td></tr>` +
`<tr><td class="k">Last seen</td><td>${esc((c.last_seen||'').slice(0,16).replace('T',' '))}</td></tr>` +
`<tr><td class="k">Coords</td><td>${(c.lat!=null&&c.lon!=null) ? c.lat.toFixed(3)+', '+c.lon.toFixed(3) : 'unknown'}</td></tr>` +
`</table>` +
camSourceLink(c) +
`</div>`;
}
async function loadCams() {
if (!map) return;
if (tooZoomedOut()) {
@ -2340,18 +2371,9 @@ async function loadCams() {
iconSize: [12, 12], iconAnchor: [6, 6],
});
camsGroup.addLayer(L.marker([c.lat, c.lon], { icon })
.bindPopup(() => `<div class="cam-pop">` +
.bindPopup(() => `<div class="cam-pop" data-cam-id="${esc(c.id)}">` +
`<b>${esc(c.location_name || 'Open camera')}</b>` +
`${c.id ? camThumb(c) : '<div class="thumb placeholder">no snapshot</div>'}` +
`<table>` +
`<tr><td class="k">Vendor</td><td>${esc(c.vendor || '?')} ${esc(c.device_type || '')}</td></tr>` +
`<tr><td class="k">Source</td><td>${esc(c.discovery_source || '?')}</td></tr>` +
`<tr><td class="k">First seen</td><td>${esc((c.first_seen||'').slice(0,16).replace('T',' '))}</td></tr>` +
`<tr><td class="k">Last seen</td><td>${esc((c.last_seen||'').slice(0,16).replace('T',' '))}</td></tr>` +
`<tr><td class="k">Coords</td><td>${(c.lat!=null&&c.lon!=null) ? c.lat.toFixed(3)+', '+c.lon.toFixed(3) : 'unknown'}</td></tr>` +
`</table>` +
camSourceLink(c) +
`</div>`));
`<div class="thumb placeholder">loading feed…</div></div>`));
});
camsGroup.addTo(map);
camsGroup.eachLayer(l => l.setOpacity && l.setOpacity(camsOpacity));

View file

@ -0,0 +1,15 @@
"""Camera list is slim (no URLs). Popup must fetch GET /api/cameras/{id}."""
from pathlib import Path
HTML = Path(__file__).resolve().parents[1] / "app/static/index.html"
def test_popupopen_fetches_camera_detail_row():
html = HTML.read_text()
start = html.index("map.on('popupopen'")
end = html.index("map.on('popupclose'")
block = html[start:end]
assert "fetch(" in block
assert "/api/cameras/" in block
assert "camPopupHtml(" in block