fetch door data from api; improved error handling

This commit is contained in:
kritzl 2026-05-11 14:48:18 +02:00
commit 5fed0859af
Signed by: kritzl
SSH key fingerprint: SHA256:5BmINP9VjZWaUk5Z+2CTut1KFhwLtd0ZynMekKbtViM
3 changed files with 243 additions and 170 deletions

View file

@ -24,6 +24,11 @@ export const ui = {
"button.open": "Open", "button.open": "Open",
"button.close": "Close", "button.close": "Close",
"login": "Login", "login": "Login",
"loggedOut.title": "Signed out",
"loggedOut.description": `Your Session is expired and you were logged out.
Log in again: <a class='underline' href='/auth/login'>Login</a>`,
"serverError.title": "Server Error",
"serverError.description": `Please try again later.`,
}, },
de: { de: {
"unauthorized.title": "Nicht berechtigt", "unauthorized.title": "Nicht berechtigt",
@ -43,5 +48,10 @@ export const ui = {
"button.open": "Öffnen", "button.open": "Öffnen",
"button.close": "Schließen", "button.close": "Schließen",
"login": "Anmelden", "login": "Anmelden",
"loggedOut.title": "Abgemeldet",
"loggedOut.description": `Deine Sitzung ist abgelaufen und du wurdest abgemeldet.
Melde dich hier erneut an: <a class='underline' href='/auth/login'>Anmelden</a>`,
"serverError.title": "Serverfehler",
"serverError.description": `Bitte versuche es später erneut.`,
}, },
} as const } as const

View file

@ -15,6 +15,7 @@ const t = useTranslations(lang)
<link rel="icon" href="/favicon.ico"/> <link rel="icon" href="/favicon.ico"/>
<meta name="generator" content={Astro.generator}/> <meta name="generator" content={Astro.generator}/>
<title>dooris</title> <title>dooris</title>
<slot name="head"/>
</head> </head>
<body class="min-h-screen"> <body class="min-h-screen">
<header class="p-2"> <header class="p-2">

View file

@ -18,7 +18,224 @@ const lang = getLangFromUrl(Astro.url)
const t = useTranslations(lang) const t = useTranslations(lang)
--- ---
<Layout username=""> <Layout>
<Fragment slot="head">
<script>
import {Fetcher} from "openapi-typescript-fetch"
import {type paths} from "../../api/schema"
const fetcher = Fetcher.for<paths>()
const list: HTMLDivElement = document.querySelector("#list")!
const template: HTMLTemplateElement = document.querySelector("#template-door")!
type DoorType = {
id: string;
label: string;
state: "unlocked" | "locked" | "unknown" | "unlocking" | "locking";
batteryLow: boolean;
unreachable: boolean;
jammed: boolean;
}
type AuthType = {
username: string;
authorized: boolean;
authenticated: boolean;
until: Date | null;
recentLogout: boolean;
}
const auth: AuthType = {
username: "user",
authorized: true,
authenticated: false,
until: null,
recentLogout: false,
}
const apiError: {
current: "serverError" | null;
} = {
current: null,
}
const doors: Array<DoorType> = []
function loadAuthFromLocalStorage() {
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() {
const getUserInfo = fetcher.path("/api/user-info/").method("get").create()
try {
const {status, data: userInfo} = await getUserInfo({})
apiError.current = null
auth.authenticated = userInfo.is_logged_in
auth.authorized = true
auth.until = userInfo.guaranteed_session_until ? new Date(userInfo.guaranteed_session_until) : null
auth.username = userInfo.user_info?.username ?? ""
auth.recentLogout = false
} catch (e) {
// check which operation threw the exception
if (e instanceof getUserInfo.Error) {
const error = e.getActualType()
if (error.status === 401) {
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))
refresh()
}
}
async function fetchDoors() {
const getDoors = fetcher.path("/api/locks/").method("get").create()
try {
const {status, 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.name, // TODO: replace by actual 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"
} else {
console.error("unknown error:", error)
}
}
} finally {
refresh()
}
}
function setDoorInfo(doorElement: HTMLDivElement, door: DoorType) {
const labelElement: HTMLDivElement = doorElement.querySelector("[data-label]")!
const buttonElements: Array<HTMLButtonElement> = Array.from(doorElement.querySelectorAll("button"))
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
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)
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 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")!
alertUnauthenticated.classList.toggle("hidden", auth.authenticated)
alertUnauthorized.classList.toggle("hidden", !auth.authenticated || auth.authorized)
alertLoggedout.classList.toggle("hidden", !auth.recentLogout)
alertServerError.classList.toggle("hidden", apiError.current !== "serverError")
alertDivider.classList.toggle("hidden", auth.authenticated && auth.authorized && !auth.recentLogout && apiError.current === null)
badgeUsername.classList.toggle("hidden", !auth.authenticated)
usernameElement.innerHTML = auth.username
buttonLogin.classList.toggle("hidden", auth.authenticated)
}
loadAuthFromLocalStorage()
fetchDoors()
checkUser()
document.addEventListener("loadeddata", () => {
refresh()
})
</script>
</Fragment>
<div class="grid place-items-center grid-cols-1 gap-4 max-w-xl mx-auto" id="list"> <div class="grid place-items-center grid-cols-1 gap-4 max-w-xl mx-auto" id="list">
<Alert <Alert
classList="hidden" classList="hidden"
@ -42,177 +259,22 @@ const t = useTranslations(lang)
<Lock class="size-7"/> <Lock class="size-7"/>
</Fragment> </Fragment>
</Alert> </Alert>
<Alert
classList="hidden"
id="alert-loggedout"
color="warning"
title={t("loggedOut.title")}
description={t("loggedOut.description")}
/>
<Alert
classList="hidden"
id="alert-serverError"
color="error"
title={t("serverError.title")}
description={t("serverError.description")}
/>
<div class="divider w-full my-0" id="alert-divider"></div> <div class="divider w-full my-0" id="alert-divider"></div>
</div> </div>
<DoorTemplate state="unknown"/> <DoorTemplate state="unknown"/>
<script>
import {Fetcher} from "openapi-typescript-fetch"
import {type paths} from "../../api/schema"
const fetcher = Fetcher.for<paths>()
const list: HTMLDivElement = document.querySelector("#list")!
const template: HTMLTemplateElement = document.querySelector("#template-door")!
type DoorType = {
id: string;
label: string;
state: "unlocked" | "locked" | "unknown" | "unlocking" | "locking";
batteryLow: boolean;
unreachable: boolean;
jammed: boolean;
}
type AuthType = {
username: string;
authorized: boolean;
authenticated: boolean;
until: Date | null;
}
const auth: AuthType = {
username: "user",
authorized: true,
authenticated: false,
until: null,
}
const doors: Array<DoorType> = [
{
id: "abcdef",
label: "Hauptraum",
state: "unlocked",
batteryLow: false,
unreachable: false,
jammed: false,
},
{
id: "12345",
label: "Werkstatt",
state: "locked",
batteryLow: true,
unreachable: false,
jammed: false,
},
]
async function checkUser() {
const getUserInfo = fetcher.path("/api/user-info/").method("get").create()
try {
const {status, data: userInfo} = await getUserInfo({})
auth.authenticated = userInfo.is_logged_in
auth.authorized = true
auth.until = userInfo.guaranteed_session_until ? new Date(userInfo.guaranteed_session_until) : null
auth.username = userInfo.user_info?.username ?? ''
} catch (e) {
// check which operation threw the exception
if (e instanceof getUserInfo.Error) {
const error = e.getActualType()
if (error.status === 401) {
auth.authenticated = false
auth.authorized = false
auth.until = null
auth.username = ''
} else {
console.log('unknown error')
}
}
} finally {
refresh()
}
}
function setDoorInfo(doorElement: HTMLDivElement, door: DoorType) {
const labelElement: HTMLDivElement = doorElement.querySelector("[data-label]")!
const buttonElements: Array<HTMLButtonElement> = Array.from(doorElement.querySelectorAll("button"))
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
buttonElements.forEach(button => {
button.disabled = ["unlocking", "locking"].includes(door.state)
})
}
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)
list.appendChild(targetElement)
}
const alertUnauthenticated: HTMLDivElement = document.querySelector("#alert-unauthenticated")!
const alertUnauthorized: HTMLDivElement = document.querySelector("#alert-unauthorized")!
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")!
alertUnauthenticated.classList.toggle("hidden", auth.authenticated)
alertUnauthorized.classList.toggle("hidden", !auth.authenticated || auth.authorized)
alertDivider.classList.toggle("hidden", auth.authenticated && auth.authorized)
badgeUsername.classList.toggle("hidden", !auth.authenticated)
usernameElement.innerHTML = auth.username
buttonLogin.classList.toggle("hidden", auth.authenticated)
}
refresh()
checkUser()
setTimeout(() => {
doors[0] = {
id: "abcdef",
label: "Hauptraum",
state: "unknown",
batteryLow: true,
unreachable: false,
jammed: true,
}
doors[1] = {
id: "12345",
label: "Werkstatt",
state: "unlocking",
batteryLow: false,
unreachable: true,
jammed: false,
}
refresh()
}, 2000)
</script>
</Layout> </Layout>