dooris/app/src/assets/main.ts
kritzl 8281848215
All checks were successful
Build Container / Build Container (push) Successful in 1m27s
use server sent events instead of polling
2026-05-19 17:09:02 +02:00

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()
})