Compare commits

..
2 Commits
Author SHA1 Message Date
ffaerber c7ea9f26e8 Drop the /bee page (#2)
Docker / build (push) Successful in 20s
2026-09-17 14:51:30 +02:00
Felix FaerberandClaude Opus 5 79e79097ae drop the /bee page
Docker / build (pull_request) Successful in 17s
It showed node health, addresses, topology, wallet, stamps and chain state --
the same ground bee-manager covers at stamps.ffaerber.duckdns.org, and covers
better: bucket maps, top-up and dilute actions, the action ledger, alerting.
Two views of one node drift apart, and the weaker one is the one people find
first, from the gateway's own nav.

Route, component and nav entry go together. The gateway's /bee/stats endpoint
is left alone: nothing else in this app called it, but it is a public API route
and removing it is a decision about the API, not about this page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYFnHida4AdiNDek2pzgXx
2026-09-17 14:27:48 +03:00
3 changed files with 0 additions and 211 deletions

No files matched your search

-1
View File
@@ -3,7 +3,6 @@ import { NavLink } from 'react-router';
const ITEMS = [ const ITEMS = [
{ to: '/chat', label: 'Chat' }, { to: '/chat', label: 'Chat' },
{ to: '/account', label: 'Account' }, { to: '/account', label: 'Account' },
{ to: '/bee', label: 'Bee' },
]; ];
export function Nav() { export function Nav() {
-1
View File
@@ -7,5 +7,4 @@ export default [
route('login', 'routes/login.tsx'), route('login', 'routes/login.tsx'),
route('account', 'routes/account.tsx'), route('account', 'routes/account.tsx'),
route('chat', 'routes/chat.tsx'), route('chat', 'routes/chat.tsx'),
route('bee', 'routes/bee.tsx'),
] satisfies RouteConfig; ] satisfies RouteConfig;
-209
View File
@@ -1,209 +0,0 @@
import { useEffect, useState } from 'react';
import { Nav } from '~/components/nav';
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<Stats> {
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 (
<section className='rounded-lg border border-neutral-200 bg-white p-5 shadow-sm'>
<h2 className='text-xs font-semibold uppercase tracking-wide text-neutral-500'>{title}</h2>
<div className='mt-3 text-sm'>{children}</div>
</section>
);
}
function Row({ label, value }: { label: string; value: React.ReactNode }) {
return (
<div className='flex justify-between gap-4 py-1'>
<span className='text-neutral-500'>{label}</span>
<span className='break-all text-right font-mono text-neutral-900'>{value}</span>
</div>
);
}
export default function BeePage() {
const [stats, setStats] = useState<Stats | null>(null);
const [error, setError] = useState<string | null>(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 (
<>
<Nav />
<main className='flex min-h-[60vh] items-center justify-center'>
<p className='text-sm text-neutral-600'>Loading Bee stats…</p>
</main>
</>
);
}
return (
<>
<Nav />
<main className='mx-auto max-w-3xl px-6 py-10'>
<h1 className='text-2xl font-semibold'>Bee node</h1>
<p className='mt-1 text-sm text-neutral-600'>Read-only status of the operator's Bee node.</p>
{error && (
<p className='mt-4 rounded-md bg-red-50 px-3 py-2 text-sm text-red-700'>{error}</p>
)}
<div className='mt-6 grid gap-4'>
<Card title='Health'>
{stats?.health ? (
<>
<Row label='Status' value={stats.health.status} />
<Row label='Version' value={stats.health.version} />
<Row label='API version' value={stats.health.apiVersion} />
</>
) : (
<p className='text-neutral-500'>unavailable</p>
)}
</Card>
<Card title='Addresses'>
{stats?.addresses ? (
<>
<Row label='Overlay' value={stats.addresses.overlay} />
<Row label='Ethereum' value={stats.addresses.ethereum} />
<Row label='Underlays' value={stats.addresses.underlay.length} />
</>
) : (
<p className='text-neutral-500'>unavailable</p>
)}
</Card>
<Card title='Topology'>
{stats?.topology ? (
<>
<Row label='Connected peers' value={stats.topology.connected} />
<Row label='Known peers' value={stats.topology.population} />
<Row label='Depth' value={stats.topology.depth} />
<Row label='Reachability' value={stats.topology.reachability} />
<Row label='Network availability' value={stats.topology.networkAvailability} />
</>
) : (
<p className='text-neutral-500'>unavailable</p>
)}
</Card>
<Card title='Wallet'>
{stats?.wallet ? (
<>
<Row label='Address' value={stats.wallet.walletAddress} />
<Row label='xBZZ' value={stats.wallet.bzz} />
<Row label='xDAI' value={stats.wallet.xdai} />
<Row label='Chain ID' value={stats.wallet.chainID} />
</>
) : (
<p className='text-neutral-500'>unavailable</p>
)}
</Card>
<Card title='Chain state'>
{stats?.chain ? (
<>
<Row label='Block' value={stats.chain.block} />
<Row label='Chain tip' value={stats.chain.chainTip} />
<Row label='Current price (PLUR/chunk)' value={stats.chain.currentPrice} />
</>
) : (
<p className='text-neutral-500'>unavailable</p>
)}
</Card>
<Card title='Postage batches'>
{stats?.stamps && stats.stamps.length > 0 ? (
<ul className='divide-y divide-neutral-200'>
{stats.stamps.map((s) => (
<li key={s.batchID} className='py-2'>
<p className='font-mono text-xs text-neutral-900'>{s.batchID}</p>
<p className='mt-1 text-xs text-neutral-500'>
{s.label || '(unlabeled)'} · depth {s.depth} · used {s.usageText} ·{' '}
{s.usable ? 'usable' : 'not usable'} · {s.durationDays.toFixed(1)}d left
</p>
</li>
))}
</ul>
) : (
<p className='text-neutral-500'>no batches</p>
)}
</Card>
</div>
</main>
</>
);
}