/* ── SWR Data Fetching Hooks ── * * Gelijktijdige data-fetching met auto-refresh, caching en revalidation. * Vervangt handmatige useEffect + useState + setInterval patronen. * * Alles fetched via de Next.js API proxy (krijgt cookies/sessie mee). */ import useSWR from 'swr'; import useSWRInfinite from 'swr/infinite'; import type { IPFSPin, RepoStats, BWStats, IPFSNodeInfo, IPFSUser } from './api'; /* ════════════════════════════ Fetcher ════════════════════════════ */ const API_BASE = ''; async function fetcher(path: string): Promise { const res = await fetch(`${API_BASE}${path}`, { credentials: 'include' }); if (!res.ok) { const text = await res.text().catch(() => ''); throw new Error(`${res.status}: ${text.slice(0, 200)}`); } return res.json(); } /* ════════════════════════════ Config ════════════════════════════ */ const DEFAULT_REFRESH = 15_000; // 15 seconden /* ════════════════════════════ Pins ════════════════════════════ */ export function usePins(refreshInterval = DEFAULT_REFRESH) { return useSWR('/api/pins', fetcher, { refreshInterval, revalidateOnFocus: true, }); } /* ════════════════════════════ Peers ════════════════════════════ */ export interface Peer { id: string; addr: string; latency: string; } export function usePeers(refreshInterval = DEFAULT_REFRESH) { return useSWR('/api/peers', async (path) => { const raw = await fetcher<{ Peers?: { Peer: string; Addr: string; Latency: string }[] }>(path); return (raw.Peers || []).map((p) => ({ id: p.Peer, addr: p.Addr, latency: p.Latency || '—', })); }, { refreshInterval, revalidateOnFocus: true, }); } /* ════════════════════════════ Node Info ════════════════════════════ */ export function useNodeInfo(refreshInterval?: number) { return useSWR('/api/node/info', fetcher, { refreshInterval, revalidateOnFocus: true, }); } /* ════════════════════════════ Repo Stats ════════════════════════════ */ export function useRepoStats(refreshInterval = DEFAULT_REFRESH) { return useSWR('/api/repo/stats', async (path) => { const raw = await fetcher<{ RepoSize?: number; StorageMax?: number; NumObjects?: number; RepoPath?: string; Version?: string; }>(path); return { repoSize: raw.RepoSize ?? 0, storageMax: raw.StorageMax ?? 0, numObjects: raw.NumObjects ?? 0, repoPath: raw.RepoPath ?? '', version: raw.Version ?? '', }; }, { refreshInterval, revalidateOnFocus: true, }); } /* ════════════════════════════ Bandwidth ════════════════════════════ */ export function useBandwidth(refreshInterval = DEFAULT_REFRESH) { return useSWR('/api/bw/stats', async (path) => { const raw = await fetcher<{ TotalIn?: number; TotalOut?: number; RateIn?: number; RateOut?: number; }>(path); return { totalIn: raw.TotalIn ?? 0, totalOut: raw.TotalOut ?? 0, rateIn: raw.RateIn ?? 0, rateOut: raw.RateOut ?? 0, }; }, { refreshInterval, revalidateOnFocus: true, }); } /* ════════════════════════════ Dashboard (alles-in-1) ════════════════════════════ */ export interface DashboardData { peers: Peer[]; pins: IPFSPin[]; repo: RepoStats | null; bw: BWStats | null; health: { status: string; node: string } | null; } export function useDashboard(refreshInterval = DEFAULT_REFRESH) { return useSWR('/api/health', async (healthPath) => { const [health, peers, pins, repo, bw] = await Promise.all([ fetcher<{ status: string; node: string }>(healthPath).catch(() => null as any), fetcher<{ Peers?: { Peer: string; Addr: string; Latency: string }[] }>('/api/peers') .then((raw) => (raw.Peers || []).map((p) => ({ id: p.Peer, addr: p.Addr, latency: p.Latency || '—' }))) .catch(() => [] as Peer[]), fetcher<{ Keys?: Record }>('/api/pins') .then((raw) => raw.Keys ? Object.entries(raw.Keys).map(([cid]) => ({ cid, name: '', size: 0, created: '' })) : []) .catch(() => [] as IPFSPin[]), fetcher<{ RepoSize?: number; StorageMax?: number; NumObjects?: number }>('/api/repo/stats') .then((raw) => ({ repoSize: raw.RepoSize ?? 0, storageMax: raw.StorageMax ?? 0, numObjects: raw.NumObjects ?? 0, repoPath: '', version: '' })) .catch(() => null), fetcher<{ TotalIn?: number; TotalOut?: number }>('/api/bw/stats') .then((raw) => ({ totalIn: raw.TotalIn ?? 0, totalOut: raw.TotalOut ?? 0, rateIn: 0, rateOut: 0 })) .catch(() => null), ]); return { peers, pins, repo, bw, health }; }, { refreshInterval, revalidateOnFocus: true, }); } /* ════════════════════════════ Daily Stats (paginated) ════════════════════════════ */ export interface DailyStat { date: string; uploads: number; bytesAdded: number; bytesRemoved: number; } const PAGE_SIZE = 30; export function useDailyStats() { return useSWRInfinite( (pageIndex, previousData) => { if (previousData && previousData.length < PAGE_SIZE) return null; return `/api/stats/daily?page=${pageIndex}&limit=${PAGE_SIZE}`; }, fetcher, { revalidateFirstPage: false }, ); } /* ════════════════════════════ Health ════════════════════════════ */ export function useHealth() { return useSWR<{ status: string; node: string }>('/api/health', fetcher, { refreshInterval: 30_000, }); } /* ════════════════════════════ Users (admin) ════════════════════════════ */ export function useUsers() { return useSWR('/api/users', async (path) => { const raw = await fetcher(path); if (Array.isArray(raw)) return raw; if (raw && Array.isArray(raw.users)) return raw.users; return []; }, { refreshInterval: 30_000, }); }