2026-11-06 08:28:04 +00:00
async function fetchEvents () {
if ( stream . busy ) {
stream . again = true ;
return ;
}
const key = ` ${ windowKey () } & ${ filterQuery () } ` ;
if ( stream . key !== key ) {
stream . key = key ;
stream . events = [];
stream . entries = [];
stream . last = 0 ;
}
stream . busy = true ;
try {
const response = await fetch ( `/api/events? ${ windowQuery () } & ${ filterQuery () } &after= ${ stream . last } ` , { cache : "no-store" });
if ( response . ok ) {
const data = await response . json ();
if ( stream . key === key ) {
const seen = new Set ( stream . entries . map (( entry ) => entry . id ));
stream . events = data . events ;
stream . entries = [... stream . entries , ... data . entries . filter (( entry ) => ! seen . has ( entry . id ))]. sort (( a , b ) => a . ts - b . ts ). slice ( - STREAM_KEEP );
stream . last = data . last ;
stream . sessions = data . sessions ;
stream . error = "" ;
}
} else {
stream . error = ` ${ response . status } ${ ( await response . text ()). trim () } ` ;
}
} catch {
ui . offline = true ;
}
stream . busy = false ;
drawStream ();
if ( stream . again ) {
stream . again = false ;
fetchEvents ();
}
}
async function expand ( entry , holder , button ) {
button . disabled = true ;
const response = await fetch ( `/api/stream/ ${ entry . ref . entry } ` , { cache : "no-store" });
if ( response . ok ) {
const data = await response . json ();
entry . text = data . text ;
entry . ref . truncated = false ;
holder . textContent = data . text ;
button . remove ();
} else {
button . textContent = "Nicht mehr verfügbar" ;
}
}
function focusSub ( session , sub ) {
filter . view = "" ;
filter . saved = "" ;
filter . kinds = new Set ([ "transcript" ]);
filter . waiting = false ;
filter . session = session ;
filter . sub = sub ;
closeAgent ();
if ( ui . screen === "stream" && stream . dom ) {
refilter ();
} else {
go ( "stream" , null );
}
}
function inboxOf ( entry ) {
const id = entry . kind === "operator" ? entry . ref . task : entry . ref . id ;
return state . inbox . find (( item ) => item . kind === entry . kind && item . id === id ) || null ;
}
function kindPill ( entry ) {
if ( entry . kind === "radio" ) {
return klass ( entry . part );
}
if ( entry . kind === "transcript" ) {
return pill (( PARTS . find (([ id ]) => id === entry . part ) || [ entry . part , entry . part ])[ 1 ], PART_TONES [ entry . part ] || "neutral" );
}
return pill (( KINDS . find (([ id ]) => id === entry . kind ) || [ entry . kind , entry . kind ])[ 1 ], KIND_TONES [ entry . kind ] || "neutral" );
}
function extras ( entry ) {
const ref = entry . ref ;
if ( entry . kind === "review" ) {
return [ status ( ref . status ), ref . gate ? pill ( ref . gate === "pass" ? "Gate grün" : "Gate rot" , ref . gate === "pass" ? "ok" : "bad" ) : null ];
}
if ( entry . kind === "ask" ) {
return [ status ( ref . status ), ... ref . warnings . map (( warning ) => pill ( warning , "bad" ))];
}
if ( entry . kind === "decision" ) {
return [ status ( ref . status ), ref . choice ? pill ( ref . choice , "ok" ) : null ];
}
if ( entry . kind === "cron" ) {
return [ pill ( ref . active ? "aktiv" : "inaktiv" , ref . active ? "ok" : "neutral" )];
}
if ( entry . kind === "run" || entry . kind === "operator" || entry . kind === "register" ) {
return [ ref . status ? status ( ref . status ) : null ];
}
return [];
}
function body ( entry ) {
if ( entry . kind === "transcript" ) {
const text = h ( "pre" , {}, entry . text );
const more = entry . ref . truncated ? h ( "button" , { type : "button" , class : "btn small ghost" }, "Ganzen Text zeigen" ) : null ;
if ( more ) {
more . addEventListener ( "click" , () => expand ( entry , text , more ));
}
return [ text , more ];
}
if ( entry . kind === "ask" || entry . kind === "operator" ) {
const where = entry . kind === "ask" ? entry . ref . cwd : entry . ref . directory ;
return [ h ( "div" , { class : "command" }, h ( "code" , {}, entry . kind === "ask" ? entry . text : entry . title ), copyButton ( entry . kind === "ask" ? entry . text : entry . title )), h ( "span" , { class : "muted" }, `in ${ dash ( where ) } ` ), entry . kind === "operator" && entry . text !== "-" ? h ( "p" , { class : "text" }, entry . text ) : null ];
}
if ( entry . kind === "review" ) {
return [ entry . text ? h ( "pre" , { class : "notes" }, entry . text ) : null , h ( "div" , { class : "line-actions" }, btn ( "Diff ansehen" , "btn small soft" , () => reviewSheet ( entry . ref . id )))];
}
if ( entry . kind === "cron" ) {
const ref = entry . ref ;
const ended = ref . deleted ? [ " · beendet " , when ( ref . deleted )] : [];
return [ entry . text ? h ( "pre" , {}, entry . text ) : null , h ( "span" , { class : "muted" }, "angelegt " , when ( ref . created ), ended )];
}
if ( entry . kind === "run" ) {
return [ entry . text ? h ( "pre" , {}, entry . text ) : null ];
}
return [ entry . text ? h ( "p" , { class : "text" }, entry . text ) : null ];
}
function eventRow ( entry ) {
const item = entry . waiting ? inboxOf ( entry ) : null ;
const alarm = entry . kind === "radio" && ( entry . part === "mayday" || entry . part === "pan-pan" );
const renamed = entry . now && entry . now !== entry . agent ? h ( "span" , { title : `jetzt: ${ entry . now } ` }, person ( entry . agent )) : person ( entry . agent );
const who = entry . agent ? [ face ( entry . agent , false ), renamed ] : entry . session ? h ( "strong" , { title : entry . session }, entry . session . slice ( 0 , 8 )) : null ;
const headline = entry . kind === "transcript" || entry . kind === "ask" || entry . kind === "operator" ? null : h ( "span" , { class : "headline" }, entry . title );
return h ( "li" , { class : `entry k- ${ entry . kind }${ entry . waiting ? " waiting" : "" }${ alarm ? " alarm" : "" } ` },
h ( "div" , { class : "msg-meta" }, when ( entry . ts ), kindPill ( entry ), extras ( entry )),
h ( "div" , {},
h ( "div" , { class : "route" }, who , entry . sub ? h ( "button" , { type : "button" , class : "linkish muted" , on : { click : () => focusSub ( entry . session , entry . sub )}}, `› Subagent ${ entry . sub . slice ( 0 , 12 ) } ` ) : null , entry . kind === "ask" ? h ( "strong" , {}, entry . title ) : null , headline ),
body ( entry ),
item ? h ( "div" , { class : "line-actions" }, actionsFor ( item )) : null ));
}
function drawStream () {
if ( ! stream . dom || ui . screen !== "stream" ) {
return ;
}
const floor = live () && ui . preset > 0 ? nowSeconds () - ui . preset : - Infinity ;
const rows = [... stream . events , ... stream . entries . filter (( entry ) => entry . ts >= floor )]. sort (( a , b ) => b . ts - a . ts || ( a . id < b . id ? 1 : - 1 ));
const shown = rows . slice ( 0 , FEED_LIMIT );
stream . dom . count . textContent = ` ${ rows . length } Einträge ${ live () ? " · folgt live" : "" } ` ;
if ( stream . error ) {
stream . dom . list . replaceChildren ( emptyState ( "Stream nicht lesbar" , stream . error ));
return ;
}
stream . dom . list . replaceChildren ( rows . length
? h ( "div" , { class : "card flush" }, h ( "ol" , { class : "feed" }, shown . map ( eventRow )),
rows . length > shown . length ? h ( "p" , { class : "footnote" }, ` ${ rows . length - shown . length } ältere Einträge ausgeblendet; grenze mit Filter oder Zeitraum ein.` ) : null )
: emptyState ( stream . busy ? "Lade …" : "Keine Einträge" , activeWaiting () ? "Gerade wartet nichts auf dich." : "Im gewählten Zeitraum und Filter gibt es nichts." ));
}
function streamScreen () {
2026-11-06 22:20:06 +00:00
const search = h ( "input" , { type : "search" , placeholder : "Suchen … agent:Name art:funk seit:2h (/)" , value : filter . q , "aria-label" : "Stream durchsuchen: Wörter, agent:, art: und seit:" , on : { input : ( event ) => {
2026-11-06 08:28:04 +00:00
filter . q = event . target . value ;
2026-11-06 22:20:06 +00:00
filter . saved = "" ;
2026-11-06 08:28:04 +00:00
window . clearTimeout ( searchTimer );
searchTimer = window . setTimeout ( fetchEvents , 300 );
}}});
const count = h ( "span" , { class : "muted" });
2026-11-06 22:20:06 +00:00
const categories = h ( "div" , { class : "chips" , role : "group" , "aria-label" : "Kategorien" });
2026-11-06 08:28:04 +00:00
const kinds = h ( "div" , { class : "chips" , role : "group" , "aria-label" : "Arten" });
const parts = h ( "div" , { class : "chips" , role : "group" , "aria-label" : "Transkript-Teile" });
2026-11-06 22:20:06 +00:00
const saved = h ( "div" , { class : "chips" , role : "group" , "aria-label" : "Gespeicherte Ansichten" });
const focus = h ( "div" , { class : "chips" });
2026-11-06 08:28:04 +00:00
const list = h ( "div" , {});
2026-11-06 22:20:06 +00:00
const more = h ( "details" , { class : "menu" }, h ( "summary" , {}, "Filter ▾" ), h ( "div" , { class : "menu-box end" },
h ( "h3" , {}, "Arten" ), kinds , h ( "h3" , {}, "Transkript-Teile" ), parts , h ( "h3" , {}, "Gespeicherte Ansichten" ), saved , h ( "h3" , {}, "Eigener Zeitraum" ), customRange ()));
stream . dom = { search , count , categories , kinds , parts , saved , focus , list };
2026-11-06 08:28:04 +00:00
window . setTimeout (() => {
drawFilters ();
drawStream ();
fetchEvents ();
}, 0 );
2026-11-06 22:20:06 +00:00
return h ( "div" , {}, h ( "div" , { class : "filters" }, categories , search , more , count ), focus , list );
2026-11-06 08:28:04 +00:00
}