feat: Add backfill script for image brightness stats and display them in the UI.

This commit is contained in:
sebseb7
2025-12-19 16:43:20 +01:00
parent 8e02178c3e
commit 4ddbca1246
3 changed files with 304 additions and 19 deletions

View File

@@ -173,6 +173,13 @@
<div id="loader" class="loader">Loading images...</div>
<div id="grid" class="grid"></div>
<div id="empty" class="empty">Select a camera and date to view images</div>
<!-- Brightness Chart -->
<div id="chartContainer"
style="margin-top: 40px; background: var(--card-bg); padding: 20px; border-radius: 8px; display: none;">
<h3>Brightness Levels</h3>
<canvas id="brightnessChart" style="width: 100%; height: 200px;"></canvas>
</div>
</div>
<!-- Lightbox Modal -->
@@ -292,7 +299,7 @@
}
}
async function loadImages() {
async function loadImages(isAutoUpdate = false) {
const cameraId = cameraSelect.value;
const dateStr = dateSelect.value; // YYYY-MM-DD
@@ -304,9 +311,12 @@
localStorage.setItem('lastCamera', cameraId);
localStorage.setItem('lastDate', dateStr);
loader.style.display = 'block';
grid.innerHTML = '';
emptyMsg.style.display = 'none';
if (!isAutoUpdate) {
loader.style.display = 'block';
grid.innerHTML = '';
emptyMsg.style.display = 'none';
document.getElementById('chartContainer').style.display = 'none';
}
const [year, month, day] = dateStr.split('-');
@@ -314,21 +324,31 @@
const res = await fetch(`cameras/${cameraId}/${year}/${month}/${day}`);
const data = await res.json();
loader.style.display = 'none';
if (!isAutoUpdate) {
loader.style.display = 'none';
}
if (data.images.length === 0) {
emptyMsg.style.display = 'block';
if (!isAutoUpdate) emptyMsg.style.display = 'block';
return;
}
// For auto-update, assume we have data now, clear grid to rebuild
if (isAutoUpdate) {
grid.innerHTML = '';
}
// Group by hour
const hours = {};
data.images.forEach(img => {
let h = '00';
try {
const match = img.timestamp.match(/(\d{2})[-:](\d{2})[-:](\d{2})/);
if (match) {
h = match[1];
// Parse UTC timestamp from filename and convert to local time
const parts = img.timestamp.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2})[-:](\d{2})[-:](\d{2})/);
if (parts) {
const [_, y, m, d, hUtc, mUtc, sUtc] = parts;
const date = new Date(Date.UTC(y, m - 1, d, hUtc, mUtc, sUtc));
h = String(date.getHours()).padStart(2, '0');
} else {
const d = new Date(img.timestamp);
if (!isNaN(d.getTime())) h = String(d.getHours()).padStart(2, '0');
@@ -347,10 +367,11 @@
const card = document.createElement('div');
card.className = 'card';
card.dataset.idx = 0; // Current viewed index
const lastIdx = group.length - 1;
card.dataset.idx = lastIdx; // Default to last (newest)
const thumb = document.createElement('img');
thumb.src = group[0].thumbnailUrl;
thumb.src = group[lastIdx].thumbnailUrl;
const info = document.createElement('div');
info.className = 'card-info';
@@ -375,7 +396,12 @@
};
card.onmouseleave = () => {
// Optional: could reset here
const lastIdx = group.length - 1;
if (card.dataset.idx != lastIdx) {
thumb.src = group[lastIdx].thumbnailUrl;
card.dataset.idx = lastIdx;
info.textContent = `${hour}:00 (${group.length} pics)`;
}
};
card.onclick = () => {
@@ -388,16 +414,116 @@
grid.appendChild(card);
});
// Draw brightness chart if stats exist
if (data.stats && data.stats.length > 0) {
document.getElementById('chartContainer').style.display = 'block';
drawChart(data.stats);
}
} catch (err) {
loader.style.display = 'none';
console.error(err);
grid.innerHTML = '<div style="color:red">Error loading images</div>';
document.getElementById('chartContainer').style.display = 'none';
}
}
function drawChart(stats) {
const canvas = document.getElementById('brightnessChart');
const ctx = canvas.getContext('2d');
// Resize canvas to match display size
const rect = canvas.parentNode.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = 200;
const width = canvas.width;
const height = canvas.height;
const padding = { top: 20, right: 20, bottom: 30, left: 40 };
ctx.clearRect(0, 0, width, height);
// Sort stats by timestamp just in case
stats.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
// Helper to parsing time to minutes from start of day (local time)
const getMinutes = (iso) => {
// Parse UCT timestamp to Date object
const parts = iso.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2})[-:](\d{2})[-:](\d{2})/);
let d;
if (parts) {
const [_, y, m, day, hUtc, mUtc, sUtc] = parts;
d = new Date(Date.UTC(y, m - 1, day, hUtc, mUtc, sUtc));
} else {
d = new Date(iso);
}
return d.getHours() * 60 + d.getMinutes();
};
const points = stats.map(s => ({
x: getMinutes(s.timestamp),
y: s.brightness
}));
// Scales
const mapX = (minutes) => padding.left + (minutes / 1440) * (width - padding.left - padding.right);
const mapY = (val) => height - padding.bottom - (val / 255) * (height - padding.top - padding.bottom);
// Draw Axes
ctx.strokeStyle = '#555';
ctx.lineWidth = 1;
// X-Axis
ctx.beginPath();
ctx.moveTo(padding.left, height - padding.bottom);
ctx.lineTo(width - padding.right, height - padding.bottom);
ctx.stroke();
// Y-Axis
ctx.beginPath();
ctx.moveTo(padding.left, padding.top);
ctx.lineTo(padding.left, height - padding.bottom);
ctx.stroke();
// Draw Line
ctx.strokeStyle = '#3b82f6';
ctx.lineWidth = 2;
ctx.beginPath();
if (points.length > 0) {
ctx.moveTo(mapX(points[0].x), mapY(points[0].y));
for (let i = 1; i < points.length; i++) {
ctx.lineTo(mapX(points[i].x), mapY(points[i].y));
}
}
ctx.stroke();
// Draw Labels (Time)
ctx.fillStyle = '#ccc';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
for (let h = 0; h <= 24; h += 4) {
const x = mapX(h * 60);
ctx.fillText(`${h}:00`, x, height - 10);
}
// Draw Labels (Brightness)
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
for (let b = 0; b <= 255; b += 64) {
const y = mapY(b);
ctx.fillText(Math.round(b), padding.left - 5, y);
}
}
function getPrettyTime(ts) {
const parts = ts.match(/(\d{2})[-:](\d{2})[-:](\d{2})/);
if (parts) return `${parts[1]}:${parts[2]}:${parts[3]}`;
const parts = ts.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2})[-:](\d{2})[-:](\d{2})/);
if (parts) {
const [_, y, m, d, hUtc, mUtc, sUtc] = parts;
const date = new Date(Date.UTC(y, m - 1, d, hUtc, mUtc, sUtc));
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`;
}
return ts;
}
@@ -424,6 +550,14 @@
if (state.selectedDate) loadImages();
});
// Auto-update every 60 seconds
setInterval(() => {
if (state.selectedCamera && state.selectedDate) {
// Only auto-update if we are looking at something
loadImages(true);
}
}, 60000);
// Start
init();