const BASE_URL = "http://localhost:8080"; const ACTION_WRITE = "write"; const state = { token: localStorage.getItem("lambdaiot.token") || "", devices: [], sensors: [], actors: [], selectedDeviceId: null, readingsLimit: 50, deviceRefreshIntervalId: null, }; const elements = { authStatus: document.getElementById("authStatus"), authMessage: document.getElementById("authMessage"), loginForm: document.getElementById("loginForm"), authBlock: document.getElementById("authBlock"), authActions: document.getElementById("authActions"), logoutButton: document.getElementById("logoutButton"), deviceList: document.getElementById("deviceList"), deviceMeta: document.getElementById("deviceMeta"), sensorList: document.getElementById("sensorList"), actorList: document.getElementById("actorList"), sensorNote: document.getElementById("sensorNote"), actorNote: document.getElementById("actorNote"), refreshDevices: document.getElementById("refreshDevices"), }; const templates = { sensorCard: document.getElementById("sensorCardTemplate"), actorCard: document.getElementById("actorCardTemplate"), readingRow: document.getElementById("readingRowTemplate"), }; function setAuthStatus(message) { elements.authStatus.textContent = message; } function setAuthMessage(message, isError = false) { elements.authMessage.textContent = message; elements.authMessage.style.color = isError ? "#b0412e" : "var(--muted)"; } function setDeviceRefreshInterval(enabled) { if (state.deviceRefreshIntervalId) { clearInterval(state.deviceRefreshIntervalId); state.deviceRefreshIntervalId = null; } if (enabled) { state.deviceRefreshIntervalId = setInterval(loadDevices, 15000); } } function updateAuthUI() { if (state.token) { setAuthStatus("Signed in"); setAuthMessage("Token stored locally."); elements.authBlock.classList.add("is-hidden"); elements.loginForm.classList.add("is-hidden"); elements.authActions.classList.remove("is-hidden"); setDeviceRefreshInterval(false); } else { setAuthStatus("Not signed in"); setAuthMessage("Login required for sensors, actors, and readings."); elements.authBlock.classList.remove("is-hidden"); elements.loginForm.classList.remove("is-hidden"); elements.authActions.classList.add("is-hidden"); setDeviceRefreshInterval(true); } } async function apiFetch(path, options = {}, authRequired = true) { const headers = new Headers(options.headers || {}); if (authRequired) { if (!state.token) { throw new Error("Missing JWT token."); } headers.set("Authorization", `Bearer ${state.token}`); } if (options.body && !headers.has("Content-Type")) { headers.set("Content-Type", "application/json"); } const response = await fetch(`${BASE_URL}${path}`, { ...options, headers, }); if (!response.ok) { const text = await response.text(); throw new Error(text || `Request failed with status ${response.status}`); } if (response.status === 204) { return null; } return response.json(); } async function loadDevices() { elements.deviceList.innerHTML = ""; try { const data = await apiFetch("/devices", {}, false); state.devices = Array.isArray(data) ? data : []; renderDeviceList(); } catch (error) { const errorItem = document.createElement("li"); errorItem.className = "device-error"; errorItem.textContent = `Failed to load devices: ${error.message}`; elements.deviceList.prepend(errorItem); } } async function refreshAll() { await loadDevices(); if (!state.token || !state.selectedDeviceId) { return; } const selectedDevice = state.devices.find( (device) => device.id === state.selectedDeviceId ); if (selectedDevice) { selectDevice(selectedDevice); } else { state.selectedDeviceId = null; elements.deviceMeta.textContent = "Select a device"; elements.sensorList.innerHTML = ""; elements.actorList.innerHTML = ""; elements.sensorNote.textContent = "No device selected."; elements.actorNote.textContent = "No device selected."; } } function renderDeviceList() { elements.deviceList.innerHTML = ""; if (state.devices.length === 0) { elements.deviceList.innerHTML = "
  • No devices found.
  • "; return; } state.devices.forEach((device) => { const item = document.createElement("li"); const statusLabel = device.status_id === 2 ? "Pending" : device.status_id === 3 ? "Lost" : ""; const statusClass = device.status_id === 2 ? "pending" : device.status_id === 3 ? "lost" : ""; const statusMarkup = statusLabel ? `${statusLabel}` : ""; item.classList.toggle("active", device.id === state.selectedDeviceId); item.innerHTML = `
    ${device.name}
    ${statusMarkup}
    ${device.location || "No location"}
    `; item.addEventListener("click", () => selectDevice(device)); elements.deviceList.appendChild(item); }); } function selectDevice(device) { state.selectedDeviceId = device.id; renderDeviceList(); elements.deviceMeta.textContent = `${device.name} • ${device.description || "No description"}`; loadDeviceDetails(device.id); } async function loadDeviceDetails(deviceId) { elements.sensorList.innerHTML = ""; elements.actorList.innerHTML = ""; elements.sensorNote.textContent = "Loading sensors..."; elements.actorNote.textContent = "Loading actors..."; if (!state.token) { elements.sensorNote.textContent = "Login required to view sensors."; elements.actorNote.textContent = "Login required to view actors."; return; } try { const [sensors, actors] = await Promise.all([ apiFetch("/sensors"), apiFetch("/actors"), ]); state.sensors = (sensors || []).filter((sensor) => sensor.device_id === deviceId); state.actors = (actors || []).filter((actor) => actor.device_id === deviceId); await renderSensors(); renderActors(); } catch (error) { elements.sensorNote.textContent = `Failed to load sensors: ${error.message}`; elements.actorNote.textContent = `Failed to load actors: ${error.message}`; } } async function renderSensors() { elements.sensorList.innerHTML = ""; if (state.sensors.length === 0) { elements.sensorNote.textContent = "No sensors for this device."; return; } elements.sensorNote.textContent = `${state.sensors.length} sensor(s)`; const readingsPromises = state.sensors.map(async (sensor) => { try { const readings = await apiFetch( `/sensor-readings?sensor_id=${sensor.id}&limit=${state.readingsLimit}&page=0` ); return { sensorId: sensor.id, readings: readings || [] }; } catch (error) { return { sensorId: sensor.id, readings: [], error: error.message }; } }); const readingsResults = await Promise.all(readingsPromises); const readingsMap = new Map(readingsResults.map((entry) => [entry.sensorId, entry])); state.sensors.forEach((sensor) => { const card = templates.sensorCard.content.cloneNode(true); const title = card.querySelector(".card-title"); const subtitle = card.querySelector(".card-subtitle"); const pill = card.querySelector(".pill"); const list = card.querySelector(".readings-list"); title.textContent = sensor.name; subtitle.textContent = `${sensor.type} • ${sensor.id}`; pill.textContent = sensor.data_type_id === 1 ? "bool" : "float"; const readingsEntry = readingsMap.get(sensor.id); if (!readingsEntry || readingsEntry.readings.length === 0) { list.textContent = readingsEntry?.error ? `Error: ${readingsEntry.error}` : "No readings yet."; } else { readingsEntry.readings.forEach((reading) => { const row = templates.readingRow.content.cloneNode(true); row.querySelector(".reading-value").textContent = reading.value; row.querySelector(".reading-time").textContent = new Date( reading.value_at ).toLocaleString(); list.appendChild(row); }); } elements.sensorList.appendChild(card); }); } function renderActors() { elements.actorList.innerHTML = ""; if (state.actors.length === 0) { elements.actorNote.textContent = "No actors for this device."; return; } elements.actorNote.textContent = `${state.actors.length} actor(s)`; state.actors.forEach((actor) => { const card = templates.actorCard.content.cloneNode(true); const cardRoot = card.querySelector(".card"); const title = card.querySelector(".card-title"); const subtitle = card.querySelector(".card-subtitle"); const pill = card.querySelector(".pill"); const body = card.querySelector(".card-body"); if (cardRoot) { cardRoot.dataset.actorId = actor.id; } title.textContent = actor.name; subtitle.textContent = `${actor.type} • ${actor.id}`; pill.textContent = actor.data_type_id === 1 ? "bool" : "float"; const controls = document.createElement("div"); controls.className = "actor-controls"; if (actor.data_type_id === 1) { const onButton = document.createElement("button"); onButton.textContent = "On"; onButton.addEventListener("click", () => writeActor(actor, 1)); const offButton = document.createElement("button"); offButton.className = "secondary"; offButton.textContent = "Off"; offButton.addEventListener("click", () => writeActor(actor, 0)); controls.appendChild(onButton); controls.appendChild(offButton); } else { const input = document.createElement("input"); input.type = "number"; input.step = "any"; input.placeholder = "Value"; const setButton = document.createElement("button"); setButton.textContent = "Set"; setButton.addEventListener("click", () => { const value = Number(input.value); if (Number.isNaN(value)) { showActorResponse(body, "Enter a numeric value.", true); return; } writeActor(actor, value); }); controls.appendChild(input); controls.appendChild(setButton); } body.appendChild(controls); const response = document.createElement("div"); response.className = "actor-response"; body.appendChild(response); elements.actorList.appendChild(card); }); } function showActorResponse(container, message, isError = false) { if (!container) return; const response = container.querySelector(".actor-response"); if (!response) return; response.textContent = message; response.style.color = isError ? "#b0412e" : "var(--muted)"; } async function writeActor(actor, value) { try { const payload = { action: ACTION_WRITE, value }; await apiFetch(`/actors/${actor.id}/write`, { method: "POST", body: JSON.stringify(payload), }); const card = findActorCard(actor.id); showActorResponse(card, "Command published."); } catch (error) { const card = findActorCard(actor.id); showActorResponse(card, `Error: ${error.message}`, true); } } function findActorCard(actorId) { const cards = Array.from(elements.actorList.querySelectorAll(".card")); return cards.find((card) => card.dataset.actorId === actorId); } async function handleLogin(event) { event.preventDefault(); const username = document.getElementById("username").value.trim(); const password = document.getElementById("password").value.trim(); if (!username || !password) { setAuthMessage("Enter both username and password.", true); return; } try { const result = await apiFetch( "/login", { method: "POST", body: JSON.stringify({ username, password }), }, false ); state.token = result.token || ""; localStorage.setItem("lambdaiot.token", state.token); updateAuthUI(); setAuthMessage("Login successful."); refreshAll(); if (state.selectedDeviceId) { loadDeviceDetails(state.selectedDeviceId); } } catch (error) { setAuthMessage(`Login failed: ${error.message}`, true); } } function handleLogout() { state.token = ""; localStorage.removeItem("lambdaiot.token"); updateAuthUI(); setAuthMessage("Logged out."); refreshAll(); elements.sensorList.innerHTML = ""; elements.actorList.innerHTML = ""; elements.sensorNote.textContent = "Login required to view sensors."; elements.actorNote.textContent = "Login required to view actors."; } elements.loginForm.addEventListener("submit", handleLogin); elements.logoutButton.addEventListener("click", handleLogout); elements.refreshDevices.addEventListener("click", refreshAll); updateAuthUI(); refreshAll();