frontend demo
This commit is contained in:
@@ -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
@@ -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();
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user