From d19a26e128563871bc4078535ff36daee5e06124 Mon Sep 17 00:00:00 2001 From: kritzl Date: Thu, 14 May 2026 16:55:53 +0200 Subject: [PATCH] update api, operate doors --- .gitignore | 1 + app/src/api/schema.ts | 117 +++++++++++++++++++++++++- app/src/assets/main.ts | 43 ++++++++-- app/src/components/DoorTemplate.astro | 4 +- 4 files changed, 154 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index bf53948..4ad5c33 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .envrc .direnv/ +.env **/__pycache__ api/dist diff --git a/app/src/api/schema.ts b/app/src/api/schema.ts index 87104a6..c9b7069 100644 --- a/app/src/api/schema.ts +++ b/app/src/api/schema.ts @@ -55,6 +55,23 @@ export interface paths { patch?: never; trace?: never; }; + "/auth/logout": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Logout */ + get: operations["logout_auth_logout_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/locks/": { parameters: { query?: never; @@ -72,6 +89,23 @@ export interface paths { patch?: never; trace?: never; }; + "/api/locks/{lock_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** Operate Lock */ + patch: operations["operate_lock_api_locks__lock_id__patch"]; + trace?: never; + }; } export type webhooks = Record; export interface components { @@ -101,13 +135,23 @@ export interface components { * @description Statically known HTTP problem types using the [type URI scheme](https://datatracker.ietf.org/doc/rfc4151/) * @enum {string} */ - HttpProblemType: "type:noc@hamburg.ccc.de,2026:UNAUTHORIZED" | "type:noc@hamburg.ccc.de,2026:DOOR_NOT_FOUND"; + HttpProblemType: "type:noc@hamburg.ccc.de,2026:UNAUTHORIZED" | "type:noc@hamburg.ccc.de,2026:LOCK_NOT_FOUND"; /** Lock */ Lock: { /** Name */ name: string; + /** Id */ + id: string; status: components["schemas"]["LockStatus"]; }; + /** LockOperation */ + LockOperation: { + /** + * Desired State + * @enum {string} + */ + desired_state: "open" | "closed"; + }; /** LockStatus */ LockStatus: { /** Is Unreachable */ @@ -241,6 +285,24 @@ export interface operations { }; }; }; + logout_auth_logout_get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 302: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; list_locks_api_locks__get: { parameters: { query?: never; @@ -270,4 +332,57 @@ export interface operations { }; }; }; + operate_lock_api_locks__lock_id__patch: { + parameters: { + query?: never; + header?: never; + path: { + lock_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["LockOperation"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HttpProblemDetail"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HttpProblemDetail"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; } diff --git a/app/src/assets/main.ts b/app/src/assets/main.ts index 48bcd57..9bab4b4 100644 --- a/app/src/assets/main.ts +++ b/app/src/assets/main.ts @@ -67,7 +67,7 @@ async function checkUser() { const {data: userInfo} = await getUserInfo({}) apiError.current = null - auth.authenticated = userInfo.is_logged_in + 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 ?? "" @@ -98,7 +98,9 @@ async function checkUser() { } async function fetchDoors() { - loading.doors = true + if (doors.length === 0) { + loading.doors = true + } refresh() const getDoors = fetcher.path("/api/locks/").method("get").create() @@ -123,7 +125,7 @@ async function fetchDoors() { } doors.push({ - id: door.name, // TODO: replace by actual ID + id: door.id, label: door.name, state: state, batteryLow: door.status.is_low_battery, @@ -157,9 +159,25 @@ async function fetchDoors() { } } -function setDoorInfo(doorElement: HTMLDivElement, door: DoorType) { +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], + }) +} + +function setDoorInfo(doorElement: HTMLDivElement, door: DoorType, initial: boolean = false) { const labelElement: HTMLDivElement = doorElement.querySelector("[data-label]")! - const buttonElements: Array = Array.from(doorElement.querySelectorAll("button")) + 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 @@ -190,6 +208,16 @@ function setDoorInfo(doorElement: HTMLDivElement, door: DoorType) { 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 }) @@ -205,7 +233,7 @@ function refresh() { const targetElement = document.importNode(template.content, true) doorElement = targetElement.querySelector("[data-state]")! - setDoorInfo(doorElement, door) + setDoorInfo(doorElement, door, true) list.appendChild(targetElement) } @@ -233,9 +261,8 @@ function refresh() { } - loadAuthFromLocalStorage() -fetchDoors() +setInterval(fetchDoors, 250) // TODO: replace with SSE checkUser() document.addEventListener("loadeddata", () => { diff --git a/app/src/components/DoorTemplate.astro b/app/src/components/DoorTemplate.astro index 199467d..35fec86 100644 --- a/app/src/components/DoorTemplate.astro +++ b/app/src/components/DoorTemplate.astro @@ -44,10 +44,10 @@ const t = useTranslations(lang)