import {Fetcher} from "openapi-typescript-fetch" import type {paths} from "../api/schema" const fetcher = Fetcher.for() const list: HTMLDivElement = document.querySelector("#list")! const template: HTMLTemplateElement = document.querySelector("#template-door")! export type DoorType = { id: string; label: string; state: "unlocked" | "locked" | "unknown" | "unlocking" | "locking"; batteryLow: boolean; unreachable: boolean; jammed: boolean; } export type AuthType = { username: string; authorized: boolean; authenticated: boolean; until: Date | null; recentLogout: boolean; } declare global { interface Window { lang: string; doors: Array; auth: AuthType; doorAction: (action: 'unlock' | 'lock', doorId: string) => void; } } const auth: AuthType = { username: "user", authorized: true, authenticated: false, until: null, recentLogout: false, } const apiError: { current: "serverError" | "networkError" | null; } = { current: null, } const loading: { doors: boolean; auth: boolean; } = { doors: false, auth: false, } const timeouts: Record = { auth: null, } const doors: Array = [] window.doors = doors window.auth = auth function triggerAuthTimeout() { const diff = auth.until ? (auth.until.getTime() - new Date().getTime()) : 0 if (timeouts.auth) clearTimeout(timeouts.auth) timeouts.auth = setTimeout(checkUser, diff) } function loadAuthFromLocalStorage() { if (localStorage.getItem("auth")) { const localAuth = JSON.parse(localStorage.getItem("auth")!) auth.recentLogout = localAuth.recentLogout // set recentLogout true, if user was logged in before auth.authenticated = localAuth.authenticated auth.authorized = localAuth.authorized auth.until = localAuth.until ? new Date(localAuth.until) : null auth.username = localAuth.username } } async function checkUser() { if (!auth.authenticated) { loading.auth = true } const getUserInfo = fetcher.path("/api/user-info/").method("get").create() try { const {data: userInfo} = await getUserInfo({}) apiError.current = null auth.authenticated = true auth.authorized = userInfo.is_authorized auth.until = userInfo.guaranteed_session_until ? new Date(userInfo.guaranteed_session_until) : null auth.username = userInfo.username ?? "" auth.recentLogout = false triggerAuthTimeout() } catch (e) { // check which operation threw the exception if (e instanceof getUserInfo.Error) { const error = e.getActualType() if (error.status === 401) { if (!auth.recentLogout) auth.recentLogout = auth.authenticated // set recentLogout true, if user was logged in before auth.authenticated = false auth.authorized = false auth.until = null auth.username = "" } else if (error.status >= 500 && error.status < 600) { apiError.current = "serverError" } else { console.error("unknown error:", error) } } } finally { localStorage.setItem("auth", JSON.stringify(auth)) loading.auth = false refresh() } } async function fetchDoors() { if (doors.length === 0) { loading.doors = true } refresh() const getDoors = fetcher.path("/api/locks/").method("get").create() try { const {data: doorInfo} = await getDoors({}) apiError.current = null while (doors.length) { doors.pop() } doorInfo.forEach(door => { let state: DoorType["state"] = "unknown" switch (door.status.activity_state) { case "locking": case "unlocking": case "unknown": state = door.status.activity_state break case "stable": state = door.status.lock_state } doors.push({ id: door.id, label: door.name, state: state, batteryLow: door.status.is_low_battery, unreachable: door.status.is_unreachable, jammed: door.status.is_error_jammed, }) }) } catch (e) { // check which operation threw the exception if (e instanceof getDoors.Error) { const error = e.getActualType() if (error.status === 401) { console.log("unauthorized") } else if (error.status >= 500 && error.status < 600) { apiError.current = "serverError" clearInterval(doorsInterval) } else { console.error("unknown error:", error) } } if (e instanceof Error) { switch (e.name) { case "TypeError": apiError.current = "networkError" } } } finally { loading.doors = false refresh() } } async function doorAction(action: "unlock" | "lock", doorId: string) { const operateDoors = fetcher.path("/api/locks/{lock_id}").method("patch").create() const stateMap: Record = { unlock: "open", lock: "closed", } operateDoors({ lock_id: doorId, desired_state: stateMap[action], }) } window.doorAction = doorAction function setDoorInfo(doorElement: HTMLDivElement, door: DoorType, initial: boolean = false) { const labelElement: HTMLDivElement = doorElement.querySelector("[data-label]")! const lockButton: HTMLButtonElement = doorElement.querySelector("[data-button='lock']")! const unlockButton: HTMLButtonElement = doorElement.querySelector("[data-button='unlock']")! const buttonElements: Array = [lockButton, unlockButton] doorElement.dataset.id = door.id doorElement.dataset.state = door.state if (auth.authenticated && auth.authorized) { doorElement.dataset.active = "" } else { delete doorElement.dataset.active } if (door.batteryLow) { doorElement.dataset.battery = "" } else { delete doorElement.dataset.battery } if (door.unreachable) { doorElement.dataset.unreachable = "" } else { delete doorElement.dataset.unreachable } if (door.jammed) { doorElement.dataset.jammed = "" } else { delete doorElement.dataset.jammed } labelElement.innerHTML = door.label if (initial) { lockButton.addEventListener("click", () => { doorAction("lock", door.id) }) unlockButton.addEventListener("click", () => { doorAction("unlock", door.id) }) } buttonElements.forEach(button => { button.disabled = ["unlocking", "locking"].includes(door.state) || apiError.current !== null }) } function refresh() { for (const door of doors) { let doorElement: HTMLDivElement | null = list.querySelector(`[data-id='${door.id}']`) if (doorElement) { setDoorInfo(doorElement, door) continue } const targetElement = document.importNode(template.content, true) doorElement = targetElement.querySelector("[data-state]")! setDoorInfo(doorElement, door, true) list.appendChild(targetElement) } const alertUnauthenticated: HTMLDivElement = document.querySelector("#alert-unauthenticated")! const alertUnauthorized: HTMLDivElement = document.querySelector("#alert-unauthorized")! const alertLoggedout: HTMLDivElement = document.querySelector("#alert-loggedout")! const alertServerError: HTMLDivElement = document.querySelector("#alert-serverError")! const alertNetworkError: HTMLDivElement = document.querySelector("#alert-networkError")! const alertDivider: HTMLDivElement = document.querySelector("#alert-divider")! const badgeUsername: HTMLDivElement = document.querySelector("#badge-username")! const buttonLogin: HTMLDivElement = document.querySelector("#button-login")! const usernameElement: HTMLSpanElement = badgeUsername.querySelector("#username")! const loadingDoors: HTMLDivElement = document.querySelector("#loading-doors")! alertUnauthenticated.classList.toggle("hidden", auth.authenticated || auth.recentLogout) alertUnauthorized.classList.toggle("hidden", !auth.authenticated || auth.authorized) alertLoggedout.classList.toggle("hidden", !auth.recentLogout) alertServerError.classList.toggle("hidden", apiError.current !== "serverError") alertNetworkError.classList.toggle("hidden", apiError.current !== "networkError") alertDivider.classList.toggle("hidden", auth.authenticated && auth.authorized && !auth.recentLogout && apiError.current === null || doors.length === 0) badgeUsername.classList.toggle("hidden", !auth.authenticated) usernameElement.innerHTML = auth.username buttonLogin.classList.toggle("hidden", auth.authenticated) loadingDoors.classList.toggle("hidden", !loading.doors) } loadAuthFromLocalStorage() const doorsInterval = setInterval(fetchDoors, 250) // TODO: replace with SSE checkUser() document.addEventListener("loadeddata", () => { refresh() })