diff --git a/app/routes.ts b/app/routes.ts index 9e3d0c5..e68063d 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -7,4 +7,5 @@ export default [ route('login', 'routes/login.tsx'), route('account', 'routes/account.tsx'), route('chat', 'routes/chat.tsx'), + route('bee', 'routes/bee.tsx'), ] satisfies RouteConfig; diff --git a/app/routes/bee.tsx b/app/routes/bee.tsx new file mode 100644 index 0000000..93e042e --- /dev/null +++ b/app/routes/bee.tsx @@ -0,0 +1,202 @@ +import { useEffect, useState } from 'react'; + +const API_URL = import.meta.env.VITE_API_URL ?? 'https://api.t4t-gateway.com'; + +type Health = { status: string; version: string; apiVersion: string }; +type Addresses = { + overlay: string; + ethereum: string; + publicKey: string; + pssPublicKey: string; + underlay: string[]; +}; +type Topology = { + connected: number; + population: number; + depth: number; + reachability: string; + networkAvailability: string; +}; +type Wallet = { + bzz: string; + xdai: string; + walletAddress: string; + chainID: number; +}; +type Stamp = { + batchID: string; + label: string; + depth: number; + amount: string; + usable: boolean; + usageText: string; + durationDays: number; +}; +type ChainState = { block: number; chainTip: number; currentPrice: number }; + +type Stats = { + health: Health | null; + addresses: Addresses | null; + topology: Topology | null; + wallet: Wallet | null; + stamps: Stamp[] | null; + chain: ChainState | null; +}; + +export function meta() { + return [{ title: 'Bee · t4t Gateway' }]; +} + +async function fetchStats(): Promise { + const token = localStorage.getItem('jwt'); + if (!token) throw new Error('Not authenticated'); + const res = await fetch(`${API_URL}/bee/stats`, { + headers: { Authorization: `Bearer ${token}` }, + }); + if (res.status === 401) { + localStorage.removeItem('jwt'); + window.location.href = '/login'; + throw new Error('Unauthorized'); + } + const body = await res.json(); + if (!res.ok) throw new Error(body?.error ?? `Request failed (${res.status})`); + return body as Stats; +} + +function Card({ title, children }: { title: string; children: React.ReactNode }) { + return ( +
+

{title}

+
{children}
+
+ ); +} + +function Row({ label, value }: { label: string; value: React.ReactNode }) { + return ( +
+ {label} + {value} +
+ ); +} + +export default function BeePage() { + const [stats, setStats] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (typeof window === 'undefined') return; + if (!localStorage.getItem('jwt')) { + window.location.href = '/login'; + return; + } + fetchStats() + .then((s) => setStats(s)) + .catch((err) => setError(err instanceof Error ? err.message : 'Unknown error')) + .finally(() => setLoading(false)); + }, []); + + if (loading) { + return ( +
+

Loading Bee stats…

+
+ ); + } + + return ( +
+

Bee node

+

Read-only status of the operator's Bee node.

+ + {error && ( +

{error}

+ )} + +
+ + {stats?.health ? ( + <> + + + + + ) : ( +

unavailable

+ )} +
+ + + {stats?.addresses ? ( + <> + + + + + ) : ( +

unavailable

+ )} +
+ + + {stats?.topology ? ( + <> + + + + + + + ) : ( +

unavailable

+ )} +
+ + + {stats?.wallet ? ( + <> + + + + + + ) : ( +

unavailable

+ )} +
+ + + {stats?.chain ? ( + <> + + + + + ) : ( +

unavailable

+ )} +
+ + + {stats?.stamps && stats.stamps.length > 0 ? ( +
    + {stats.stamps.map((s) => ( +
  • +

    {s.batchID}

    +

    + {s.label || '(unlabeled)'} · depth {s.depth} · used {s.usageText} ·{' '} + {s.usable ? 'usable' : 'not usable'} · {s.durationDays.toFixed(1)}d left +

    +
  • + ))} +
+ ) : ( +

no batches

+ )} +
+
+
+ ); +}