@@ -219,6 +219,7 @@
< h1 > meshnet tracker< / h1 >
< span class = "meta" id = "self-url" > < / span >
< span class = "meta" id = "refreshed" > < / span >
< button class = "small" id = "refresh-btn" type = "button" onclick = "refreshActiveTab(true)" style = "margin-left:auto" > refresh< / button >
< / header >
< nav class = "dashboard-tabs" aria-label = "Dashboard sections" >
< button id = "tab-overview" class = "active" onclick = "switchDashboardTab('overview')" > Overview< / button >
@@ -379,6 +380,75 @@ function modelServedCopiesFromMap(map, model) {
return best ;
}
const panelSig = { } ;
let pendingChatModelRefresh = false ;
function stableSig ( value ) {
return JSON . stringify ( value ) ;
}
function renderIfChanged ( panelId , data , renderFn ) {
const sig = stableSig ( data ) ;
if ( panelSig [ panelId ] === sig ) return false ;
panelSig [ panelId ] = sig ;
renderFn ( data ) ;
return true ;
}
function isEditing ( idOrEl ) {
const el = typeof idOrEl === "string" ? $ ( idOrEl ) : idOrEl ;
if ( ! el ) return false ;
const active = document . activeElement ;
return active === el || ( active && el . contains ( active ) ) ;
}
function refreshBlocked ( ) {
return selectionActive ( )
|| chatBusy
|| isEditing ( "account-nickname" )
|| isEditing ( "chat-prompt" )
|| isEditing ( "chat-model" ) ;
}
function syncKeyedList ( container , items , keyFn , sigFn , createFn , patchFn ) {
if ( ! container ) return ;
const sig = stableSig ( items . map ( item => sigFn ( item ) ) ) ;
if ( container . dataset . listSig === sig ) return ;
container . dataset . listSig = sig ;
if ( ! items . length ) {
container . className = "chat-session-list empty-state" ;
container . replaceChildren ( document . createTextNode ( "No chats yet" ) ) ;
return ;
}
container . className = "chat-session-list" ;
const existing = new Map ( ) ;
for ( const child of container . querySelectorAll ( "[data-session-id]" ) ) {
existing . set ( child . dataset . sessionId , child ) ;
}
const seen = new Set ( ) ;
for ( const item of items ) {
const key = keyFn ( item ) ;
seen . add ( key ) ;
let el = existing . get ( key ) ;
if ( ! el ) {
el = createFn ( item ) ;
} else {
patchFn ( el , item ) ;
}
container . appendChild ( el ) ;
}
for ( const [ key , el ] of existing ) {
if ( ! seen . has ( key ) ) el . remove ( ) ;
}
}
function chatModelsSignature ( ) {
return availableModels . map ( model => ( {
id : model . id ,
label : chatModelOptionLabel ( model , lastRouting ) ,
} ) ) ;
}
async function fetchJson ( path ) {
try {
const headers = { } ;
@@ -997,7 +1067,7 @@ function createNewChatSession() {
clearChatPrompt ( ) ;
saveChatSessionsStore ( ) ;
renderChatSessionList ( ) ;
renderChatHistory ( ) ;
renderChatHistory ( true ) ;
renderChatAuthHint ( ) ;
const promptEl = $ ( "chat-prompt" ) ;
if ( promptEl ) promptEl . focus ( ) ;
@@ -1019,7 +1089,7 @@ function selectChatSession(sessionId) {
}
localStorage . setItem ( CHAT _ACTIVE _SESSION _KEY , activeChatSessionId ) ;
renderChatSessionList ( ) ;
renderChatHistory ( ) ;
renderChatHistory ( true ) ;
renderChatAuthHint ( ) ;
}
@@ -1045,7 +1115,7 @@ function deleteChatSession(sessionId) {
}
saveChatSessionsStore ( ) ;
renderChatSessionList ( ) ;
renderChatHistory ( ) ;
renderChatHistory ( true ) ;
renderChatModels ( ) ;
}
@@ -1067,31 +1137,54 @@ function initChatSessions() {
if ( active . model ) selectedChatModel = active . model ;
}
renderChatSessionList ( ) ;
renderChatHistory ( ) ;
renderChatHistory ( true ) ;
}
function renderChatSessionList ( ) {
const list = $ ( "chat-session-list" ) ;
if ( ! list ) return ;
if ( ! chatSessions . length ) {
list . className = " chat-s ession-list empty-state" ;
list . innerHTML = "No chats yet" ;
return ;
syncKeyedList (
list ,
chatS essions ,
session => session . id ,
session => ` ${ session . id } | ${ session . updatedAt || "" } | ${ chatSessionTitle ( session ) } | ${ session . id === activeChatSessionId } ` ,
createSessionRowElement ,
patchSessionRowElement ,
) ;
}
function createSessionRowElement ( session ) {
const el = document . createElement ( "div" ) ;
el . className = "chat-session-item" ;
el . dataset . sessionId = session . id ;
el . setAttribute ( "role" , "button" ) ;
el . tabIndex = 0 ;
const title = document . createElement ( "div" ) ;
title . className = "chat-session-title" ;
const meta = document . createElement ( "div" ) ;
meta . className = "chat-session-meta" ;
const del = document . createElement ( "button" ) ;
del . type = "button" ;
del . className = "chat-session-delete" ;
del . title = "Delete chat" ;
del . dataset . deleteSession = session . id ;
del . textContent = "× " ;
el . append ( title , meta , del ) ;
patchSessionRowElement ( el , session ) ;
return el ;
}
function patchSessionRowElement ( el , session ) {
el . classList . toggle ( "active" , session . id === activeChatSessionId ) ;
el . querySelector ( ".chat-session-title" ) . textContent = chatSessionTitle ( session ) ;
const when = formatSessionTime ( session . updatedAt || session . createdAt ) ;
const meta = el . querySelector ( ".chat-session-meta" ) ;
if ( when ) {
meta . textContent = when ;
meta . hidden = false ;
} else {
meta . textContent = "" ;
meta . hidden = true ;
}
list . className = "chat-session-list" ;
list . innerHTML = chatSessions . map ( session => {
const active = session . id === activeChatSessionId ? " active" : "" ;
const title = esc ( chatSessionTitle ( session ) ) ;
const when = esc ( formatSessionTime ( session . updatedAt || session . createdAt ) ) ;
const sessionId = esc ( session . id ) ;
return ` <div class="chat-session-item ${ active } " role="button" tabindex="0" ` +
` data-session-id=" ${ sessionId } "> ` +
` <div class="chat-session-title"> ${ title } </div> ` +
( when ? ` <div class="chat-session-meta"> ${ when } </div> ` : "" ) +
` <button type="button" class="chat-session-delete" title="Delete chat" ` +
` data-delete-session=" ${ sessionId } ">× </button> ` +
` </div> ` ;
} ) . join ( "" ) ;
}
function bindChatSessionList ( ) {
@@ -1134,6 +1227,7 @@ function switchDashboardTab(name) {
const promptEl = $ ( "chat-prompt" ) ;
if ( promptEl ) promptEl . focus ( ) ;
}
refreshActiveTab ( true ) ;
}
function updateSectionVisibility ( ) {
@@ -1149,22 +1243,33 @@ function renderChatStatus(text) {
$ ( "chat-status" ) . textContent = text ;
}
function renderChatHistory ( ) {
function renderChatHistory ( force ) {
const history = $ ( "chat-history" ) ;
if ( ! history ) return ;
if ( ! force && chatHistory . some ( msg => msg . streaming ) ) return ;
if ( ! chatHistory . length ) {
if ( history . dataset . historySig === "empty" ) return ;
history . dataset . historySig = "empty" ;
history . className = "chat-messages empty" ;
history . innerHTML = '<div class="chat-messages-inner">Send a message to start this conversation.</div>' ;
return ;
}
const sig = stableSig ( chatHistory . map ( msg => ( {
role : msg . role ,
content : msg . content ,
streaming : Boolean ( msg . streaming ) ,
} ) ) ) ;
if ( ! force && history . dataset . historySig === sig ) return ;
history . dataset . historySig = sig ;
history . className = "chat-messages" ;
const nearBottom = history . scrollHeight - history . scrollTop - history . clientHeight < 80 ;
const rows = chatHistory . map ( msg => {
const role = msg . role === "user" ? "user" : msg . role === "assistant" ? "assistant" : "error" ;
const streaming = msg . streaming ? " streaming" : "" ;
return ` <div class="chat-row ${ role } "><div class="chat-bubble ${ role } ${ streaming } "> ${ esc ( msg . content ) } </div></div> ` ;
} ) . join ( "" ) ;
history . innerHTML = ` <div class="chat-messages-inner"> ${ rows } </div> ` ;
history . scrollTop = history . scrollHeight ;
if ( nearBottom || force ) history . scrollTop = history . scrollHeight ;
}
function findRoutingForModel ( model , routing ) {
@@ -1205,9 +1310,17 @@ function chatModelOptionLabel(model, routing) {
return ` ${ base } · ${ health } · ${ speedText } ${ suffix } ` ;
}
function renderChatModels ( ) {
function renderChatModels ( force ) {
const select = $ ( "chat-model" ) ;
if ( ! select ) return ;
const sig = stableSig ( chatModelsSignature ( ) ) ;
if ( ! force && panelSig . chatModels === sig ) return ;
if ( ! force && isEditing ( select ) ) {
pendingChatModelRefresh = true ;
return ;
}
pendingChatModelRefresh = false ;
panelSig . chatModels = sig ;
const models = availableModels . slice ( ) ;
if ( ! models . length ) {
select . innerHTML = '<option value="">no models available</option>' ;
@@ -1226,6 +1339,15 @@ function renderChatModels() {
select . value = selectedChatModel ;
}
function bindChatModelSelect ( ) {
const select = $ ( "chat-model" ) ;
if ( ! select || select . dataset . bound ) return ;
select . dataset . bound = "1" ;
select . addEventListener ( "blur" , ( ) => {
if ( pendingChatModelRefresh ) renderChatModels ( true ) ;
} ) ;
}
function selectChatModel ( value ) {
selectedChatModel = value || "" ;
localStorage . setItem ( "meshnet_chat_model" , selectedChatModel ) ;
@@ -1325,7 +1447,8 @@ async function doRegister() {
{ email : email || null , wallet : wallet || null , nickname : nickname || null , password } ) ;
if ( ! r . ok ) { renderAuthForms ( r . data . error || "registration failed" ) ; return ; }
setSession ( r . data . session _token ) ;
await render AccountPanel ( ) ;
await load AccountSummary ( true ) ;
await refreshActiveTab ( true ) ;
}
async function doLogin ( ) {
@@ -1334,7 +1457,8 @@ async function doLogin() {
const r = await apiCall ( "/v1/auth/login" , "POST" , { identifier , password } ) ;
if ( ! r . ok ) { renderAuthForms ( r . data . error || "login failed" ) ; return ; }
setSession ( r . data . session _token ) ;
await render AccountPanel ( ) ;
await load AccountSummary ( true ) ;
await refreshActiveTab ( true ) ;
}
async function doLogout ( ) {
@@ -1345,19 +1469,19 @@ async function doLogout() {
async function createKey ( ) {
const r = await apiCall ( "/v1/account/keys" , "POST" , { } ) ;
if ( r . ok ) await render AccountPanel ( ) ;
if ( r . ok ) await load AccountSummary ( true ) ;
}
async function revokeKey ( key ) {
if ( ! confirm ( "Revoke this API key? Clients using it will stop working." ) ) return ;
await apiCall ( "/v1/account/keys/revoke" , "POST" , { api _key : key } ) ;
await render AccountPanel ( ) ;
await load AccountSummary ( true ) ;
}
async function topupKey ( key ) {
const r = await apiCall ( "/v1/account/topup" , "POST" , { api _key : key } ) ;
if ( ! r . ok ) alert ( r . data . error || "top-up failed" ) ;
await render AccountPanel ( ) ;
await load AccountSummary ( true ) ;
}
const COPY _TOOLTIP _MS = 2000 ;
@@ -1421,42 +1545,32 @@ function renderChatAuthHint() {
}
}
async function saveNickname ( ) {
const nickname = $ ( "account-nickname" ) . value . trim ( ) ;
const r = await apiCall ( "/v1/account/profile" , "POST" , { nickname : nickname || null } ) ;
if ( ! r . ok ) alert ( r . data . error || "nickname update failed" ) ;
else await renderAccountPanel ( ) ;
}
let accountTopupAmount = 0 ;
async function render AccountPanel( ) {
const r = await apiCall ( "/v1/ account" ) ;
if ( r . status === 404 ) { // accounts disabled on this tracker
$ ( "account-section" ) . style . display = "none" ;
accountApiKeys = [ ] ;
accountUsageRecords = [ ] ;
renderChatAuthHint ( ) ;
setLoggedInMode ( false ) ;
setAdminMode ( false ) ;
return ;
}
if ( ! r . ok ) { setSession ( null ) ; renderAuthForms ( ) ; return ; }
const { account , api _keys , balances , total _balance , usage , topup _amount } = r . data ;
accountApiKeys = Array . isArray ( api _keys ) ? api _keys . slice ( ) : [ ] ;
accountUsageRecords = ( usage && ( usage . records || usage . recent ) ) || [ ] ;
const who = accountDisplayName ( account ) ;
let html =
` <div><b> ${ esc ( who ) } </b> <span class="pill"> ${ esc ( account . role ) } </span> ` +
function build AccountPanelShell ( ) {
$ ( "account" ) . innerHTML =
` <div id="account-panel-root"> ` +
` <div><b data-bind="who"></b> <span class="pill" data-bind="role"></span> ` +
` <button class="small" style="float:right" onclick="doLogout()">log out</button></div> ` +
` <div style="margin:6px 0;display:flex;gap:8px;align-items:center;flex-wrap:wrap"> ` +
` <label class="dim" style="margin:0">nickname</label> ` +
` <input id="account-nickname" type="text" value=" ${ esc ( account . nickname || "" ) } " placeholder="display name" style="min-width:180px"> ` +
` <input id="account-nickname" type="text" placeholder="display name" style="min-width:180px"> ` +
` <button class="small" type="button" onclick="saveNickname()">save</button></div> ` +
` <div style="margin:6px 0">balance: <b class=" ${ total _balance > 0 ? "ok" : "bad" } "> ` +
` ${ usdt ( total _balance ) } </b> USDT · requests: <b> ${ usage . requests } </b> · ` +
` tokens: <b> ${ usage . total _tokens } </b> · spent: <b> ${ usdt ( usage . total _cost ) } </b> USDT</div> ` ;
html += '<div style ="margin:6px 0"><b class="dim">API keys </b> ' +
'<button class="small" onclick="createKey()">+ new key</button ></div>' ;
if ( api _keys . length ) {
` <div style="margin:6px 0">balance: <b data-bind="balance"></b> USDT · requests: <b data-bind="requests"></b> · ` +
` tokens: <b data-bind="tokens"></b> · spent: <b data-bind="spent"></b> USDT</div> ` +
` <div style="margin:6px 0"><b class="dim">API keys</b> ` +
` <button class ="s mall" onclick="createKey()">+ new key</button></div> ` +
` <div id="account-keys"></div ></div>` ;
}
function renderAccountKeys ( api _keys , balances , topup _amount ) {
const el = $ ( "account-keys" ) ;
if ( ! el ) return ;
if ( ! api _keys . length ) {
el . innerHTML = '<div class="empty">no active keys</div>' ;
return ;
}
let html = "" ;
for ( const key of api _keys ) {
html += ` <div class="keybox"> ` +
` <span class="key-text" data-key=" ${ esc ( key ) } " onclick="selectApiKeyText(this)" ondblclick="copyApiKeyFromTextEl(this)"> ${ esc ( key ) } </span> ` +
@@ -1467,19 +1581,76 @@ async function renderAccountPanel() {
: "" ) +
` <button class="small" onclick="revokeKey(' ${ esc ( key ) } ')">revoke</button></div> ` ;
}
} else {
html += '<div class="empty">no active keys</div>' ;
el . innerHTML = html ;
}
function patchAccountPanelView ( account , api _keys , balances , total _balance , usage , topup _amount ) {
const root = $ ( "account-panel-root" ) ;
if ( ! root ) return false ;
accountTopupAmount = topup _amount || 0 ;
root . querySelector ( '[data-bind="who"]' ) . textContent = accountDisplayName ( account ) ;
root . querySelector ( '[data-bind="role"]' ) . textContent = account . role ;
const bal = root . querySelector ( '[data-bind="balance"]' ) ;
bal . textContent = usdt ( total _balance ) ;
bal . className = total _balance > 0 ? "ok" : "bad" ;
root . querySelector ( '[data-bind="requests"]' ) . textContent = String ( usage . requests ) ;
root . querySelector ( '[data-bind="tokens"]' ) . textContent = String ( usage . total _tokens ) ;
root . querySelector ( '[data-bind="spent"]' ) . textContent = usdt ( usage . total _cost ) ;
const nick = $ ( "account-nickname" ) ;
if ( nick && ! isEditing ( nick ) ) nick . value = account . nickname || "" ;
renderIfChanged ( "account-keys" , { api _keys , balances , topup _amount } , data => {
renderAccountKeys ( data . api _keys , data . balances , data . topup _amount ) ;
} ) ;
return true ;
}
async function saveNickname ( ) {
const nickname = $ ( "account-nickname" ) . value . trim ( ) ;
const r = await apiCall ( "/v1/account/profile" , "POST" , { nickname : nickname || null } ) ;
if ( ! r . ok ) alert ( r . data . error || "nickname update failed" ) ;
else await loadAccountSummary ( true ) ;
}
function applyAccountSummary ( data , force ) {
const { account , api _keys , balances , total _balance , usage , topup _amount } = data ;
accountApiKeys = Array . isArray ( api _keys ) ? api _keys . slice ( ) : [ ] ;
if ( force || ! patchAccountPanelView ( account , api _keys , balances , total _balance , usage , topup _amount ) ) {
buildAccountPanelShell ( ) ;
patchAccountPanelView ( account , api _keys , balances , total _balance , usage , topup _amount ) ;
}
$ ( "account" ) . innerHTML = html ;
renderUsageSummary ( accountUsageRecords ) ;
renderNodeThroughput ( lastStats ) ;
renderBillingUsage ( accountUsageRecords ) ;
renderChatAuthHint ( ) ;
renderChatModels ( ) ;
renderChatHistory ( ) ;
setLoggedInMode ( true ) ;
setAdminMode ( account . role === "admin" ) ;
if ( account . role === "admin" ) await renderAdminPanel ( ) ;
}
async function loadAccountSummary ( force ) {
const r = await apiCall ( "/v1/account" ) ;
if ( r . status === 404 ) {
$ ( "account-section" ) . style . display = "none" ;
accountApiKeys = [ ] ;
accountUsageRecords = [ ] ;
renderChatAuthHint ( ) ;
setLoggedInMode ( false ) ;
setAdminMode ( false ) ;
return false ;
}
if ( ! r . ok ) { setSession ( null ) ; renderAuthForms ( ) ; return false ; }
applyAccountSummary ( r . data , force ) ;
if ( dashboardTab === "chat" && ! refreshBlocked ( ) ) renderChatModels ( force ) ;
return true ;
}
async function loadAccountUsage ( ) {
const r = await apiCall ( "/v1/account/usage" ) ;
if ( ! r . ok ) return ;
accountUsageRecords = ( r . data . records || r . data . recent ) || [ ] ;
renderIfChanged ( "usage-summary" , accountUsageRecords , ( ) => renderUsageSummary ( accountUsageRecords ) ) ;
renderIfChanged ( "billing-usage" , accountUsageRecords , ( ) => renderBillingUsage ( accountUsageRecords ) ) ;
}
/** @deprecated use loadAccountSummary */
async function renderAccountPanel ( force ) {
return loadAccountSummary ( force ) ;
}
let chatAbortController = null ;
@@ -1501,7 +1672,7 @@ function updateStreamingChatBubble(content) {
if ( ! history ) return ;
const bubbles = history . querySelectorAll ( ".chat-bubble.assistant.streaming" ) ;
const bubble = bubbles [ bubbles . length - 1 ] ;
if ( ! bubble ) { renderChatHistory ( ) ; return ; }
if ( ! bubble ) { renderChatHistory ( true ) ; return ; }
bubble . textContent = content ;
const nearBottom = history . scrollHeight - history . scrollTop - history . clientHeight < 80 ;
if ( nearBottom ) history . scrollTop = history . scrollHeight ;
@@ -1511,7 +1682,7 @@ function finalizeAssistantMessage(msg) {
delete msg . streaming ;
if ( ! chatHistory . includes ( msg ) ) chatHistory . push ( msg ) ;
if ( ! msg . content ) msg . content = "(empty response)" ;
renderChatHistory ( ) ;
renderChatHistory ( true ) ;
persistActiveChatSession ( ) ;
}
@@ -1519,7 +1690,7 @@ function removeAssistantMessage(msg) {
const index = chatHistory . indexOf ( msg ) ;
if ( index >= 0 ) {
chatHistory . splice ( index , 1 ) ;
renderChatHistory ( ) ;
renderChatHistory ( true ) ;
}
}
@@ -1550,7 +1721,7 @@ async function sendChat() {
chatHistory . push ( { role : "user" , content : prompt , model : selectedChatModel } ) ;
const assistantMessage = { role : "assistant" , content : "" , model : selectedChatModel , streaming : true } ;
chatHistory . push ( assistantMessage ) ;
renderChatHistory ( ) ;
renderChatHistory ( true ) ;
persistActiveChatSession ( ) ;
renderChatStatus ( "sending request…" ) ;
let tokens = 0 ;
@@ -1633,7 +1804,7 @@ async function sendChat() {
removeAssistantMessage ( assistantMessage ) ;
const error = ( err && err . message ) || "request failed" ;
chatHistory . push ( { role : "error" , content : error , model : selectedChatModel } ) ;
renderChatHistory ( ) ;
renderChatHistory ( true ) ;
persistActiveChatSession ( ) ;
renderChatStatus ( error ) ;
}
@@ -1661,29 +1832,13 @@ function bindChatPromptShortcuts() {
} ) ;
}
async function renderAdminPanel ( ) {
const r = await apiCall ( "/v1/admin/accounts" ) ;
if ( ! r . ok ) { setAdminMode ( false ) ; return ; }
const rows = ( r . data . accounts || [ ] ) . map ( a => {
const balance = Object . values ( a . balances || { } ) . reduce ( ( x , y ) => x + y , 0 ) ;
return [
esc ( short ( accountDisplayName ( a ) , 24 ) ) ,
` <span class="pill"> ${ esc ( a . role ) } </span> ` ,
String ( ( a . api _keys || [ ] ) . length ) ,
` <span class="num ${ balance > 0 ? "ok" : "bad" } "> ${ usdt ( balance ) } </span> ` ,
new Date ( a . created _ts * 1000 ) . toLocaleDateString ( ) ,
] ;
} ) ;
$ ( "admin" ) . innerHTML = table ( [ "account" , "role" , "keys" , "balance (USDT)" , "created" ] , rows ) ;
}
async function cancelProxyRequest ( requestId ) {
const r = await apiCall (
` /v1/proxy/requests/ ${ encodeURIComponent ( requestId ) } /cancel ` ,
"POST" ,
{ } ,
) ;
if ( r . ok ) refresh ( ) ;
if ( r . ok ) await refreshCallWall ( ) ;
}
$ ( "call-wall" ) . addEventListener ( "click" , ( event ) => {
@@ -1694,25 +1849,8 @@ $("call-wall").addEventListener("click", (event) => {
if ( requestId ) cancelProxyRequest ( requestId ) ;
} ) ;
async function refresh ( ) {
$ ( "self-url" ) . textContent = location . host ;
const [ raft , map , stats , models , consoleData , routing , adminData ] = await Promise . all ( [
fetchJson ( "/v1/raft/status" ) ,
fetchJson ( "/v1/network/map" ) ,
fetchJson ( "/v1/stats" ) ,
fetchJson ( "/v1/models" ) ,
fetchJson ( "/v1/console" ) ,
fetchJson ( "/v1/routing" ) ,
isAdmin ? Promise . all ( [
fetchJson ( "/v1/billing/summary" ) ,
fetchJson ( "/v1/billing/settlements" ) ,
fetchJson ( "/v1/registry/wallets" ) ,
] ) : Promise . resolve ( [ null , null , null ] ) ,
] ) ;
const [ summary , settlements , wallets ] = adminData ;
lastStats = stats ;
lastRouting = routing ;
lastNetworkMap = map ;
function applyAvailableModels ( models , map ) {
lastNetworkMap = map || lastNetworkMap ;
availableModels = ( ( models && models . data ) || [ ] ) . map ( model => {
const entry = {
id : model . id ,
@@ -1723,50 +1861,154 @@ async function refresh() {
coverage : model . shard _coverage _percentage ,
servedCopies : model . served _model _copies ,
} ;
entry . servedCopies = modelServedCopiesFromMap ( m ap, entry ) ;
entry . servedCopies = modelServedCopiesFromMap ( lastNetworkM ap, entry ) ;
return entry ;
} ) . filter ( model => model . id ) ;
renderHive ( raft ) ;
renderNodes ( map ) ;
renderBilling ( summary ) ;
renderSettlements ( settlements ) ;
renderFraud ( wallets , summary ) ;
renderStats ( stats ) ;
renderRouting ( routing ) ;
renderCallWall ( consoleData , stats , map ) ;
renderConsole ( consoleData ) ;
renderNodeThroughput ( stats ) ;
renderChatModels ( ) ;
renderChatHistory ( ) ;
}
function markRefreshed ( ) {
$ ( "refreshed" ) . textContent = "refreshed " + new Date ( ) . toLocaleTimeString ( ) ;
}
const REFRESH _MS = 10000 ;
async function fetchOverviewTab ( ) {
const fetches = [
fetchJson ( "/v1/raft/status" ) ,
fetchJson ( "/v1/network/map" ) ,
fetchJson ( "/v1/stats" ) ,
fetchJson ( "/v1/routing" ) ,
fetchJson ( "/v1/console" ) ,
] ;
if ( isLoggedIn ) fetches . push ( apiCall ( "/v1/account" ) ) ;
const results = await Promise . all ( fetches ) ;
const [ raft , map , stats , routing , consoleData , accountResp ] = results ;
lastStats = stats ;
lastRouting = routing ;
lastNetworkMap = map ;
if ( accountResp && accountResp . ok ) applyAccountSummary ( accountResp . data , false ) ;
renderIfChanged ( "hive" , raft , renderHive ) ;
renderIfChanged ( "nodes" , map , renderNodes ) ;
renderIfChanged ( "stats" , stats , renderStats ) ;
renderIfChanged ( "routing" , routing , renderRouting ) ;
renderIfChanged ( "call-wall" , { consoleData , stats , map } , data => renderCallWall ( data . consoleData , data . stats , data . map ) ) ;
}
async function fetchChatTab ( ) {
const fetches = [
fetchJson ( "/v1/network/map" ) ,
fetchJson ( "/v1/models" ) ,
fetchJson ( "/v1/routing" ) ,
] ;
if ( isLoggedIn ) fetches . push ( apiCall ( "/v1/account" ) ) ;
const [ map , models , routing , accountResp ] = await Promise . all ( fetches ) ;
lastRouting = routing ;
applyAvailableModels ( models , map ) ;
if ( accountResp && accountResp . ok ) applyAccountSummary ( accountResp . data , false ) ;
if ( ! refreshBlocked ( ) ) {
renderChatModels ( true ) ;
} else {
pendingChatModelRefresh = true ;
}
}
async function fetchBillingTab ( ) {
const [ stats , , accountResp , adminData ] = await Promise . all ( [
fetchJson ( "/v1/stats" ) ,
loadAccountUsage ( ) ,
isLoggedIn ? apiCall ( "/v1/account" ) : Promise . resolve ( null ) ,
isAdmin ? Promise . all ( [
fetchJson ( "/v1/billing/summary" ) ,
fetchJson ( "/v1/billing/settlements" ) ,
] ) : Promise . resolve ( null ) ,
] ) ;
lastStats = stats ;
if ( accountResp && accountResp . ok ) applyAccountSummary ( accountResp . data , false ) ;
renderIfChanged ( "node-throughput" , stats , renderNodeThroughput ) ;
if ( adminData ) {
const [ summary , settlements ] = adminData ;
renderIfChanged ( "billing-summary" , summary , renderBilling ) ;
renderIfChanged ( "settlements" , settlements , renderSettlements ) ;
}
}
async function fetchAdminTab ( ) {
const fetches = [
fetchJson ( "/v1/console" ) ,
fetchJson ( "/v1/billing/summary" ) ,
fetchJson ( "/v1/registry/wallets" ) ,
] ;
if ( isAdmin ) fetches . push ( apiCall ( "/v1/admin/accounts" ) ) ;
const results = await Promise . all ( fetches ) ;
const [ consoleData , summary , wallets , adminResp ] = results ;
renderIfChanged ( "console" , consoleData , renderConsole ) ;
renderIfChanged ( "billing-summary" , summary , data => renderBilling ( data ) ) ;
renderIfChanged ( "fraud" , { wallets , summary } , data => renderFraud ( data . wallets , data . summary ) ) ;
if ( adminResp && adminResp . ok ) {
renderIfChanged ( "admin" , adminResp . data . accounts || [ ] , accounts => {
const rows = accounts . map ( a => {
const balance = Object . values ( a . balances || { } ) . reduce ( ( x , y ) => x + y , 0 ) ;
return [
esc ( short ( accountDisplayName ( a ) , 24 ) ) ,
` <span class="pill"> ${ esc ( a . role ) } </span> ` ,
String ( ( a . api _keys || [ ] ) . length ) ,
` <span class="num ${ balance > 0 ? "ok" : "bad" } "> ${ usdt ( balance ) } </span> ` ,
new Date ( a . created _ts * 1000 ) . toLocaleDateString ( ) ,
] ;
} ) ;
$ ( "admin" ) . innerHTML = table ( [ "account" , "role" , "keys" , "balance (USDT)" , "created" ] , rows ) ;
} ) ;
}
}
async function refreshCallWall ( ) {
const [ consoleData , map ] = await Promise . all ( [
fetchJson ( "/v1/console" ) ,
fetchJson ( "/v1/network/map" ) ,
] ) ;
if ( map ) lastNetworkMap = map ;
renderIfChanged ( "call-wall" , { consoleData , stats : lastStats , map } , data =>
renderCallWall ( data . consoleData , data . stats , data . map ) ) ;
}
const TAB _FETCHERS = {
overview : fetchOverviewTab ,
chat : fetchChatTab ,
billing : fetchBillingTab ,
admin : fetchAdminTab ,
} ;
async function refreshActiveTab ( force ) {
$ ( "self-url" ) . textContent = location . host ;
if ( refreshBlocked ( ) && ! force ) return ;
const fetcher = TAB _FETCHERS [ dashboardTab ] ;
if ( fetcher ) await fetcher ( ) ;
markRefreshed ( ) ;
}
const CALL _WALL _POLL _MS = 5000 ;
async function pollCallWallIfIdle ( ) {
if ( dashboardTab !== "overview" || refreshBlocked ( ) ) return ;
await refreshCallWall ( ) ;
}
function selectionActive ( ) {
const sel = window . getSelection ( ) ;
return sel && ! sel . isCollapsed && sel . toString ( ) . length > 0 ;
}
async function refreshIfIdle ( ) {
if ( selectionActive ( ) ) return ;
await refresh ( ) ;
}
refresh ( ) ;
bindChatSessionList ( ) ;
bindChatModelSelect ( ) ;
initChatSessions ( ) ;
bindChatPromptShortcuts ( ) ;
renderAccountPanel ( ) ;
renderChatModels ( ) ;
renderChatHistory ( ) ;
renderChatAuthHint ( ) ;
setInterval ( refreshIfIdle , REFRESH _MS ) ;
setInterval ( ( ) => {
if ( ! sessionToken && ! isLoggedIn ) return ;
if ( selectionActive ( ) ) return ;
renderAccountPanel ( ) ;
} , REFRESH _MS ) ;
( async ( ) => {
if ( sessionToken ) await loadAccountSummary ( true ) ;
else renderAuthForms ( ) ;
await refreshActiveTab ( true ) ;
renderChatHistory ( true ) ;
renderChatAuthHint ( ) ;
if ( dashboardTab === "chat" ) renderChatModels ( true ) ;
} ) ( ) ;
setInterval ( pollCallWallIfIdle , CALL _WALL _POLL _MS ) ;
< / script >
< / body >
< / html >