2026-10-14 21:06:30 -07:00
import { get } from "./api.js" ;
2026-10-15 22:21:20 -07:00
import { ago , errorText , h , replace } from "./dom.js" ;
2026-10-14 21:06:30 -07:00
import { findTask , on , onEach , state } from "./state.js" ;
2026-10-15 13:30:09 -07:00
import { taskStyle } from "./taskcolor.js" ;
2026-10-14 22:35:53 -07:00
const refreshKinds = new Set ([ "session.created" , "session.state" , "task.created" , "task.merged" , "task.discarded" , "project.created" , "admin.health" ]);
2026-10-15 22:21:20 -07:00
export function mountStatus ( root ) {
2026-10-14 21:06:30 -07:00
let status ;
let error = "" ;
let inFlight = false ;
let again = false ;
let disposed = false ;
const render = () => {
if ( ! status ) {
replace ( root , h ( "div" , { class : error ? "empty error-text" : "empty" }, error || "Loading…" ));
return ;
}
2026-10-15 09:40:30 -07:00
const scrollTop = root . querySelector ( ".status-view" ) ? . scrollTop ?? 0 ;
2026-10-14 22:35:53 -07:00
const open = new Set ([... root . querySelectorAll ( ".problems details[open]" )]. map (( d ) => d . dataset [ "vendor" ] ?? "" ));
2026-10-15 09:40:30 -07:00
replace ( root , h ( "div" , { class : "status-view" }, error && h ( "p" , { class : "error-text" }, error ), problemsSection ( status , open ), turnsSection ( status ), rateLimitsSection ( status . rate_limits ), serverSection ( status )));
root . querySelector ( ".status-view" ). scrollTop = scrollTop ;
2026-10-14 21:06:30 -07:00
};
const refresh = async () => {
if ( inFlight ) {
again = true ;
return ;
}
inFlight = true ;
try {
const next = await get ( "/api/admin/status" );
if ( disposed ) {
return ;
}
status = next ;
error = "" ;
}
catch ( err ) {
error = `Couldn't load status: ${ errorText ( err ) } ` ;
}
inFlight = false ;
render ();
if ( again && ! disposed ) {
again = false ;
void refresh ();
}
};
const onEvent = ( detail ) => {
const ev = detail ;
if ( refreshKinds . has ( ev . kind ) || ( ev . kind === "agent.vendor" && ev . payload [ "type" ] === "rate_limit_event" )) {
void refresh ();
return ;
}
2026-10-14 22:35:53 -07:00
if ( ! status ) {
return ;
}
let changed = false ;
const proc = status . agent_processes . find (( p ) => p . session_id === ev . agent_session_id );
if ( proc && Date . parse ( ev . created_at ) - Date . parse ( proc . last_event_at ) >= 1000 ) {
proc . last_event_at = ev . created_at ;
changed = true ;
}
if ( ev . seq > status . last_event_seq ) {
2026-10-14 21:06:30 -07:00
status . last_event_seq = ev . seq ;
2026-10-14 22:35:53 -07:00
changed = true ;
}
if ( changed ) {
2026-10-14 21:06:30 -07:00
render ();
}
};
const offs = [
on ( "event" , onEvent ),
on ( "connection" , () => {
if ( state . connected ) {
void refresh ();
}
}),
onEach ([ "projects" , "tasks" ], render ),
];
render ();
void refresh ();
return () => {
disposed = true ;
for ( const off of offs ) {
off ();
}
};
}
function turnsSection ( s ) {
2026-10-15 14:55:58 -07:00
return h ( "section" , { class : "status-section" }, h ( "h2" , {}, "Running turns" ), turnList ( s . running_turns , s . agent_processes ));
2026-10-14 21:06:30 -07:00
}
2026-10-15 14:55:58 -07:00
function turnList ( turns , procs ) {
return h ( "ul" , { class : "task-list" }, ... turns . map (( t ) => {
2026-10-14 21:06:30 -07:00
const task = findTask ( t . task_id );
const project = task ? state . projects . find (( p ) => p . id === task . project_id ) : undefined ;
2026-10-15 14:55:58 -07:00
return h ( "li" , { style : taskStyle ( t . task_id ) }, h ( "a" , { href : `#/task/ ${ t . task_id } ` }, h ( "span" , { class : "dot running" , title : "running" }), h ( "span" , { class : "task-title" }, project && h ( "span" , { class : "project-name" }, ` ${ project . name } ▸ ` ), h ( "span" , { class : "task-name" }, task ? . title ?? t . task_id )), liveness ( procs . find (( p ) => p . session_id === t . session_id )), h ( "span" , { class : "task-state" }, t . vendor )));
}), turns . length === 0 && h ( "li" , { class : "empty-row" }, "No turns running." ));
2026-10-14 21:06:30 -07:00
}
2026-10-14 22:35:53 -07:00
function taskLink ( taskId ) {
if ( ! taskId ) {
2026-10-15 09:40:30 -07:00
return "lead" ;
2026-10-14 22:35:53 -07:00
}
const task = findTask ( taskId );
const project = task ? state . projects . find (( p ) => p . id === task . project_id ) : undefined ;
2026-10-15 13:30:09 -07:00
return h ( "a" , { href : `#/task/ ${ taskId } ` , class : "task-link" , style : taskStyle ( taskId ) }, project ? ` ${ project . name } ▸ ` : "" , task ? . title ?? taskId );
2026-10-14 22:35:53 -07:00
}
function since ( iso ) {
const seconds = Math . max ( 0 , Math . round (( Date . now () - Date . parse ( iso )) / 1000 ));
if ( seconds < 60 ) {
return ` ${ seconds } s ago` ;
}
if ( seconds < 3600 ) {
return ` ${ Math . floor ( seconds / 60 ) } m ${ seconds % 60 } s ago` ;
}
return ago ( iso );
}
function liveness ( p ) {
if ( ! p ) {
return false ;
}
if ( p . health === "ok" ) {
return h ( "span" , { class : "task-state" }, since ( p . last_event_at ));
}
return h ( "span" , { class : "task-state danger" }, ` ${ p . health } · ${ since ( p . last_event_at ) } ` );
}
const zombieLabel = {
running_without_process : "Running with no process" ,
process_without_session : "Process with no session" ,
};
function problemsSection ( s , openVendors ) {
const pe = s . protocol_errors ;
const vendors = Object . keys ( pe . counts ). sort ();
if ( s . zombies . length === 0 && vendors . length === 0 ) {
return false ;
}
2026-10-15 09:40:30 -07:00
return h ( "section" , { class : "status-section" }, h ( "h2" , {}, "Problems" ), h ( "ul" , { class : "problems" }, ... s . zombies . map (( z ) => h ( "li" , {}, h ( "span" , { class : "danger" }, zombieLabel [ z . kind ]), " " , z . task_id ? taskLink ( z . task_id ) : "" , " " , h ( "span" , { class : "hint" }, z . detail ))), ... vendors . map (( v ) => {
2026-10-14 22:35:53 -07:00
const latest = pe . recent . find (( e ) => e . vendor === v );
const n = pe . counts [ v ] ?? 0 ;
return h ( "li" , {}, h ( "span" , { class : "danger" }, ` ${ v } : ${ n } bad stream line ${ n === 1 ? "" : "s" } ` ), latest &&
h ( "details" , { data : { vendor : v }, open : openVendors . has ( v ) }, h ( "summary" , {}, `latest ${ since ( latest . at ) } : ${ latest . reason } ` ), h ( "pre" , { class : "raw-line" }, latest . raw , new TextEncoder (). encode ( latest . raw ). length < latest . size ? `… ( ${ bytes ( latest . size ) } )` : "" )));
})));
}
2026-10-14 21:06:30 -07:00
function rateLimitsSection ( limits ) {
const vendors = Object . keys ( limits ). sort ();
2026-10-15 09:40:30 -07:00
return h ( "section" , { class : "status-section" }, h ( "h2" , {}, "Rate limits" ), ... vendors . map (( v ) => rateLimitCard ( v , limits [ v ] ?? {})), vendors . length === 0 && h ( "p" , { class : "hint" }, "No vendor has reported a rate-limit status since byre started." ));
2026-10-14 21:06:30 -07:00
}
function rateLimitCard ( vendor , info ) {
const { status , rateLimitType , resetsAt , unifiedWindows , ... rest } = info ;
const statusText = String ( status ?? "unknown" );
const tone = statusText === "allowed" ? "ok" : statusText === "rejected" ? "danger" : "warn" ;
const windows = ( unifiedWindows ?? {});
return h ( "div" , { class : "card rate-limit" }, h ( "header" , {}, h ( "span" , { class : "vendor" }, vendor ), h ( "span" , { class : `rl-status ${ tone } ` }, statusText ), rateLimitType !== undefined && h ( "span" , {}, String ( rateLimitType )), typeof resetsAt === "number" && h ( "span" , { class : "resets" }, "resets " , resetText ( resetsAt ))), Object . keys ( windows ). length > 0 &&
h ( "table" , { class : "rl-windows" }, h ( "tr" , {}, h ( "th" , {}, "Window" ), h ( "th" , {}, "Used" ), h ( "th" , {}, "Resets" )), ... Object . entries ( windows ). map (([ name , w ]) => h ( "tr" , {}, h ( "td" , {}, name ), h ( "td" , { class : utilizationClass ( w . utilization ) }, w . utilization === undefined ? "—" : ` ${ Math . round ( w . utilization * 100 ) } %` ), h ( "td" , {}, w . resetsAt === undefined ? "—" : resetText ( w . resetsAt ))))), Object . keys ( rest ). length > 0 &&
h ( "dl" , { class : "kv" }, ... Object . entries ( rest ). flatMap (([ k , v ]) => [ h ( "dt" , {}, k ), h ( "dd" , {}, typeof v === "object" ? JSON . stringify ( v ) : String ( v ))])));
}
function utilizationClass ( u ) {
if ( u === undefined ) {
return "" ;
}
if ( u >= 0.9 ) {
return "danger" ;
}
if ( u >= 0.7 ) {
return "warn" ;
}
return "" ;
}
function resetText ( unixSeconds ) {
const at = new Date ( unixSeconds * 1000 );
const time = at . toLocaleString ( undefined , { weekday : "short" , hour : "numeric" , minute : "2-digit" });
const minutes = Math . round (( at . getTime () - Date . now ()) / 60000 );
if ( minutes <= 0 ) {
return ` ${ time } (passed)` ;
}
const rel = minutes < 60 ? ` ${ minutes } m` : minutes < 1440 ? ` ${ Math . floor ( minutes / 60 ) } h ${ minutes % 60 } m` : ` ${ Math . floor ( minutes / 1440 ) } d ${ Math . floor (( minutes % 1440 ) / 60 ) } h` ;
return ` ${ time } (in ${ rel } )` ;
}
function serverSection ( s ) {
const rows = [
[ "Started" , ` ${ new Date ( s . started_at ). toLocaleString () } ( ${ ago ( s . started_at ) } )` ],
[ "Agent processes" , String ( s . live_agents )],
[ "Projects" , String ( s . projects )],
[ "Active tasks" , String ( s . active_tasks )],
[ "Live clients" , String ( s . sse_clients )],
[ "Database" , bytes ( s . db_bytes )],
[ "Last event" , `# ${ s . last_event_seq } ` ],
];
2026-10-15 09:40:30 -07:00
return h ( "section" , { class : "status-section" }, h ( "h2" , {}, "Server" ), h ( "dl" , { class : "kv" }, ... rows . flatMap (([ k , v ]) => [ h ( "dt" , {}, k ), h ( "dd" , {}, v )])));
2026-10-14 21:06:30 -07:00
}
function bytes ( n ) {
const units = [ "B" , "KB" , "MB" , "GB" , "TB" ];
let i = 0 ;
let v = n ;
while ( v >= 1024 && i < units . length - 1 ) {
v /= 1024 ;
i ++ ;
}
return ` ${ v . toFixed ( i === 0 ? 0 : 1 ) } ${ units [ i ] } ` ;
}
2026-10-15 09:40:30 -07:00
//# sourceMappingURL=status.js.map