diff --git a/src/static/admin.js b/src/static/admin.js
index 5e8c88b..07da280 100644
--- a/src/static/admin.js
+++ b/src/static/admin.js
@@ -27,7 +27,7 @@ async function api(method, path, body) {
if (res.status === 401) {
clearToken();
showLogin();
- throw new Error("Session expired.");
+ throw new Error("Session expired or invalid credentials");
}
if (!res.ok) {
const j = await res.json().catch(() => null);
@@ -53,7 +53,9 @@ function showLogin() {
function showAdmin() {
document.getElementById("login-view").classList.add("hidden");
document.getElementById("admin-view").classList.remove("hidden");
- const isAdmin = _tokenPayload().scope === "admin";
+ const payload = _tokenPayload();
+ const isAdmin = payload.scope === "admin";
+ document.getElementById("header-username").textContent = payload.sub || "";
document.querySelectorAll(".admin-only").forEach(el => {
el.style.display = isAdmin ? "" : "none";
});
@@ -504,11 +506,21 @@ async function loadAdmins() {
const tr = document.createElement("tr");
tr.innerHTML = `
${esc(u.username)}${u.username === me ? ' you' : ""} ${roleBadge} |
-
+ |
+
${u.username !== me ? `` : ""}
| `;
tbody.appendChild(tr);
}
+ tbody.querySelectorAll("[data-chpw]").forEach(btn => {
+ btn.addEventListener("click", () => {
+ document.getElementById("chpw-username").value = btn.dataset.chpw;
+ document.getElementById("chpw-new").value = "";
+ document.getElementById("chpw-confirm").value = "";
+ document.getElementById("chpw-error").classList.add("hidden");
+ document.getElementById("chpw-modal").classList.remove("hidden");
+ });
+ });
tbody.querySelectorAll("[data-del-admin]").forEach(btn => {
btn.addEventListener("click", async () => {
if (!confirm(`Delete admin "${btn.dataset.delAdmin}"?`)) return;
@@ -549,6 +561,32 @@ document.getElementById("admin-form").addEventListener("submit", async e => {
}
});
+// ── Change password modal ─────────────────────────────────────────────────────
+document.getElementById("chpw-cancel").addEventListener("click", () => {
+ document.getElementById("chpw-modal").classList.add("hidden");
+});
+document.getElementById("chpw-form").addEventListener("submit", async e => {
+ e.preventDefault();
+ const username = document.getElementById("chpw-username").value;
+ const newPw = document.getElementById("chpw-new").value;
+ const confirm = document.getElementById("chpw-confirm").value;
+ const errEl = document.getElementById("chpw-error");
+ errEl.classList.add("hidden");
+ if (newPw !== confirm) {
+ errEl.textContent = "Passwords do not match";
+ errEl.classList.remove("hidden");
+ return;
+ }
+ try {
+ await api("PATCH", `/api/admin/admins/${encodeURIComponent(username)}/password`, { new_password: newPw });
+ document.getElementById("chpw-modal").classList.add("hidden");
+ showToast("Password updated");
+ } catch (e) {
+ errEl.textContent = e.message;
+ errEl.classList.remove("hidden");
+ }
+});
+
// ── Load all data ─────────────────────────────────────────────────────────────
function loadAllData() {
const isAdmin = _tokenPayload().scope === "admin";
diff --git a/src/static/app.js b/src/static/app.js
index f480cb0..89afa13 100644
--- a/src/static/app.js
+++ b/src/static/app.js
@@ -28,7 +28,7 @@ async function apiFetch(method, path, body) {
if (res.status === 401) {
clearToken();
showLogin();
- throw new Error("Session expired - please log in again.");
+ throw new Error("Session expired or invalid keypass");
}
if (!res.ok) {
const json = await res.json().catch(() => null);
@@ -71,7 +71,7 @@ function renderGates(gates) {
btn.className = `gate-btn ${gate.gate_type}`;
btn.dataset.gateId = gate.id;
btn.innerHTML = `
${icon}${gate.name}`;
- btn.addEventListener("click", () => handleOpenGate(btn, gate.id));
+ btn.addEventListener("click", () => handleOpenGate(btn, gate));
grid.appendChild(btn);
}
}
@@ -87,7 +87,28 @@ async function loadGates() {
}
// ── Open gate action ──────────────────────────────────────────────────────────
-async function handleOpenGate(btn, gateId) {
+let _pendingGate = null;
+
+document.getElementById("confirm-cancel").addEventListener("click", () => {
+ document.getElementById("confirm-modal").classList.add("hidden");
+ _pendingGate = null;
+});
+document.getElementById("confirm-ok").addEventListener("click", () => {
+ document.getElementById("confirm-modal").classList.add("hidden");
+ if (_pendingGate) {
+ const { btn, gateId } = _pendingGate;
+ _pendingGate = null;
+ _doOpenGate(btn, gateId);
+ }
+});
+
+async function handleOpenGate(btn, gate) {
+ _pendingGate = { btn, gateId: gate.id };
+ document.getElementById("confirm-gate-name").textContent = gate.name;
+ document.getElementById("confirm-modal").classList.remove("hidden");
+}
+
+async function _doOpenGate(btn, gateId) {
btn.disabled = true;
btn.classList.add("loading");
btn.classList.remove("ok", "fail");
diff --git a/src/static/index.html b/src/static/index.html
index 752ee10..97b008d 100644
--- a/src/static/index.html
+++ b/src/static/index.html
@@ -131,6 +131,18 @@
+
+
+
+
+
Open this gate?
+
+
+
+
+
+
+