mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
feat(metadata): add runtime provider throttle status in settings
This commit is contained in:
@@ -1,25 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, watch } from 'vue'
|
||||
import { onMounted, onUnmounted, watch } from 'vue'
|
||||
import type { FieldPreference, MetadataField, ProviderConfigurations } from '@projectx/types'
|
||||
import { useLibraries } from '@/features/library/composables/useLibraries'
|
||||
import ProviderConfigPanel from './components/ProviderConfigPanel.vue'
|
||||
import GlobalPreferencePanel from './components/GlobalPreferencePanel.vue'
|
||||
import LibraryPreferencePanel from './components/LibraryPreferencePanel.vue'
|
||||
import { useProviderConfig } from './composables/useProviderConfig'
|
||||
import { useProviderThrottleRuntime } from './composables/useProviderThrottleRuntime'
|
||||
import { useMetadataPreferences } from './composables/useMetadataPreferences'
|
||||
import { Info } from 'lucide-vue-next'
|
||||
|
||||
const { config, statuses, saving: savingProviders, fetchConfig, saveConfig } = useProviderConfig()
|
||||
const { runtimeByKey, startPolling, stopPolling } = useProviderThrottleRuntime()
|
||||
const { globalPrefs, libraryPrefs, savingGlobal, savingField, fetchGlobal, saveGlobal, fetchLibrary, saveFieldOverride, resetLibrary } =
|
||||
useMetadataPreferences()
|
||||
const { libraries, fetchLibraries } = useLibraries()
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([fetchConfig(), fetchGlobal(), fetchLibraries()])
|
||||
onMounted(() => {
|
||||
startPolling()
|
||||
void Promise.all([fetchConfig(), fetchGlobal(), fetchLibraries()]).catch(() => undefined)
|
||||
})
|
||||
|
||||
const fetchedLibraryIds = new Set<number>()
|
||||
|
||||
onUnmounted(() => {
|
||||
stopPolling()
|
||||
})
|
||||
|
||||
watch(libraries, async (libs) => {
|
||||
const newLibs = libs.filter((lib) => !fetchedLibraryIds.has(lib.id))
|
||||
newLibs.forEach((lib) => fetchedLibraryIds.add(lib.id))
|
||||
@@ -43,6 +50,7 @@ async function onResetLibrary(libraryId: number) {
|
||||
<ProviderConfigPanel
|
||||
:config="config"
|
||||
:statuses="statuses"
|
||||
:runtime-by-key="runtimeByKey"
|
||||
:saving="savingProviders"
|
||||
@save="saveConfig($event as Partial<ProviderConfigurations>)"
|
||||
/>
|
||||
|
||||
+62
-2
@@ -1,18 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { Loader2, Save } from 'lucide-vue-next'
|
||||
import type { ProviderConfigurations, ProviderStatus } from '@projectx/types'
|
||||
import type { MetadataProviderKey, ProviderConfigurations, ProviderStatus, ProviderThrottleRuntimeState } from '@projectx/types'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
||||
const props = defineProps<{
|
||||
config: ProviderConfigurations | null
|
||||
statuses: ProviderStatus[]
|
||||
runtimeByKey?: Partial<Record<MetadataProviderKey, ProviderThrottleRuntimeState>>
|
||||
saving: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ save: [patch: Partial<ProviderConfigurations>] }>()
|
||||
|
||||
const draft = ref<ProviderConfigurations | null>(null)
|
||||
const nowMs = ref(Date.now())
|
||||
let nowTicker: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
onMounted(() => {
|
||||
nowTicker = setInterval(() => {
|
||||
nowMs.value = Date.now()
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (!nowTicker) return
|
||||
clearInterval(nowTicker)
|
||||
nowTicker = null
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.config,
|
||||
@@ -77,6 +92,41 @@ function statusFor(key: string) {
|
||||
return props.statuses.find((s) => s.key === key)
|
||||
}
|
||||
|
||||
function runtimeFor(key: string): ProviderThrottleRuntimeState | undefined {
|
||||
return props.runtimeByKey?.[key as MetadataProviderKey]
|
||||
}
|
||||
|
||||
function throttleSecondsLeft(key: string): number | null {
|
||||
const state = runtimeFor(key)
|
||||
if (!state?.throttled || !state.throttledUntil) return null
|
||||
const remaining = Math.ceil((Date.parse(state.throttledUntil) - nowMs.value) / 1000)
|
||||
return remaining > 0 ? remaining : null
|
||||
}
|
||||
|
||||
function isThrottled(key: string): boolean {
|
||||
return throttleSecondsLeft(key) !== null
|
||||
}
|
||||
|
||||
function throttleMessage(key: string): string | null {
|
||||
const seconds = throttleSecondsLeft(key)
|
||||
if (seconds === null) return null
|
||||
return `Retry in ${formatDuration(seconds)}`
|
||||
}
|
||||
|
||||
function formatDuration(totalSeconds: number): string {
|
||||
if (totalSeconds >= 3600) {
|
||||
const hours = Math.floor(totalSeconds / 3600)
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60)
|
||||
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`
|
||||
}
|
||||
if (totalSeconds >= 60) {
|
||||
const minutes = Math.floor(totalSeconds / 60)
|
||||
const seconds = totalSeconds % 60
|
||||
return `${minutes}m ${seconds}s`
|
||||
}
|
||||
return `${totalSeconds}s`
|
||||
}
|
||||
|
||||
function save() {
|
||||
if (!draft.value) return
|
||||
emit('save', draft.value)
|
||||
@@ -111,6 +161,13 @@ function save() {
|
||||
<Badge v-if="!statusFor(row.key)?.configured" variant="destructive" class="h-4 px-1.5 text-[9px] font-bold uppercase tracking-wide">
|
||||
Setup Required
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else-if="isThrottled(row.key)"
|
||||
variant="outline"
|
||||
class="h-4 px-1.5 text-[9px] font-bold uppercase tracking-wide text-amber-600 border-amber-500/30 bg-amber-500/5"
|
||||
>
|
||||
Throttled
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else
|
||||
variant="outline"
|
||||
@@ -123,6 +180,9 @@ function save() {
|
||||
<p v-if="row.hint" class="settings-hint max-w-sm">
|
||||
{{ row.hint }}
|
||||
</p>
|
||||
<p v-if="throttleMessage(row.key)" class="text-xs text-amber-600">
|
||||
{{ throttleMessage(row.key) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Right: Config & Toggle -->
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { ref } from 'vue'
|
||||
import type { MetadataProviderKey, ProviderThrottleRuntimeSnapshot, ProviderThrottleRuntimeState } from '@projectx/types'
|
||||
import { api } from '@/lib/api'
|
||||
|
||||
export function useProviderThrottleRuntime() {
|
||||
const snapshot = ref<ProviderThrottleRuntimeSnapshot | null>(null)
|
||||
const runtimeByKey = ref<Partial<Record<MetadataProviderKey, ProviderThrottleRuntimeState>>>({})
|
||||
const loading = ref(false)
|
||||
let timer: ReturnType<typeof setInterval> | null = null
|
||||
let inFlight: Promise<void> | null = null
|
||||
|
||||
function applySnapshot(next: ProviderThrottleRuntimeSnapshot) {
|
||||
snapshot.value = next
|
||||
runtimeByKey.value = Object.fromEntries(next.providers.map((p) => [p.key, p])) as Partial<
|
||||
Record<MetadataProviderKey, ProviderThrottleRuntimeState>
|
||||
>
|
||||
}
|
||||
|
||||
async function fetchRuntime() {
|
||||
if (inFlight) return inFlight
|
||||
inFlight = (async () => {
|
||||
loading.value = snapshot.value === null
|
||||
try {
|
||||
const res = await api('/api/v1/metadata-fetch/providers/runtime')
|
||||
if (!res.ok) return
|
||||
const data = (await res.json()) as ProviderThrottleRuntimeSnapshot
|
||||
applySnapshot(data)
|
||||
} catch {
|
||||
// Best-effort runtime indicator; fail silently and retry on next poll.
|
||||
} finally {
|
||||
loading.value = false
|
||||
inFlight = null
|
||||
}
|
||||
})()
|
||||
|
||||
return inFlight
|
||||
}
|
||||
|
||||
function startPolling(intervalMs = 15_000) {
|
||||
stopPolling()
|
||||
void fetchRuntime()
|
||||
timer = setInterval(() => {
|
||||
void fetchRuntime()
|
||||
}, intervalMs)
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (!timer) return
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
}
|
||||
|
||||
return {
|
||||
snapshot,
|
||||
runtimeByKey,
|
||||
loading,
|
||||
fetchRuntime,
|
||||
startPolling,
|
||||
stopPolling,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user