@@ -2,7 +2,13 @@ import { fetchUserAttributes, signOut } from 'aws-amplify/auth'
22import { useCallback , useEffect , useMemo , useRef , useState } from 'react'
33import { api , apiUrl } from '../lib/cloud'
44import { newToken , pairingCode , sha256Hex } from '../lib/enrollment'
5+ import { fleetOf , rankBoard } from '../lib/leaderboard'
56import { 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'
612import { useNow } from './board/util'
713import BoardView from './BoardView'
814import 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. */
2531const 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+
2742export 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