diff --git a/src/lib/serverpulsApi.js b/src/lib/serverpulsApi.js index 402f460..f999b85 100644 --- a/src/lib/serverpulsApi.js +++ b/src/lib/serverpulsApi.js @@ -1,4 +1,5 @@ import { account } from './appwrite' +import { createJwtCache, fetchWithRetry } from './serverpulsAuth' /** * Client fuer das Serverpuls-Backend (nur lesend). @@ -6,9 +7,13 @@ import { account } from './appwrite' */ const BASE = '/api/serverpuls' +// JWT 10 Min. gecacht (Appwrite begrenzt das Erzeugen pro Nutzer) +const jwtCache = createJwtCache({ createJWT: () => account.createJWT() }) + async function get(path) { - const { jwt } = await account.createJWT() - const res = await fetch(`${BASE}${path}`, { headers: { 'X-Appwrite-JWT': jwt } }) + const res = await fetchWithRetry(jwtCache, (jwt) => + fetch(`${BASE}${path}`, { headers: { 'X-Appwrite-JWT': jwt } }) + ) const data = await res.json().catch(() => ({})) if (!res.ok) throw new Error(data.error || `Serverpuls-Fehler ${res.status}`) return data diff --git a/src/lib/serverpulsAuth.js b/src/lib/serverpulsAuth.js new file mode 100644 index 0000000..5648bc7 --- /dev/null +++ b/src/lib/serverpulsAuth.js @@ -0,0 +1,37 @@ +// JWT-Cache fuer Serverpuls (rein, ohne Appwrite-Import; createJWT und Uhr werden injiziert). +export const JWT_TTL_MS = 10 * 60 * 1000 + +export function createJwtCache({ createJWT, now = Date.now, ttlMs = JWT_TTL_MS }) { + let entry = null // { jwt, at } oder { promise } + let pending = null + + async function get() { + if (entry && now() - entry.at < ttlMs) return entry.jwt + if (pending) return pending + const p = (async () => { + try { + const { jwt } = await createJWT() + entry = { jwt, at: now() } + return jwt + } finally { + if (pending === p) pending = null + } + })() + pending = p + return p + } + + function invalidate() { + entry = null + } + + return { get, invalidate } +} + +// doFetch(jwt) -> Response-artig ({status}); bei 401 einmal mit frischem JWT wiederholen. +export async function fetchWithRetry(cache, doFetch) { + const first = await doFetch(await cache.get()) + if (first.status !== 401) return first + cache.invalidate() + return doFetch(await cache.get()) +} diff --git a/src/pages/ServerpulsPage.jsx b/src/pages/ServerpulsPage.jsx index 2814735..c3e2d8a 100644 --- a/src/pages/ServerpulsPage.jsx +++ b/src/pages/ServerpulsPage.jsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useRef, useMemo, useState } from 'react' import { Navigate } from 'react-router-dom' import { useAuth } from '../context/AuthContext' import { PageHeader } from '../components/ui' @@ -50,7 +50,7 @@ function displayPoints(metric, points, total) { function tileModel(key, m) { if (!m) return null const total = m.total ?? null - const spark = (m.spark24h || []).map((p) => (key === 'ram' && total ? total - p.v : p.v)) + const spark = (m.spark24h || []).map((p) => (key === 'ram' && total && p.v !== null && p.v !== undefined ? total - p.v : p.v)) if (m.value === null || m.value === undefined) return { main: '–', sub: '', spark, delta: null } switch (key) { case 'cpu': @@ -159,16 +159,21 @@ export default function ServerpulsPage() { const [tick, setTick] = useState(0) // Uebersicht, Projekte, Aufraeumbar: beim Laden und jede Minute (nur sichtbar) + const reqId = useRef(0) const loadMain = useCallback(async () => { + reqId.current += 1 + const mine = reqId.current try { const [o, a, c] = await Promise.all([ serverpulsApi.overview(), serverpulsApi.apps(), serverpulsApi.cleanup(), ]) + if (mine !== reqId.current) return setOverview(o) setAppsData(a) setCleanup(c) setError('') } catch (e) { + if (mine !== reqId.current) return setError(e.message || 'Serverpuls ist nicht erreichbar.') } setNow(Date.now()) @@ -206,6 +211,11 @@ export default function ServerpulsPage() { return () => { dead = true } }, [isAdmin, selected, detailRange, tick]) + const changeMetric = (v) => { if (v !== metric) { setChart(null); setMetric(v) } } + const changeRange = (v) => { if (v !== range) { setChart(null); setRange(v) } } + const selectApp = (id) => { setDetail(null); setSelected(id) } + const changeDetailRange = (v) => { if (v !== detailRange) { setDetail(null); setDetailRange(v) } } + const m = METRICS.find((x) => x.id === metric) const chartData = useMemo(() => { @@ -286,8 +296,8 @@ export default function ServerpulsPage() {