From 86e0ab356a578b0c263d512dedca308eab137546 Mon Sep 17 00:00:00 2001 From: Felix Faerber Date: Wed, 27 May 2026 15:46:28 +0300 Subject: [PATCH] account: add Recent jobs table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- app/routes/account.tsx | 77 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/app/routes/account.tsx b/app/routes/account.tsx index 3bc2593..76f7099 100644 --- a/app/routes/account.tsx +++ b/app/routes/account.tsx @@ -31,6 +31,21 @@ type ApiKey = { modelAllowlist: 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() { return [{ title: 'Account · t4t Gateway' }]; @@ -75,6 +90,7 @@ export default function Account() { const [account, setAccount] = useState(null); const [ledger, setLedger] = useState([]); const [keys, setKeys] = useState([]); + const [jobs, setJobs] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [newKeyName, setNewKeyName] = useState(''); @@ -90,16 +106,18 @@ export default function Account() { } (async () => { try { - const [u, a, l, k] = await Promise.all([ + const [u, a, l, k, j] = await Promise.all([ api('/users/me'), api('/accounts/me'), api('/accounts/me/ledger'), api('/api-keys'), + api('/v1/jobs?limit=50'), ]); setUser(u); setAccount(a); setLedger(l); setKeys(k); + setJobs(j); } catch (err) { setError(err instanceof Error ? err.message : 'Unknown error'); } finally { @@ -254,6 +272,63 @@ export default function Account() { )} +
+

Recent jobs

+ {jobs.length === 0 ? ( +

No jobs yet.

+ ) : ( +
+ + + + + + + + + + + + + {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 ( + + + + + + + + + ); + })} + +
ModelStatusProviderTokensxBZZWhen
{j.modelId} + + {j.status} + + + {j.provider.slice(0, 6)}…{j.provider.slice(-4)} + {tokens}{xbzz} + {new Date(j.createdAt).toLocaleString()} +
+
+ )} +
+

Recent activity

{ledger.length === 0 ? (