update api, operate doors

This commit is contained in:
kritzl 2026-05-14 16:55:53 +02:00
commit d19a26e128
Signed by: kritzl
SSH key fingerprint: SHA256:5BmINP9VjZWaUk5Z+2CTut1KFhwLtd0ZynMekKbtViM
4 changed files with 154 additions and 11 deletions

1
.gitignore vendored
View file

@ -1,5 +1,6 @@
.envrc
.direnv/
.env
**/__pycache__
api/dist

View file

@ -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<string, never>;
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"];
};
};
};
};
}

View file

@ -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() {
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<string, "open" | "closed"> = {
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<HTMLButtonElement> = Array.from(doorElement.querySelectorAll("button"))
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
@ -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", () => {

View file

@ -44,10 +44,10 @@ const t = useTranslations(lang)
</div>
</div>
<div class="hidden group-data-active:grid grid-cols-2 w-full items-center gap-2 mt-4">
<button class="btn btn-outline btn-error">
<button class="btn btn-outline btn-error" data-button="lock">
{t("button.close")}
</button>
<button class="btn btn-outline btn-success">
<button class="btn btn-outline btn-success" data-button="unlock">
{t("button.open")}
</button>
</div>