refactor: improve error messaging during failure to load config
This commit is contained in:
parent
f88422e92b
commit
dd0633c2aa
7 changed files with 57 additions and 73 deletions
24
API.md
24
API.md
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')}`)
|
||||
|
|
|
|||
10
ui/app/types/sockets.d.ts
vendored
10
ui/app/types/sockets.d.ts
vendored
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue