Skip to content

Commit c54d7af

Browse files
committed
feat: add admin shell to cloud portal (RootRail, sidebar, leaderboard)
The cloud portal at platform.tokenhud.com now uses the same two-level navigation as the self-host board: - AdminTopbar with breadcrumb and live status - RootRail for section switching (monitoring, leaderboard, settings) - AdminSidebar with machine list and section nav (monitoring) - SectionRail with leaderboard sub-pages (standings, live, models, demand) - BoardView in embedded mode (strips its own chrome) - LeaderboardPage with full sub-page routing Extracted AdminTopbar, AdminSidebar, and MachineRow from SelfHost.jsx into admin/ directory so both Portal and SelfHost share them.
1 parent 5de5f35 commit c54d7af

6 files changed

Lines changed: 468 additions & 284 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
description: Never add AI coding tools as contributors or co-authors
3+
alwaysApply: true
4+
---
5+
6+
# No Third-Party Tool Contributors
7+
8+
Never add third-party coding tools (Devin, Claude, Copilot, Cursor, etc.) as contributors or co-authors in git commits.
9+
10+
- Do NOT include `Co-Authored-By` trailers for any AI tool or bot
11+
- Do NOT include `Generated with [Tool]` lines in commit messages
12+
- Commits should only attribute the human developer

.devin/AGENTS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Project Rules
2+
3+
## Git Commits
4+
5+
- Never add Co-Authored-By trailers for AI tools (Devin, Claude, Copilot, etc.)
6+
- Never add "Generated with" lines in commit messages
7+
- Commits should only attribute the human developer

site/src/components/Portal.jsx

Lines changed: 164 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import { fetchUserAttributes, signOut } from 'aws-amplify/auth'
22
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
33
import { api, apiUrl } from '../lib/cloud'
44
import { newToken, pairingCode, sha256Hex } from '../lib/enrollment'
5+
import { fleetOf, rankBoard } from '../lib/leaderboard'
56
import { buildOverview } from '../lib/overview'
7+
import AdminSidebar from './admin/AdminSidebar'
8+
import AdminTopbar from './admin/AdminTopbar'
9+
import LeaderboardPage, { LEADERBOARD_KEYS, LEADERBOARD_PAGES } from './admin/LeaderboardPage'
10+
import RootRail, { SECTIONS } from './admin/RootRail'
11+
import SectionRail from './admin/SectionRail'
612
import { useNow } from './board/util'
713
import BoardView from './BoardView'
814
import AuthCard from './portal/AuthCard'
@@ -24,6 +30,15 @@ import AuthCard from './portal/AuthCard'
2430
screen, and far enough under that a missed poll is invisible. */
2531
const POLL_MS = 20_000
2632

33+
const SECTION_KEYS = SECTIONS.map(x => x.key)
34+
const SK_SECTION = 'tokenhud_cloud_section'
35+
const SK_LBPAGE = 'tokenhud_cloud_lb_page'
36+
const SK_ROOTNAV = 'tokenhud_cloud_nav_root'
37+
const SK_SUBNAV = 'tokenhud_cloud_nav_sub'
38+
39+
function load(k, fb) { try { return localStorage.getItem(k) || fb } catch { return fb } }
40+
function save(k, v) { try { localStorage.setItem(k, v) } catch {} }
41+
2742
export default function Portal({ onClose, user, onUser, onSelfHost }) {
2843
const [machines, setMachines] = useState(null)
2944
const [profile, setProfile] = useState(null)
@@ -33,11 +48,24 @@ export default function Portal({ onClose, user, onUser, onSelfHost }) {
3348
const [lastUpdate, setLastUpdate] = useState(null)
3449
const [synced, setSynced] = useState(false)
3550

51+
/* ── admin shell state ── */
52+
const [section, setSection] = useState(() => {
53+
const v = load(SK_SECTION, 'monitoring')
54+
return SECTION_KEYS.includes(v) ? v : 'monitoring'
55+
})
56+
const [lbPage, setLbPage] = useState(() => {
57+
const v = load(SK_LBPAGE, 'standings')
58+
return LEADERBOARD_KEYS.includes(v) ? v : 'standings'
59+
})
60+
const [rootMini, setRootMini] = useState(() => load(SK_ROOTNAV, '') === '1')
61+
const [collapsed, setCollapsed] = useState(() => load(SK_SUBNAV, '') === '1')
62+
const [boardState, setBoardState] = useState(null)
63+
3664
/* Every fetch this component has in flight, so a sign-out or a close does
3765
not land a response into an unmounted tree. */
3866
const inflight = useRef(null)
3967

40-
const load = useCallback(async (signal) => {
68+
const loadData = useCallback(async (signal) => {
4169
const board = await api('/api/v1/overview', { signal })
4270
setMachines(board.machines ?? [])
4371
setLastUpdate(new Date())
@@ -56,7 +84,7 @@ export default function Portal({ onClose, user, onUser, onSelfHost }) {
5684
/* A hidden tab costs a request and shows nobody anything. */
5785
if (document.hidden) return
5886
try {
59-
await load(ctrl.signal)
87+
await loadData(ctrl.signal)
6088
} catch (e) {
6189
if (e?.name === 'AbortError') return
6290
setSynced(false)
@@ -66,7 +94,7 @@ export default function Portal({ onClose, user, onUser, onSelfHost }) {
6694

6795
/* The first load runs even when the tab is hidden — the portal was just
6896
opened, so somebody is looking whatever the visibility API believes. */
69-
load(ctrl.signal).catch(e => {
97+
loadData(ctrl.signal).catch(e => {
7098
if (e?.name === 'AbortError') return
7199
setSynced(false)
72100
setError(e?.message || String(e))
@@ -83,7 +111,7 @@ export default function Portal({ onClose, user, onUser, onSelfHost }) {
83111
document.removeEventListener('visibilitychange', onVisible)
84112
setSynced(false)
85113
}
86-
}, [user, live, nonce, load])
114+
}, [user, live, nonce, loadData])
87115

88116
/* The account: the handle, and whether it is on the public board. Read once
89117
— it only changes when this page changes it. */
@@ -185,6 +213,41 @@ export default function Portal({ onClose, user, onUser, onSelfHost }) {
185213
[machines, now],
186214
)
187215

216+
/* ── admin shell helpers ── */
217+
const toggleCollapse = useCallback(() => {
218+
setCollapsed(c => { save(SK_SUBNAV, c ? '' : '1'); return !c })
219+
}, [])
220+
const toggleRoot = useCallback(() => {
221+
setRootMini(c => { save(SK_ROOTNAV, c ? '' : '1'); return !c })
222+
}, [])
223+
const goto = useCallback(key => { setSection(key); save(SK_SECTION, key) }, [])
224+
const gotoLb = useCallback(key => { setLbPage(key); save(SK_LBPAGE, key) }, [])
225+
226+
/* The fleet in the shape the Leaderboard pages expect. Computed here rather
227+
than inside the board: the Leaderboard is its own section and must not
228+
need the board mounted to have something to show. */
229+
const fleet = useMemo(() => (data ? fleetOf(data) : { entries: [] }), [data])
230+
const profiles = fleet.entries
231+
232+
/* "You" on the leaderboard. */
233+
const meId = boardState?.cur?.host || data?.latest?.[0]?.host || null
234+
235+
const liveCount = useMemo(
236+
() => profiles.reduce((a, e) => a + (e.running || []).length, 0),
237+
[profiles],
238+
)
239+
const modelCount = useMemo(
240+
() => new Set(profiles.flatMap(e => (e.models || []).filter(m => m.tokens > 0).map(m => m.model))).size,
241+
[profiles],
242+
)
243+
const myRank = useMemo(() => {
244+
if (!meId || profiles.length < 2) return null
245+
const row = rankBoard(profiles, { metric: 'tokens', period: 'all' }).rows.find(r => r.id === meId)
246+
return row ? row.rank : null
247+
}, [profiles, meId])
248+
249+
const hasSubNav = section === 'monitoring' || section === 'leaderboard'
250+
188251
/* App is still asking Cognito whether a session exists — don't flash the
189252
sign-in card at a returning visitor. */
190253
if (user === undefined) return null
@@ -197,22 +260,109 @@ export default function Portal({ onClose, user, onUser, onSelfHost }) {
197260
}
198261

199262
return (
200-
<div className="dashboard-frame">
201-
<BoardView
202-
data={data}
203-
loading={machines == null}
204-
error={error}
263+
<div className="dashboard-frame adm">
264+
<AdminTopbar
265+
onCollapse={toggleRoot}
266+
onClose={onClose}
267+
onSignOut={handleSignOut}
205268
streaming={synced}
206269
lastUpdate={lastUpdate}
207270
live={live}
208271
onToggleLive={() => setLive(l => !l)}
209-
onRetry={() => setNonce(n => n + 1)}
272+
error={error}
273+
crumb={SECTIONS.find(x => x.key === section)?.label}
210274
connLabel={user}
211-
onClose={onClose}
212-
onSignOut={handleSignOut}
213-
cloud={cloud}
214-
publicBoard={publicBoard}
215275
/>
276+
<div className={'adm-shell'
277+
+ (rootMini ? ' adm-shell--rootmini' : '')
278+
+ (collapsed ? ' adm-shell--submini' : '')
279+
+ (hasSubNav ? '' : ' adm-shell--nosub')}>
280+
281+
<RootRail
282+
section={section} onSection={goto}
283+
collapsed={rootMini} onCollapse={toggleRoot}
284+
serverLabel={user}
285+
badges={{
286+
monitoring: (data?.hosts || []).length || null,
287+
leaderboard: myRank ? '#' + myRank : null,
288+
}}
289+
/>
290+
291+
{section === 'leaderboard' && (
292+
<SectionRail
293+
title="Leaderboard"
294+
rows={LEADERBOARD_PAGES.map(row => ({
295+
...row,
296+
badge: row.key === 'standings' && myRank ? '#' + myRank
297+
: row.key === 'live' ? String(liveCount) || null
298+
: row.key === 'models' ? String(modelCount) || null
299+
: null,
300+
tone: row.key === 'live' && liveCount ? 'on' : null,
301+
}))}
302+
active={lbPage}
303+
onPick={gotoLb}
304+
collapsed={collapsed}
305+
onCollapse={toggleCollapse}
306+
/>
307+
)}
308+
309+
{section === 'monitoring' && (
310+
<AdminSidebar
311+
collapsed={collapsed} onCollapse={toggleCollapse}
312+
phase="live"
313+
board={boardState}
314+
hosts={data?.hosts || []}
315+
onPhase={() => {}}
316+
onRename={(id, label) => cloud.rename(id, label).catch(() => {})}
317+
onRemove={(host) => {
318+
/* Cloud uses machine id, not hostname. Find the machine. */
319+
const m = (machines || []).find(x => x.host === host || x.id === host)
320+
if (m) cloud.remove(m.id).catch(() => {})
321+
}}
322+
/>
323+
)}
324+
325+
<main className="adm-content">
326+
{section === 'monitoring' && (
327+
<BoardView
328+
data={data}
329+
loading={machines == null}
330+
error={error}
331+
streaming={synced}
332+
lastUpdate={lastUpdate}
333+
live={live}
334+
onToggleLive={() => setLive(l => !l)}
335+
onRetry={() => setNonce(n => n + 1)}
336+
connLabel={user}
337+
onClose={onClose}
338+
onSignOut={handleSignOut}
339+
cloud={cloud}
340+
publicBoard={publicBoard}
341+
embedded
342+
onBoardState={setBoardState}
343+
/>
344+
)}
345+
{section === 'leaderboard' && (
346+
<LeaderboardPage
347+
board={fleet}
348+
page={lbPage}
349+
meId={meId}
350+
onGoToMachines={() => goto('monitoring')}
351+
/>
352+
)}
353+
{section === 'settings' && (
354+
<div className="adm-page" style={{ padding: 'var(--space-xl) var(--page-gutter)' }}>
355+
<h2>Account</h2>
356+
<p style={{ color: 'var(--color-ink-2)', marginTop: 'var(--space-sm)' }}>
357+
Signed in as <strong>{user}</strong>
358+
</p>
359+
<div style={{ marginTop: 'var(--space-lg)', display: 'flex', gap: 'var(--space-md)' }}>
360+
<button className="btn btn--ghost" onClick={handleSignOut}>Sign out</button>
361+
</div>
362+
</div>
363+
)}
364+
</main>
365+
</div>
216366
</div>
217367
)
218368
}

0 commit comments

Comments
 (0)