Add error reporting to the daemon web UI

This error reporting comes in two forms:
- Errors updating the UI
- Errors with user actions

The former is displayed as one error until a refresh succeeds again. The
latter creates an number of persistent errors until they are cleared by
the user.
This commit is contained in:
Sashanoraa
2025-08-03 19:27:28 -04:00
committed by Sashanoraa
parent bbab29ae0b
commit 56122f6559
11 changed files with 209 additions and 14 deletions

View File

@@ -0,0 +1,24 @@
export class ActionError extends Error {
// The number of this an identical error has happened.
// This is shown as a number next to the error in the UI.
times = $state(1);
constructor(message: string, cause: Error) {
super(message);
this.cause = cause;
}
}
export const action_errors: ActionError[] = $state([]);
export function add_error(e: Error, msg: string): void {
for (const existing of action_errors) {
if (existing.message === msg) {
existing.times += 1;
return;
}
}
const action_error = new ActionError(msg, e);
action_errors.unshift(action_error);
console.log(action_errors.length);
}

View File

@@ -0,0 +1,86 @@
<script lang="ts">
import { action_errors } from '../action_errors.svelte';
let pos = $state(0);
let current_error = $derived(action_errors[pos]);
function prev_error() {
if (pos > 0) pos -= 1;
else pos = action_errors.length - 1;
}
function next_error() {
if (pos + 1 < action_errors.length) pos += 1;
else pos = 0;
}
function clear_errors() {
pos = 0;
action_errors.length = 0;
}
</script>
{#if action_errors.length > 0}
<div
class="bg-red-100 border-red-100 drop-shadow p-4 flex flex-col gap-2
border rounded-md flex-1 justify-between fixed z-10 right-3 bottom-3 ml-3"
>
<div class="flex flex-row justify-between">
<span class="text-xl font-bold mb-2 mr-5 flex flex-row items-center gap-1 text-red-600">
<svg
class="w-6 h-6 text-red-600"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="currentColor"
viewBox="0 0 24 24"
>
<path
fill-rule="evenodd"
d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm11-4a1 1 0 1 0-2 0v5a1 1 0 1 0 2 0V8Zm-1 7a1 1 0 1 0 0 2h.01a1 1 0 1 0 0-2H12Z"
clip-rule="evenodd"
/>
</svg>
Error Completing Action {current_error.times > 1 ? `x${current_error.times}` : ''}
</span>
<div class="flex items-center mb-2">
<span>{pos + 1}/{action_errors.length}</span>
<button title="previous error" aria-label="previous error" onclick={prev_error}>
<svg aria-hidden="true" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m 15.499979,19.499979 -6.9999997,-7 6.9999997,-6.9999997"
/>
</svg>
</button>
<button title="next error" aria-label="next error" onclick={next_error}>
<svg aria-hidden="true" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m 8.5000207,5.4999793 7.0000003,6.9999997 -7.0000003,7"
/>
</svg>
</button>
<button title="clear errors" aria-label="clear errors" onclick={clear_errors}>
<svg style="width:24px;height:24px" viewBox="0 0 24 24">
<path
d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z"
/>
</svg>
</button>
</div>
</div>
<span>{current_error.message}</span>
{#if current_error.cause}
<details>
<summary>Details</summary>
<code>{current_error.cause}</code>
</details>
{/if}
</div>
{/if}

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { req } from '$lib/utils.svelte';
import { user_action_req } from '$lib/utils.svelte';
let {
url,
@@ -11,6 +11,7 @@
icon,
onclick,
ariaLabel,
errorMessage,
}: {
url: string;
method?: string;
@@ -21,6 +22,7 @@
icon?: any; // Svelte snippet
onclick?: () => void | Promise<void>;
ariaLabel?: string;
errorMessage?: string;
} = $props();
let is_requesting = $state(false);
@@ -46,7 +48,11 @@
is_requesting = true;
try {
await req(method, url);
await user_action_req(
method,
url,
errorMessage ? errorMessage : 'Error performing action'
);
if (onclick) {
await onclick();
}

View File

@@ -7,5 +7,6 @@
text="Delete ALL Recordings"
prompt={`Are you sure you want to delete ALL recordings?`}
url={`/api/delete-all-recordings`}
name="all recodings"
/>
</div>

View File

@@ -1,18 +1,20 @@
<script lang="ts">
import { req } from '$lib/utils.svelte';
import { user_action_req } from '$lib/utils.svelte';
let {
text,
url,
prompt,
name,
}: {
text?: string;
url: string;
prompt: string;
name: string;
} = $props();
function confirmDelete() {
if (window.confirm(prompt)) {
req('POST', url);
user_action_req('POST', url, 'Unable to delete recording ' + name);
}
}
</script>

View File

@@ -91,6 +91,7 @@
<DeleteButton
prompt={`Are you sure you want to delete entry ${entry.name}?`}
url={entry.get_delete_url()}
name={entry.name}
/>
{/if}
</div>

View File

@@ -60,6 +60,7 @@
<DeleteButton
prompt={`Are you sure you want to delete entry ${entry.name}?`}
url={entry.get_delete_url()}
name={entry.name}
/>
</td>
{/if}

View File

@@ -35,6 +35,7 @@
variant="blue"
onclick={handleReAnalyze}
ariaLabel="re-analyze"
errorMessage="Error re-analyzing recoding"
>
{#snippet icon()}
<svg style="width:20px;height:20px" viewBox="0 0 24 24">

View File

@@ -1,6 +1,5 @@
<script lang="ts">
import ApiRequestButton from './ApiRequestButton.svelte';
let {
server_is_recording,
}: {
@@ -10,7 +9,12 @@
<div>
{#if server_is_recording}
<ApiRequestButton url="/api/stop-recording" label="Stop" variant="red">
<ApiRequestButton
url="/api/stop-recording"
label="Stop"
variant="red"
errorMessage="Error stoppping recording"
>
{#snippet icon()}
<svg
class="w-6 h-6 text-white"
@@ -28,7 +32,12 @@
{/snippet}
</ApiRequestButton>
{:else}
<ApiRequestButton url="/api/start-recording" label="Start" variant="blue">
<ApiRequestButton
url="/api/start-recording"
label="Start"
variant="blue"
errorMessage="Error starting recording"
>
{#snippet icon()}
<svg
class="w-6 h-6 text-white"

View File

@@ -1,3 +1,4 @@
import { add_error } from './action_errors.svelte';
import { Manifest } from './manifest.svelte';
import type { SystemStats } from './systemStats';
@@ -31,6 +32,23 @@ export async function req(method: string, url: string): Promise<string> {
}
}
// A wrapper around req that reports errors to the UI
export async function user_action_req(
method: string,
url: string,
error_msg: string
): Promise<string | undefined> {
try {
return await req(method, url);
} catch (error) {
if (error instanceof Error) {
console.log('beeeo');
add_error(error, error_msg);
}
return undefined;
}
}
export async function get_manifest(): Promise<Manifest> {
const manifest_json = JSON.parse(await req('GET', '/api/qmdl-manifest'));
return new Manifest(manifest_json);