const expandedItems = JSON.parse(localStorage.getItem("expandedItems") || "{}");
const renamedLabels = JSON.parse(localStorage.getItem("renamedLabels") || "{}"); // Allow card to change their name (original identified by ts)
let previousKeys = [];
let previousEvents = {}; // Track event status to avoid redundant updates
// ─────────────────────────────────────────
// Refresh event per interval
// ─────────────────────────────────────────
async function fetchEvents() {
const res = await fetch('/events');
const data = await res.json();
renderEvents(data);
}
// ─────────────────────────────────────────
// Update or Create new card
// ─────────────────────────────────────────
function renderEvents(events) {
const container = document.getElementById('log-container');
const currentKeys = Object.keys(events).sort();
const newlyAdded = currentKeys.find(k => !previousKeys.includes(k));
previousKeys = currentKeys;
currentKeys.forEach(key => {
const event = events[key];
const existing = document.getElementById(`card-${key}`);
const prevStatus = previousEvents[key]?.status;
if (!existing) {
const card = createCard(key, event);
container.appendChild(card);
if (key === newlyAdded && event.status === 'done') {
setTimeout(() => card.scrollIntoView({ behavior: 'smooth', block: 'center' }), 300);
}
} else if (event.status !== prevStatus) {
updateCard(key, event); // Only update if status changed
}
previousEvents[key] = { status: event.status }; // Cache latest status
});
}
// ─────────────────────────────────────────
// Create new card on unmatched key
// ─────────────────────────────────────────
function createCard(key, event) {
const readable = renamedLabels[key] || formatTimestamp(key);
const safeKey = key.replace(/[:.]/g, "-");
const card = document.createElement('div');
card.id = `card-${key}`;
card.className = 'card';
const removeBtn = document.createElement('button');
removeBtn.className = 'btn-remove';
removeBtn.textContent = 'X';
removeBtn.onclick = () => removeItem(key);
const tsDiv = document.createElement('div');
tsDiv.className = 'timestamp';
tsDiv.innerHTML = `${readable}`;
const editIcon = document.createElement('img');
editIcon.src = '/static/edit.png';
editIcon.className = 'icon-edit';
editIcon.onclick = () => toggleEditMode(tsDiv, key);
tsDiv.appendChild(editIcon);
const statusDiv = document.createElement('div');
statusDiv.className = 'status';
const actionDiv = document.createElement('div');
actionDiv.className = 'actions';
card.appendChild(removeBtn);
card.appendChild(tsDiv);
card.appendChild(statusDiv);
card.appendChild(actionDiv);
updateCardContent(card, key, event);
return card;
}
// ─────────────────────────────────────────
// Validate existing card
// ─────────────────────────────────────────
function updateCard(key, event) {
const card = document.getElementById(`card-${key}`);
if (card) {
updateCardContent(card, key, event);
}
}
// ─────────────────────────────────────────
// Update existing card content
// ─────────────────────────────────────────
function updateCardContent(card, key, event) {
const statusDiv = card.querySelector('.status');
const actionDiv = card.querySelector('.actions');
const safeKey = key.replace(/[:.]/g, "-");
actionDiv.innerHTML = '';
if (event.status === 'started') {
statusDiv.textContent = "Received signal. Data logging started.";
card.style.backgroundColor = '#780606';
} else if (event.status === 'processed') {
statusDiv.textContent = "Data logging finished. Start cleaning process.";
card.style.backgroundColor = '#2e6930';
} else if (event.status === 'done') {
statusDiv.textContent = "Cleaned data saved. Insights is ready.";
card.style.backgroundColor = '#8a00c2';
const expandBtn = document.createElement('button');
expandBtn.className = 'btn-expand';
expandBtn.textContent = expandedItems[key] ? 'Collapse' : 'Expand';
expandBtn.onclick = () => toggleExpand(key, expandBtn);
const expandDiv = document.createElement('div');
expandDiv.id = `expand-${key}`;
expandDiv.className = 'expanded-content';
if (expandedItems[key]) expandDiv.classList.add('show');
expandDiv.innerHTML = `
`;
actionDiv.appendChild(expandBtn);
actionDiv.appendChild(expandDiv);
}
}
// ─────────────────────────────────────────
// Toggle card expansion
// ─────────────────────────────────────────
function toggleExpand(key, btn) {
const el = document.getElementById(`expand-${key}`);
const showing = el.classList.contains('show');
if (showing) {
el.classList.remove('show');
expandedItems[key] = false;
btn.textContent = 'Expand';
} else {
el.classList.add('show');
expandedItems[key] = true;
btn.textContent = 'Collapse';
}
localStorage.setItem("expandedItems", JSON.stringify(expandedItems));
}
// ─────────────────────────────────────────
// Toggle card edit-view mode
// ─────────────────────────────────────────
function toggleEditMode(container, key) {
const icon = container.querySelector('.icon-edit');
if (!container.classList.contains('editing')) {
const span = container.querySelector('.label-text');
if (!span) return;
const input = document.createElement('input');
input.type = 'text';
input.value = span.textContent;
input.className = 'label-input';
span.replaceWith(input);
icon.src = '/static/check.png';
container.classList.add('editing');
} else {
const input = container.querySelector('.label-input');
if (!input) return;
const newLabel = input.value.trim() || formatTimestamp(key);
renamedLabels[key] = newLabel;
localStorage.setItem("renamedLabels", JSON.stringify(renamedLabels));
const newSpan = document.createElement('span');
newSpan.className = 'label-text';
newSpan.textContent = newLabel;
input.replaceWith(newSpan);
icon.src = '/static/edit.png';
container.classList.remove('editing');
}
}
// ─────────────────────────────────────────
// Remove a card item
// ─────────────────────────────────────────
function removeItem(key) {
const card = document.getElementById(`card-${key}`);
if (card) card.remove();
delete expandedItems[key];
delete previousEvents[key];
localStorage.setItem("expandedItems", JSON.stringify(expandedItems));
fetch(`/events/remove/${key}`, { method: 'DELETE' });
}
// ─────────────────────────────────────────
// Format timestamp as hh:mm dd/mm/yyyy
// ─────────────────────────────────────────
function formatTimestamp(norm_ts) {
try {
const parts = norm_ts.split("T");
if (parts.length !== 2) throw new Error("Invalid format");
// Extract date and time parts
const datePart = parts[0]; // e.g., "2025-05-21"
const timeParts = parts[1].split("-"); // ["hh", "mm", "ss"]
if (timeParts.length < 3) throw new Error("Incomplete time");
// Reformat
const [year, month, day] = datePart.split("-").map(Number);
let [hour, minute, second] = timeParts.map(Number);
hour = (hour - 2 + 24) % 24;
// Create Date in local time (note: month is 0-based)
const dt = new Date(year, month - 1, day, hour, minute, second);
// Write string
const timeStr = dt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
const dateStr = dt.toLocaleDateString('en-AU');
return `${timeStr} ${dateStr}`;
} catch (err) {
console.warn("formatTimestamp fallback:", err.message);
return norm_ts;
}
}
// ─────────────────────────────────────────
// Sanitize filenames from timestamp
// ─────────────────────────────────────────
function sanitizeFilename(ts) {
return ts.replace(/:/g, '-').replace(/ /g, 'T').replace(/\//g, '-');
}
// ─────────────────────────────────────────
fetchEvents();
setInterval(fetchEvents, 1000);