refactor: improve error messaging during failure to load config

This commit is contained in:
arabcoders 2026-01-28 17:00:34 +03:00
parent f88422e92b
commit dd0633c2aa
7 changed files with 57 additions and 73 deletions

24
API.md
View file

@ -117,7 +117,6 @@ This document describes the available endpoints and their usage. All endpoints r
- [Connection Events](#connection-events)
- [`config_update`](#config_update)
- [`connected`](#connected)
- [`active_queue`](#active_queue)
- [Logging Events](#logging-events)
- [`log_info`](#log_info)
- [`log_success`](#log_success)
@ -2742,29 +2741,6 @@ Emitted when a client successfully connects to the WebSocket.
---
##### `active_queue`
Emitted periodically with the current active queue status.
**Event**:
```json
{
"event": "active_queue",
"data": {
"queue": [
{
"id": "abc123",
"status": "downloading",
"progress": 45.6,
...
}
]
}
}
```
---
#### Logging Events
##### `log_info`

View file

@ -25,9 +25,7 @@ class Events:
CONNECTED: str = "connected"
CONFIGURATION: str = "configuration"
CONFIG_UPDATE: str = "config_update"
ACTIVE_QUEUE: str = "active_queue"
LOG_INFO: str = "log_info"
LOG_WARNING: str = "log_warning"
@ -79,10 +77,8 @@ class Events:
"""
return [
Events.CONFIGURATION,
Events.CONFIG_UPDATE,
Events.CONNECTED,
Events.ACTIVE_QUEUE,
Events.LOG_INFO,
Events.LOG_WARNING,
Events.LOG_ERROR,

View file

@ -135,10 +135,20 @@
<div>
<NuxtLoadingIndicator />
<NuxtPage v-if="config.is_loaded" :isLoading="loadingImage" @reload_bg="() => loadImage(true)" />
<Message v-if="!config.is_loaded" class="is-info mt-5" title="Loading Configuration"
icon="fas fa-spinner fa-spin">
<p>This usually takes less than a second. If this is taking too long,
<NuxtLink class="button is-text p-0" @click="config.loadConfig">reload configuration</NuxtLink>.
<Message v-if="!config.is_loaded" class="mt-5"
:class="{ 'is-info': config.is_loading, 'is-danger': !config.is_loading }"
:title="config.is_loading ? 'Loading configuration...' : 'Failed to load configuration'"
:icon="config.is_loading ? 'fas fa-spinner fa-spin' : 'fas fa-triangle-exclamation'">
<p v-if="config.is_loading">
This usually takes less than a few seconds. If this is taking too long,
<NuxtLink class="button is-text p-0" @click="reloadPage">click here</NuxtLink> to reload the
page.
</p>
<p v-if="!config.is_loading">
Failed to load the application configuration. This likely indicates a problem with the backend. Try to
<NuxtLink class="button is-text p-0" @click="() => config.loadConfig(true)">reload
configuration</NuxtLink> or <NuxtLink class="button is-text p-0" @click="reloadPage">reload the page
</NuxtLink>.
</p>
<template v-if="socket.error">
<hr>

View file

@ -5,6 +5,9 @@ import type { Preset } from '~/types/presets';
import type { ConfigFeature, ConfigUpdateAction } from '~/types/sockets';
import { request } from '~/utils';
let last_reload = 0;
const CONFIG_TTL = 10;
export const useConfigStore = defineStore('config', () => {
const state = reactive<ConfigState>({
showForm: useStorage('showForm', true),
@ -54,14 +57,23 @@ export const useConfigStore = defineStore('config', () => {
is_loading: false,
});
const loadConfig = async () => {
const loadConfig = async (force: boolean = false) => {
if (state.is_loading) {
return;
}
const now = Date.now()
if (state.is_loaded && !force && last_reload > 0) {
const age = (now - last_reload) / 1000
if (age < CONFIG_TTL) {
return
}
}
state.is_loaded = false;
state.is_loading = true;
try {
const resp = await request('/api/system/configuration');
const resp = await request('/api/system/configuration', { timeout: 10 });
if (!resp.ok) {
return;
}
@ -79,11 +91,12 @@ export const useConfigStore = defineStore('config', () => {
}
setAll(data);
state.is_loaded = true;
last_reload = now;
} catch (e: any) {
console.error('Failed to load configuration', e);
console.error(`Failed to load configuration: ${e}`);
}
finally {
state.is_loaded = true;
state.is_loading = false;
}
}

View file

@ -1,6 +1,5 @@
import { ref, readonly } from 'vue'
import { defineStore } from 'pinia'
import type { ConfigState } from "~/types/config";
import type { StoreItem } from "~/types/store";
import type {
ConfigUpdatePayload,
@ -263,14 +262,7 @@ export const useSocketStore = defineStore('socket', () => {
setupVisibilityListener()
}
on('configuration', (data: WSEP['configuration']) => {
config.setAll({
app: data.data.config,
presets: data.data.presets,
dl_fields: data.data.dl_fields,
paused: Boolean(data.data.paused)
} as unknown as Partial<ConfigState>)
})
on('connect', () => config.loadConfig(false))
on('connected', (data: WSEP['connected']) => {
if (!data?.data) {
@ -288,13 +280,6 @@ export const useSocketStore = defineStore('socket', () => {
error.value = null
})
on('active_queue', (data: WSEP['active_queue']) => {
if (!data.data.queue) {
return
}
stateStore.addAll('queue', data.data.queue || {})
})
on('item_added', (data: WSEP['item_added']) => {
stateStore.add('queue', data.data._id, data.data)
toast.success(`Item queued: ${ag(stateStore.get('queue', data.data._id, {} as StoreItem), 'title')}`)

View file

@ -1,4 +1,3 @@
import type { AppConfig, ConfigState } from "./config"
import type { StoreItem } from "./store"
export type Event = {
@ -21,20 +20,11 @@ export type WSEP = {
connect: null
disconnect: null
connect_error: { message?: string }
configuration: EventPayload<{
config: AppConfig
presets: ConfigState['presets']
dl_fields: ConfigState['dl_fields']
paused: boolean
}>
connected: EventPayload<{
folders?: string[]
history_count?: number
queue?: Record<string, StoreItem>
}>
active_queue: EventPayload<{
queue?: Record<string, StoreItem>
}>
item_added: EventPayload<StoreItem>
item_updated: EventPayload<StoreItem>
item_cancelled: EventPayload<StoreItem>

View file

@ -228,25 +228,39 @@ const encodePath = (item: string): string => {
* @param options - Optional fetch options, automatically extended with common headers and credentials.
* @returns The fetch Response promise.
*/
const request = (url: string, options: RequestInit = {}): Promise<Response> => {
options.method = options.method || 'GET'
options.headers = options.headers || {}; (options as any).withCredentials = true
const request = (url: string, options: RequestInit & { timeout?: number } = {}): Promise<Response> => {
const { timeout, ...fetchOptions } = options
if (undefined === (options.headers as Record<string, any>)['Content-Type']) {
fetchOptions.method = fetchOptions.method || 'GET'
fetchOptions.headers = fetchOptions.headers || {};
(fetchOptions as any).withCredentials = true
if (undefined === (fetchOptions.headers as Record<string, any>)['Content-Type']) {
if (!(options?.body instanceof FormData)) {
; (options.headers as Record<string, any>)['Content-Type'] = 'application/json'
; (fetchOptions.headers as Record<string, any>)['Content-Type'] = 'application/json'
}
}
if (undefined === (options.headers as Record<string, any>)['Accept']) {
; (options.headers as Record<string, any>)['Accept'] = 'application/json'
if (undefined === (fetchOptions.headers as Record<string, any>)['Accept']) {
; (fetchOptions.headers as Record<string, any>)['Accept'] = 'application/json'
}
if (url.startsWith('/')) {
options.credentials = 'same-origin'
fetchOptions.credentials = 'same-origin'
}
return fetch(url.startsWith('/') ? uri(url) : url, options)
let controller: AbortController | undefined
let timer: ReturnType<typeof setTimeout> | undefined
if (typeof timeout === 'number' && timeout > 0) {
controller = new AbortController()
fetchOptions.signal = controller.signal
timer = setTimeout(() => controller!.abort(`Request timed out.`), timeout*1000)
}
return fetch(url.startsWith('/') ? uri(url) : url, fetchOptions).finally(() => {
if (timer) { clearTimeout(timer) }
})
}
/**
@ -879,10 +893,10 @@ const parse_api_error = async (json: unknown): Promise<string> => {
}
if (payload.error) {
return String(payload.error+(extra_detail ? ` - ${extra_detail}` : ''))
return String(payload.error + (extra_detail ? ` - ${extra_detail}` : ''))
}
if (payload.message) {
return String(payload.message+(extra_detail ? ` - ${extra_detail}` : ''))
return String(payload.message + (extra_detail ? ` - ${extra_detail}` : ''))
}
if (extra_detail) {