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: [],
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 = `<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) => {
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.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>
`;
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();
+5 -3
View File
@@ -23,7 +23,7 @@
<aside class="panel devices-panel">
<div class="panel-header">
<h2>Devices</h2>
<button class="ghost" id="refreshDevices" type="button">Refresh</button>
<button class="ghost" id="refreshDevices" type="button">Refresh All</button>
</div>
<div class="panel-body">
<ul class="device-list" id="deviceList"></ul>
@@ -36,7 +36,7 @@
<div class="device-meta" id="deviceMeta">Select a device</div>
</div>
<div class="panel-body">
<section class="auth-block">
<section class="auth-block" id="authBlock">
<div class="auth-title">Sign in for protected endpoints</div>
<form class="auth-form" id="loginForm">
<input
@@ -54,8 +54,10 @@
required
/>
<button type="submit">Login</button>
<button class="ghost" type="button" id="logoutButton">Logout</button>
</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>
</section>
+64
View File
@@ -154,11 +154,49 @@ body {
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 {
font-size: 12px;
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 {
font-size: 13px;
color: var(--muted);
@@ -206,6 +244,32 @@ body {
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 {
margin-top: 10px;
font-size: 12px;