account: add Recent jobs table

Fetch /v1/jobs alongside the existing account data and render the
last 25 in a table — model, status pill, provider, tokens, cost,
timestamp. Mirrors the homelab gateway's /wallet job list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Felix FaerberandClaude Opus 4.7 committed 2026-05-27 15:46:28 +03:00
1 parent c538822e4f
commit 86e0ab356a
1 file changed
+76 -1
+76 -1
View File
@@ -31,6 +31,21 @@ type ApiKey = {
modelAllowlist: string[]; modelAllowlist: string[];
}; };
type NewApiKey = { id: string; name: string; key: string; createdAt: string }; type NewApiKey = { id: string; name: string; key: string; createdAt: string };
type Job = {
id: string;
status: string;
modelId: string;
provider: string;
estimatedMaxXbzzWei: string;
actualXbzzWei: string | null;
promptTokens: number | null;
completionTokens: number | null;
createdAt: string;
ackedAt: string | null;
deliveredAt: string | null;
claimedAt: string | null;
errorMessage: string | null;
};
export function meta() { export function meta() {
return [{ title: 'Account · t4t Gateway' }]; return [{ title: 'Account · t4t Gateway' }];
@@ -75,6 +90,7 @@ export default function Account() {
const [account, setAccount] = useState<Account | null>(null); const [account, setAccount] = useState<Account | null>(null);
const [ledger, setLedger] = useState<LedgerEntry[]>([]); const [ledger, setLedger] = useState<LedgerEntry[]>([]);
const [keys, setKeys] = useState<ApiKey[]>([]); const [keys, setKeys] = useState<ApiKey[]>([]);
const [jobs, setJobs] = useState<Job[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [newKeyName, setNewKeyName] = useState(''); const [newKeyName, setNewKeyName] = useState('');
@@ -90,16 +106,18 @@ export default function Account() {
} }
(async () => { (async () => {
try { try {
const [u, a, l, k] = await Promise.all([ const [u, a, l, k, j] = await Promise.all([
api<User>('/users/me'), api<User>('/users/me'),
api<Account>('/accounts/me'), api<Account>('/accounts/me'),
api<LedgerEntry[]>('/accounts/me/ledger'), api<LedgerEntry[]>('/accounts/me/ledger'),
api<ApiKey[]>('/api-keys'), api<ApiKey[]>('/api-keys'),
api<Job[]>('/v1/jobs?limit=50'),
]); ]);
setUser(u); setUser(u);
setAccount(a); setAccount(a);
setLedger(l); setLedger(l);
setKeys(k); setKeys(k);
setJobs(j);
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error'); setError(err instanceof Error ? err.message : 'Unknown error');
} finally { } finally {
@@ -254,6 +272,63 @@ export default function Account() {
)} )}
</section> </section>
<section className='mt-8'>
<h2 className='text-lg font-semibold'>Recent jobs</h2>
{jobs.length === 0 ? (
<p className='mt-3 text-sm text-neutral-500'>No jobs yet.</p>
) : (
<div className='mt-3 overflow-x-auto rounded-lg border border-neutral-200 bg-white'>
<table className='min-w-full text-sm'>
<thead className='border-b border-neutral-200 bg-neutral-50 text-left text-xs uppercase tracking-wide text-neutral-500'>
<tr>
<th className='px-4 py-2 font-medium'>Model</th>
<th className='px-4 py-2 font-medium'>Status</th>
<th className='px-4 py-2 font-medium'>Provider</th>
<th className='px-4 py-2 font-medium'>Tokens</th>
<th className='px-4 py-2 text-right font-medium'>xBZZ</th>
<th className='px-4 py-2 font-medium'>When</th>
</tr>
</thead>
<tbody className='divide-y divide-neutral-200'>
{jobs.slice(0, 25).map((j) => {
const tokens =
j.promptTokens !== null || j.completionTokens !== null
? `${j.promptTokens ?? '—'} / ${j.completionTokens ?? '—'}`
: '—';
const xbzz = j.actualXbzzWei
? formatXbzz(j.actualXbzzWei)
: `~${formatXbzz(j.estimatedMaxXbzzWei)}`;
const statusColor =
j.status === 'claimed' || j.status === 'delivered'
? 'bg-emerald-100 text-emerald-800'
: j.status === 'failed' || j.status === 'cancelled'
? 'bg-red-100 text-red-800'
: 'bg-neutral-100 text-neutral-700';
return (
<tr key={j.id}>
<td className='px-4 py-2 font-mono text-xs'>{j.modelId}</td>
<td className='px-4 py-2'>
<span className={`rounded-full px-2 py-0.5 text-xs font-medium ${statusColor}`}>
{j.status}
</span>
</td>
<td className='px-4 py-2 font-mono text-xs text-neutral-500'>
{j.provider.slice(0, 6)}…{j.provider.slice(-4)}
</td>
<td className='px-4 py-2 font-mono text-xs'>{tokens}</td>
<td className='px-4 py-2 text-right font-mono text-xs'>{xbzz}</td>
<td className='px-4 py-2 text-xs text-neutral-500'>
{new Date(j.createdAt).toLocaleString()}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</section>
<section className='mt-8'> <section className='mt-8'>
<h2 className='text-lg font-semibold'>Recent activity</h2> <h2 className='text-lg font-semibold'>Recent activity</h2>
{ledger.length === 0 ? ( {ledger.length === 0 ? (