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
|
||||||
+64
-4
@@ -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-header">
|
||||||
<div class="device-name">${device.name}</div>
|
<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();
|
||||||
@@ -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>
|
||||||
|
|
||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user