frontend demo

This commit is contained in:
2026-02-07 23:59:44 +01:00
parent abb2c9838e
commit 3ad1492612
4 changed files with 144 additions and 8 deletions
+10
View File
@@ -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
+65 -5
View File
@@ -8,12 +8,15 @@ const state = {
actors: [], actors: [],
selectedDeviceId: null, selectedDeviceId: null,
readingsLimit: 50, readingsLimit: 50,
deviceRefreshIntervalId: null,
}; };
const elements = { const elements = {
authStatus: document.getElementById("authStatus"), authStatus: document.getElementById("authStatus"),
authMessage: document.getElementById("authMessage"), authMessage: document.getElementById("authMessage"),
loginForm: document.getElementById("loginForm"), loginForm: document.getElementById("loginForm"),
authBlock: document.getElementById("authBlock"),
authActions: document.getElementById("authActions"),
logoutButton: document.getElementById("logoutButton"), logoutButton: document.getElementById("logoutButton"),
deviceList: document.getElementById("deviceList"), deviceList: document.getElementById("deviceList"),
deviceMeta: document.getElementById("deviceMeta"), deviceMeta: document.getElementById("deviceMeta"),
@@ -39,13 +42,32 @@ function setAuthMessage(message, isError = false) {
elements.authMessage.style.color = isError ? "#b0412e" : "var(--muted)"; 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() { function updateAuthUI() {
if (state.token) { if (state.token) {
setAuthStatus("Signed in"); setAuthStatus("Signed in");
setAuthMessage("Token stored locally."); 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 { } else {
setAuthStatus("Not signed in"); setAuthStatus("Not signed in");
setAuthMessage("Login required for sensors, actors, and readings."); 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() { async function loadDevices() {
elements.deviceList.innerHTML = ""; elements.deviceList.innerHTML = "";
state.devices = [];
try { try {
const data = await apiFetch("/devices", {}, false); const data = await apiFetch("/devices", {}, false);
state.devices = Array.isArray(data) ? data : []; state.devices = Array.isArray(data) ? data : [];
renderDeviceList(); renderDeviceList();
} catch (error) { } catch (error) {
elements.deviceList.innerHTML = `<li>Failed to load devices: ${error.message}</li>`; 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) => { state.devices.forEach((device) => {
const item = document.createElement("li"); 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
? `<span class="device-status device-status--${statusClass}">${statusLabel}</span>`
: "";
item.classList.toggle("active", device.id === state.selectedDeviceId); item.classList.toggle("active", device.id === state.selectedDeviceId);
item.innerHTML = ` item.innerHTML = `
<div class="device-name">${device.name}</div> <div class="device-header">
<div class="device-name">${device.name}</div>
${statusMarkup}
</div>
<div class="device-location">${device.location || "No location"}</div> <div class="device-location">${device.location || "No location"}</div>
`; `;
item.addEventListener("click", () => selectDevice(device)); item.addEventListener("click", () => selectDevice(device));
@@ -320,6 +378,7 @@ async function handleLogin(event) {
localStorage.setItem("lambdaiot.token", state.token); localStorage.setItem("lambdaiot.token", state.token);
updateAuthUI(); updateAuthUI();
setAuthMessage("Login successful."); setAuthMessage("Login successful.");
refreshAll();
if (state.selectedDeviceId) { if (state.selectedDeviceId) {
loadDeviceDetails(state.selectedDeviceId); loadDeviceDetails(state.selectedDeviceId);
@@ -334,6 +393,7 @@ function handleLogout() {
localStorage.removeItem("lambdaiot.token"); localStorage.removeItem("lambdaiot.token");
updateAuthUI(); updateAuthUI();
setAuthMessage("Logged out."); setAuthMessage("Logged out.");
refreshAll();
elements.sensorList.innerHTML = ""; elements.sensorList.innerHTML = "";
elements.actorList.innerHTML = ""; elements.actorList.innerHTML = "";
elements.sensorNote.textContent = "Login required to view sensors."; elements.sensorNote.textContent = "Login required to view sensors.";
@@ -342,7 +402,7 @@ function handleLogout() {
elements.loginForm.addEventListener("submit", handleLogin); elements.loginForm.addEventListener("submit", handleLogin);
elements.logoutButton.addEventListener("click", handleLogout); elements.logoutButton.addEventListener("click", handleLogout);
elements.refreshDevices.addEventListener("click", loadDevices); elements.refreshDevices.addEventListener("click", refreshAll);
updateAuthUI(); updateAuthUI();
loadDevices(); refreshAll();
+5 -3
View File
@@ -23,7 +23,7 @@
<aside class="panel devices-panel"> <aside class="panel devices-panel">
<div class="panel-header"> <div class="panel-header">
<h2>Devices</h2> <h2>Devices</h2>
<button class="ghost" id="refreshDevices" type="button">Refresh</button> <button class="ghost" id="refreshDevices" type="button">Refresh All</button>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<ul class="device-list" id="deviceList"></ul> <ul class="device-list" id="deviceList"></ul>
@@ -36,7 +36,7 @@
<div class="device-meta" id="deviceMeta">Select a device</div> <div class="device-meta" id="deviceMeta">Select a device</div>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<section class="auth-block"> <section class="auth-block" id="authBlock">
<div class="auth-title">Sign in for protected endpoints</div> <div class="auth-title">Sign in for protected endpoints</div>
<form class="auth-form" id="loginForm"> <form class="auth-form" id="loginForm">
<input <input
@@ -54,8 +54,10 @@
required required
/> />
<button type="submit">Login</button> <button type="submit">Login</button>
<button class="ghost" type="button" id="logoutButton">Logout</button>
</form> </form>
<div class="auth-actions is-hidden" id="authActions">
<button class="ghost" type="button" id="logoutButton">Logout</button>
</div>
<div class="auth-message" id="authMessage"></div> <div class="auth-message" id="authMessage"></div>
</section> </section>
+64
View File
@@ -154,11 +154,49 @@ body {
margin-bottom: 4px; margin-bottom: 4px;
} }
.device-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.device-status {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.08em;
padding: 4px 8px;
border-radius: 999px;
border: 1px solid transparent;
font-weight: 600;
}
.device-status--pending {
background: rgba(246, 209, 84, 0.35);
color: #8a6400;
border-color: rgba(246, 209, 84, 0.7);
}
.device-status--lost {
background: rgba(176, 65, 46, 0.16);
color: #b0412e;
border-color: rgba(176, 65, 46, 0.5);
}
.device-location { .device-location {
font-size: 12px; font-size: 12px;
color: var(--muted); color: var(--muted);
} }
.device-error {
padding: 10px 18px;
margin: 0 12px;
border-radius: 14px;
background: rgba(176, 65, 46, 0.12);
color: #b0412e;
font-size: 12px;
}
.device-meta { .device-meta {
font-size: 13px; font-size: 13px;
color: var(--muted); color: var(--muted);
@@ -206,6 +244,32 @@ body {
color: var(--accent); color: var(--accent);
} }
.auth-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.auth-actions button {
padding: 10px 14px;
border-radius: 12px;
border: none;
background: var(--accent-2);
color: #f7fbfb;
font-weight: 600;
cursor: pointer;
}
.auth-actions button.ghost {
background: transparent;
border: 1px solid var(--accent-2);
color: var(--accent-2);
}
.is-hidden {
display: none;
}
.auth-message { .auth-message {
margin-top: 10px; margin-top: 10px;
font-size: 12px; font-size: 12px;