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)
|
- [Connection Events](#connection-events)
|
||||||
- [`config_update`](#config_update)
|
- [`config_update`](#config_update)
|
||||||
- [`connected`](#connected)
|
- [`connected`](#connected)
|
||||||
- [`active_queue`](#active_queue)
|
|
||||||
- [Logging Events](#logging-events)
|
- [Logging Events](#logging-events)
|
||||||
- [`log_info`](#log_info)
|
- [`log_info`](#log_info)
|
||||||
- [`log_success`](#log_success)
|
- [`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
|
#### Logging Events
|
||||||
|
|
||||||
##### `log_info`
|
##### `log_info`
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,7 @@ class Events:
|
||||||
|
|
||||||
CONNECTED: str = "connected"
|
CONNECTED: str = "connected"
|
||||||
|
|
||||||
CONFIGURATION: str = "configuration"
|
|
||||||
CONFIG_UPDATE: str = "config_update"
|
CONFIG_UPDATE: str = "config_update"
|
||||||
ACTIVE_QUEUE: str = "active_queue"
|
|
||||||
|
|
||||||
LOG_INFO: str = "log_info"
|
LOG_INFO: str = "log_info"
|
||||||
LOG_WARNING: str = "log_warning"
|
LOG_WARNING: str = "log_warning"
|
||||||
|
|
@ -79,10 +77,8 @@ class Events:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return [
|
return [
|
||||||
Events.CONFIGURATION,
|
|
||||||
Events.CONFIG_UPDATE,
|
Events.CONFIG_UPDATE,
|
||||||
Events.CONNECTED,
|
Events.CONNECTED,
|
||||||
Events.ACTIVE_QUEUE,
|
|
||||||
Events.LOG_INFO,
|
Events.LOG_INFO,
|
||||||
Events.LOG_WARNING,
|
Events.LOG_WARNING,
|
||||||
Events.LOG_ERROR,
|
Events.LOG_ERROR,
|
||||||
|
|
|
||||||
|
|
@ -135,10 +135,20 @@
|
||||||
<div>
|
<div>
|
||||||
<NuxtLoadingIndicator />
|
<NuxtLoadingIndicator />
|
||||||
<NuxtPage v-if="config.is_loaded" :isLoading="loadingImage" @reload_bg="() => loadImage(true)" />
|
<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"
|
<Message v-if="!config.is_loaded" class="mt-5"
|
||||||
icon="fas fa-spinner fa-spin">
|
:class="{ 'is-info': config.is_loading, 'is-danger': !config.is_loading }"
|
||||||
<p>This usually takes less than a second. If this is taking too long,
|
:title="config.is_loading ? 'Loading configuration...' : 'Failed to load configuration'"
|
||||||
<NuxtLink class="button is-text p-0" @click="config.loadConfig">reload configuration</NuxtLink>.
|
: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>
|
</p>
|
||||||
<template v-if="socket.error">
|
<template v-if="socket.error">
|
||||||
<hr>
|
<hr>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@ import type { Preset } from '~/types/presets';
|
||||||
import type { ConfigFeature, ConfigUpdateAction } from '~/types/sockets';
|
import type { ConfigFeature, ConfigUpdateAction } from '~/types/sockets';
|
||||||
import { request } from '~/utils';
|
import { request } from '~/utils';
|
||||||
|
|
||||||
|
let last_reload = 0;
|
||||||
|
const CONFIG_TTL = 10;
|
||||||
|
|
||||||
export const useConfigStore = defineStore('config', () => {
|
export const useConfigStore = defineStore('config', () => {
|
||||||
const state = reactive<ConfigState>({
|
const state = reactive<ConfigState>({
|
||||||
showForm: useStorage('showForm', true),
|
showForm: useStorage('showForm', true),
|
||||||
|
|
@ -54,14 +57,23 @@ export const useConfigStore = defineStore('config', () => {
|
||||||
is_loading: false,
|
is_loading: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const loadConfig = async () => {
|
const loadConfig = async (force: boolean = false) => {
|
||||||
if (state.is_loading) {
|
if (state.is_loading) {
|
||||||
return;
|
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_loaded = false;
|
||||||
state.is_loading = true;
|
state.is_loading = true;
|
||||||
try {
|
try {
|
||||||
const resp = await request('/api/system/configuration');
|
const resp = await request('/api/system/configuration', { timeout: 10 });
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -79,11 +91,12 @@ export const useConfigStore = defineStore('config', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
setAll(data);
|
setAll(data);
|
||||||
|
state.is_loaded = true;
|
||||||
|
last_reload = now;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('Failed to load configuration', e);
|
console.error(`Failed to load configuration: ${e}`);
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
state.is_loaded = true;
|
|
||||||
state.is_loading = false;
|
state.is_loading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { ref, readonly } from 'vue'
|
import { ref, readonly } from 'vue'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import type { ConfigState } from "~/types/config";
|
|
||||||
import type { StoreItem } from "~/types/store";
|
import type { StoreItem } from "~/types/store";
|
||||||
import type {
|
import type {
|
||||||
ConfigUpdatePayload,
|
ConfigUpdatePayload,
|
||||||
|
|
@ -263,14 +262,7 @@ export const useSocketStore = defineStore('socket', () => {
|
||||||
setupVisibilityListener()
|
setupVisibilityListener()
|
||||||
}
|
}
|
||||||
|
|
||||||
on('configuration', (data: WSEP['configuration']) => {
|
on('connect', () => config.loadConfig(false))
|
||||||
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('connected', (data: WSEP['connected']) => {
|
on('connected', (data: WSEP['connected']) => {
|
||||||
if (!data?.data) {
|
if (!data?.data) {
|
||||||
|
|
@ -288,13 +280,6 @@ export const useSocketStore = defineStore('socket', () => {
|
||||||
error.value = null
|
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']) => {
|
on('item_added', (data: WSEP['item_added']) => {
|
||||||
stateStore.add('queue', data.data._id, data.data)
|
stateStore.add('queue', data.data._id, data.data)
|
||||||
toast.success(`Item queued: ${ag(stateStore.get('queue', data.data._id, {} as StoreItem), 'title')}`)
|
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"
|
import type { StoreItem } from "./store"
|
||||||
|
|
||||||
export type Event = {
|
export type Event = {
|
||||||
|
|
@ -21,20 +20,11 @@ export type WSEP = {
|
||||||
connect: null
|
connect: null
|
||||||
disconnect: null
|
disconnect: null
|
||||||
connect_error: { message?: string }
|
connect_error: { message?: string }
|
||||||
configuration: EventPayload<{
|
|
||||||
config: AppConfig
|
|
||||||
presets: ConfigState['presets']
|
|
||||||
dl_fields: ConfigState['dl_fields']
|
|
||||||
paused: boolean
|
|
||||||
}>
|
|
||||||
connected: EventPayload<{
|
connected: EventPayload<{
|
||||||
folders?: string[]
|
folders?: string[]
|
||||||
history_count?: number
|
history_count?: number
|
||||||
queue?: Record<string, StoreItem>
|
queue?: Record<string, StoreItem>
|
||||||
}>
|
}>
|
||||||
active_queue: EventPayload<{
|
|
||||||
queue?: Record<string, StoreItem>
|
|
||||||
}>
|
|
||||||
item_added: EventPayload<StoreItem>
|
item_added: EventPayload<StoreItem>
|
||||||
item_updated: EventPayload<StoreItem>
|
item_updated: EventPayload<StoreItem>
|
||||||
item_cancelled: 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.
|
* @param options - Optional fetch options, automatically extended with common headers and credentials.
|
||||||
* @returns The fetch Response promise.
|
* @returns The fetch Response promise.
|
||||||
*/
|
*/
|
||||||
const request = (url: string, options: RequestInit = {}): Promise<Response> => {
|
const request = (url: string, options: RequestInit & { timeout?: number } = {}): Promise<Response> => {
|
||||||
options.method = options.method || 'GET'
|
const { timeout, ...fetchOptions } = options
|
||||||
options.headers = options.headers || {}; (options as any).withCredentials = true
|
|
||||||
|
|
||||||
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)) {
|
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']) {
|
if (undefined === (fetchOptions.headers as Record<string, any>)['Accept']) {
|
||||||
; (options.headers as Record<string, any>)['Accept'] = 'application/json'
|
; (fetchOptions.headers as Record<string, any>)['Accept'] = 'application/json'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url.startsWith('/')) {
|
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) {
|
if (payload.error) {
|
||||||
return String(payload.error+(extra_detail ? ` - ${extra_detail}` : ''))
|
return String(payload.error + (extra_detail ? ` - ${extra_detail}` : ''))
|
||||||
}
|
}
|
||||||
if (payload.message) {
|
if (payload.message) {
|
||||||
return String(payload.message+(extra_detail ? ` - ${extra_detail}` : ''))
|
return String(payload.message + (extra_detail ? ` - ${extra_detail}` : ''))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extra_detail) {
|
if (extra_detail) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue