Typing lessons like duolingo & musicmouse cleanup

This commit is contained in:
2026-09-12 18:58:02 +02:00
parent 498243af46
commit a5210fead2
50 changed files with 3074 additions and 710 deletions

View File

@@ -30,32 +30,34 @@ function post(path: string, body?: unknown): Promise<void> {
});
}
/** `null` means the room-control page isn't configured, not an error - unlike
* `request()`, a 404 here is expected and shouldn't throw. */
async function fetchHaConfig(): Promise<HaConfig | null> {
const response = await fetch("/api/ha");
/** GET, treating a 404 as an expected "not configured" rather than an error - unlike
* `request()`, which throws on it. Anything else that isn't ok still throws. */
async function fetchOrNullOn404<T>(path: string): Promise<T | null> {
const response = await fetch(`/api${path}`);
if (response.status === 404) return null;
if (!response.ok) throw new Error(`GET /ha failed: ${response.status}`);
return (await response.json()) as HaConfig;
if (!response.ok) throw new Error(`GET ${path} failed: ${response.status}`);
return (await response.json()) as T;
}
/** `null` means the IR remote isn't configured, not an error - same convention as
* `fetchHaConfig`. */
async function fetchLircConfig(): Promise<LircConfig | null> {
const response = await fetch("/api/lirc");
if (response.status === 404) return null;
if (!response.ok) throw new Error(`GET /lirc failed: ${response.status}`);
return (await response.json()) as LircConfig;
/** GET, treating *any* non-ok response as "nothing to show" rather than an error - for
* callers that poll and would rather fall back quietly than crash the poll loop. */
async function fetchOrNullOnError<T>(path: string): Promise<T | null> {
const response = await fetch(`/api${path}`);
if (!response.ok) return null;
return (await response.json()) as T;
}
/** `null` means the room-control page isn't configured, not an error. */
const fetchHaConfig = (): Promise<HaConfig | null> => fetchOrNullOn404<HaConfig>("/ha");
/** `null` means the IR remote isn't configured, not an error. */
const fetchLircConfig = (): Promise<LircConfig | null> => fetchOrNullOn404<LircConfig>("/lirc");
/** `null` covers both "unknown to Home Assistant" and "Home Assistant unreachable
* right now" (the backend answers the latter with a 502) - the room page treats a
* device with no state the same way either way, rather than crashing on a poll. */
async function fetchHaState(entityId: string): Promise<HaEntityState | null> {
const response = await fetch(`/api/ha/states/${entityId}`);
if (!response.ok) return null;
return (await response.json()) as HaEntityState;
}
const fetchHaState = (entityId: string): Promise<HaEntityState | null> =>
fetchOrNullOnError<HaEntityState>(`/ha/states/${entityId}`);
async function fetchHaStates(entityIds: string[]): Promise<Record<string, HaEntityState>> {
const results = await Promise.all(entityIds.map(fetchHaState));
@@ -68,11 +70,8 @@ async function fetchHaStates(entityIds: string[]): Promise<Record<string, HaEnti
/** `null` means "not analyzed" (the backend's expected 404 for this), not an error -
* the ambient background just falls back to its un-analyzed baseline for that track. */
async function fetchTrackDetail(albumId: string, trackIndex: number): Promise<TrackDetail | null> {
const response = await fetch(`/api/tracks/${albumId}/${trackIndex}/analysis`);
if (!response.ok) return null;
return (await response.json()) as TrackDetail;
}
const fetchTrackDetail = (albumId: string, trackIndex: number): Promise<TrackDetail | null> =>
fetchOrNullOnError<TrackDetail>(`/tracks/${albumId}/${trackIndex}/analysis`);
export const api = {
library: () => request<{ albums: Album[] }>("/library").then((body) => body.albums),