Serverpuls: JWT-Cache, Lade-Zustand beim Umschalten, Antwort-Reihenfolge
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ec740ca970
commit
16f3150ff1
4 files changed
+116
-8
No files matched your search
@@ -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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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() {
|
||||
<div className="sp-panel-head">
|
||||
<h3>Verlauf</h3>
|
||||
<div className="sp-controls">
|
||||
<Pills items={METRICS} value={metric} onChange={setMetric} testid="sp-metric-select" />
|
||||
<Pills items={RANGES} value={range} onChange={setRange} testid="sp-range-select" />
|
||||
<Pills items={METRICS} value={metric} onChange={changeMetric} testid="sp-metric-select" />
|
||||
<Pills items={RANGES} value={range} onChange={changeRange} testid="sp-range-select" />
|
||||
</div>
|
||||
</div>
|
||||
{chartData && chartData.data[0].length > 0
|
||||
@@ -300,12 +310,12 @@ export default function ServerpulsPage() {
|
||||
<h3>Projekte</h3>
|
||||
{appsData?.sizesDay && <span className="sp-muted sp-small">Größen von {appsData.sizesDay}</span>}
|
||||
</div>
|
||||
<AppsTable rows={appsData?.rows || []} selectedId={selected} onSelect={setSelected} />
|
||||
<AppsTable rows={appsData?.rows || []} selectedId={selected} onSelect={selectApp} />
|
||||
{selected && (
|
||||
<div className="sp-detail" data-testid="sp-detail">
|
||||
<div className="sp-panel-head">
|
||||
<h3>{detail?.name || selected}</h3>
|
||||
<Pills items={RANGES} value={detailRange} onChange={setDetailRange} testid="sp-detail-range" />
|
||||
<Pills items={RANGES} value={detailRange} onChange={changeDetailRange} testid="sp-detail-range" />
|
||||
</div>
|
||||
{detailData ? (
|
||||
<div className="sp-detail-charts">
|
||||
|
||||
Reference in new issue
Block a user