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.
This commit is contained in:
parent
1b1f2d3b3a
commit
700f56ef51
2 changed files with 51 additions and 14 deletions
|
|
@ -1905,9 +1905,7 @@ async function initMap() {
|
||||||
L.control.zoom({ position: 'topright' }).addTo(map);
|
L.control.zoom({ position: 'topright' }).addTo(map);
|
||||||
map.attributionControl.setPrefix('');
|
map.attributionControl.setPrefix('');
|
||||||
let activeHls = null;
|
let activeHls = null;
|
||||||
map.on('popupopen', (e) => {
|
const startHlsFrom = (root) => {
|
||||||
if (activeHls) { try { activeHls.destroy(); } catch (_) {} activeHls = null; }
|
|
||||||
const root = e.popup.getElement();
|
|
||||||
const v = root && root.querySelector('video[data-hls]');
|
const v = root && root.querySelector('video[data-hls]');
|
||||||
if (!v) return;
|
if (!v) return;
|
||||||
const fallback = () => {
|
const fallback = () => {
|
||||||
|
|
@ -1935,6 +1933,25 @@ async function initMap() {
|
||||||
};
|
};
|
||||||
if (window.Hls) startHls();
|
if (window.Hls) startHls();
|
||||||
else loadHls().then(startHls).catch(fallback);
|
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', () => {
|
map.on('popupclose', () => {
|
||||||
if (activeHls) { try { activeHls.destroy(); } catch (_) {} activeHls = null; }
|
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}">`;
|
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() {
|
async function loadCams() {
|
||||||
if (!map) return;
|
if (!map) return;
|
||||||
if (tooZoomedOut()) {
|
if (tooZoomedOut()) {
|
||||||
|
|
@ -2340,18 +2371,9 @@ async function loadCams() {
|
||||||
iconSize: [12, 12], iconAnchor: [6, 6],
|
iconSize: [12, 12], iconAnchor: [6, 6],
|
||||||
});
|
});
|
||||||
camsGroup.addLayer(L.marker([c.lat, c.lon], { icon })
|
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>` +
|
`<b>${esc(c.location_name || 'Open camera')}</b>` +
|
||||||
`${c.id ? camThumb(c) : '<div class="thumb placeholder">no snapshot</div>'}` +
|
`<div class="thumb placeholder">loading feed…</div></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>`));
|
|
||||||
});
|
});
|
||||||
camsGroup.addTo(map);
|
camsGroup.addTo(map);
|
||||||
camsGroup.eachLayer(l => l.setOpacity && l.setOpacity(camsOpacity));
|
camsGroup.eachLayer(l => l.setOpacity && l.setOpacity(camsOpacity));
|
||||||
|
|
|
||||||
15
tests/test_camera_popup_contract.py
Normal file
15
tests/test_camera_popup_contract.py
Normal 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
|
||||||
Loading…
Add table
Reference in a new issue