diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ad8711f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +services: + caddy: + image: caddy:2 + container_name: lambdaiot-web + ports: + - "18080:80" + volumes: + - ./webroot:/srv:ro + command: ["caddy", "file-server", "--root", "/srv", "--listen", ":80"] + restart: unless-stopped diff --git a/app.js b/webroot/app.js similarity index 82% rename from app.js rename to webroot/app.js index b16075f..7b3c533 100644 --- a/app.js +++ b/webroot/app.js @@ -8,12 +8,15 @@ const state = { 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"), @@ -39,13 +42,32 @@ function setAuthMessage(message, isError = false) { 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); } } @@ -80,13 +102,38 @@ async function apiFetch(path, options = {}, authRequired = true) { async function loadDevices() { elements.deviceList.innerHTML = ""; - state.devices = []; try { const data = await apiFetch("/devices", {}, false); state.devices = Array.isArray(data) ? data : []; renderDeviceList(); } catch (error) { - elements.deviceList.innerHTML = `
  • Failed to load devices: ${error.message}
  • `; + 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."; } } @@ -99,9 +146,20 @@ function renderDeviceList() { 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}
    +
    +
    ${device.name}
    + ${statusMarkup} +
    ${device.location || "No location"}
    `; item.addEventListener("click", () => selectDevice(device)); @@ -320,6 +378,7 @@ async function handleLogin(event) { localStorage.setItem("lambdaiot.token", state.token); updateAuthUI(); setAuthMessage("Login successful."); + refreshAll(); if (state.selectedDeviceId) { loadDeviceDetails(state.selectedDeviceId); @@ -334,6 +393,7 @@ function handleLogout() { localStorage.removeItem("lambdaiot.token"); updateAuthUI(); setAuthMessage("Logged out."); + refreshAll(); elements.sensorList.innerHTML = ""; elements.actorList.innerHTML = ""; elements.sensorNote.textContent = "Login required to view sensors."; @@ -342,7 +402,7 @@ function handleLogout() { elements.loginForm.addEventListener("submit", handleLogin); elements.logoutButton.addEventListener("click", handleLogout); -elements.refreshDevices.addEventListener("click", loadDevices); +elements.refreshDevices.addEventListener("click", refreshAll); updateAuthUI(); -loadDevices(); +refreshAll(); diff --git a/index.html b/webroot/index.html similarity index 95% rename from index.html rename to webroot/index.html index 29c1115..443f167 100644 --- a/index.html +++ b/webroot/index.html @@ -23,7 +23,7 @@