diff --git a/frontend/composables/useApi.ts b/frontend/composables/useApi.ts index ad284c9..54c0445 100644 --- a/frontend/composables/useApi.ts +++ b/frontend/composables/useApi.ts @@ -1,13 +1,55 @@ +const RETRY_DELAYS = [0, 500, 1500] +const CACHE_TTL_MS = 24 * 60 * 60 * 1000 + +function cacheKey(url: string) { + return `nids_cache:${url}` +} + +function saveToCache(url: string, data: unknown) { + try { + localStorage.setItem(cacheKey(url), JSON.stringify({ data, ts: Date.now() })) + } catch {} +} + +function loadFromCache(url: string): T | null { + try { + const raw = localStorage.getItem(cacheKey(url)) + if (!raw) return null + const { data, ts } = JSON.parse(raw) + if (Date.now() - ts > CACHE_TTL_MS) return null + return data as T + } catch { + return null + } +} + // Базовый fetch к Go API export function useApi() { - async function get(path: string, params?: Record) { + async function get(path: string, params?: Record): Promise { const url = new URL('/api' + path, window.location.origin) if (params) { Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, String(v))) } - const res = await fetch(url.toString()) - if (!res.ok) throw new Error(await res.text()) - return res.json() as Promise + const urlStr = url.toString() + + let lastErr: unknown + for (let i = 0; i < RETRY_DELAYS.length; i++) { + if (i > 0) await new Promise(r => setTimeout(r, RETRY_DELAYS[i])) + try { + const res = await fetch(urlStr, { signal: AbortSignal.timeout(5000) }) + if (!res.ok) throw new Error(await res.text()) + const data = await res.json() as T + saveToCache(urlStr, data) + return data + } catch (e) { + lastErr = e + } + } + + const cached = loadFromCache(urlStr) + if (cached !== null) return cached + + throw lastErr } async function post(path: string, body?: unknown) { diff --git a/frontend/composables/useBackendStatus.ts b/frontend/composables/useBackendStatus.ts new file mode 100644 index 0000000..e74b6e8 --- /dev/null +++ b/frontend/composables/useBackendStatus.ts @@ -0,0 +1,24 @@ +// Singleton: один interval на всё приложение +let checkInterval: number | null = null +const isOnline = ref(true) + +export function useBackendStatus() { + async function check() { + try { + const res = await fetch('/api/health', { signal: AbortSignal.timeout(3000) }) + isOnline.value = res.ok + } catch { + isOnline.value = false + } + } + + function startPolling() { + if (checkInterval || !import.meta.client) return + check() + checkInterval = window.setInterval(check, 5000) + } + + onMounted(startPolling) + + return { isOnline, check } +} diff --git a/frontend/layouts/default.vue b/frontend/layouts/default.vue index 7ecab00..a02365a 100644 --- a/frontend/layouts/default.vue +++ b/frontend/layouts/default.vue @@ -31,8 +31,8 @@ - -
+ +
+
+ + + {{ isOnline ? 'Бэкенд онлайн' : 'Бэкенд недоступен' }} + +
@@ -71,6 +80,15 @@ + +
+ + Бэкенд недоступен — отображаются данные из БД +
+
@@ -82,6 +100,7 @@