All checks were successful
Build Container / Build Container (push) Successful in 1m27s
289 lines
No EOL
8.7 KiB
TypeScript
289 lines
No EOL
8.7 KiB
TypeScript
import {Fetcher} from "openapi-typescript-fetch"
|
|
import type {components, paths} from "../api/schema"
|
|
import type {ui} from "../i18n/ui.ts"
|
|
|
|
const fetcher = Fetcher.for<paths>()
|
|
|
|
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: keyof typeof ui;
|
|
doors: Array<DoorType>;
|
|
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<string, number | null> = {
|
|
auth: null,
|
|
}
|
|
|
|
const doors: Array<DoorType> = []
|
|
|
|
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 >= 500 && error.status < 600) {
|
|
apiError.current = "serverError"
|
|
}
|
|
|
|
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 = ""
|
|
|
|
}
|
|
} finally {
|
|
localStorage.setItem("auth", JSON.stringify(auth))
|
|
loading.auth = false
|
|
refresh()
|
|
}
|
|
}
|
|
|
|
async function subscribeDoorEvents() {
|
|
if (doors.length === 0) {
|
|
loading.doors = true
|
|
}
|
|
refresh()
|
|
|
|
const evtSource = new EventSource("/api/locks/stream")
|
|
|
|
evtSource.onerror = () => {
|
|
if (!window.navigator.onLine) {
|
|
apiError.current = "networkError"
|
|
} else {
|
|
apiError.current = "serverError"
|
|
}
|
|
}
|
|
|
|
evtSource.onmessage = (event) => {
|
|
const doorInfo: Array<components["schemas"]["Lock"]> = JSON.parse(event.data)
|
|
|
|
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,
|
|
})
|
|
})
|
|
|
|
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<string, "open" | "closed"> = {
|
|
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<HTMLButtonElement> = [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()
|
|
subscribeDoorEvents()
|
|
checkUser()
|
|
|
|
document.addEventListener("loadeddata", () => {
|
|
refresh()
|
|
}) |