@@ -69,6 +69,9 @@ export default function RuntimeManager() {
6969 const [ resourceTelemetry , setResourceTelemetry ] = useState < ResourceTelemetry | null > ( null ) ;
7070 const [ resourceSettings , setResourceSettings ] = useState < { mode : "balanced" | "performance" | "efficient" | "manual" ; maxRamMB ?: number ; maxVramMB ?: number ; cpuThreadCeiling ?: number } > ( { mode : "balanced" } ) ;
7171 const [ resourceSettingsSaving , setResourceSettingsSaving ] = useState ( false ) ;
72+ const [ fleetFingerprint , setFleetFingerprint ] = useState < string | null > ( null ) ;
73+ const [ fleetStatus , setFleetStatus ] = useState < { enabled : boolean ; nodeId : string | null ; running : boolean } | null > ( null ) ;
74+ const [ fleetSaving , setFleetSaving ] = useState ( false ) ;
7275 const [ modelAssessments , setModelAssessments ] = useState < GgufAssessment [ ] > ( [ ] ) ;
7376 const [ loadedModelPaths , setLoadedModelPaths ] = useState < Set < string > > ( new Set ( ) ) ;
7477 const [ modelAssessmentsLoading , setModelAssessmentsLoading ] = useState ( false ) ;
@@ -107,6 +110,27 @@ export default function RuntimeManager() {
107110 setResourceSettingsSaving ( false ) ;
108111 }
109112 } , [ hasApi , tr , toast ] ) ;
113+ const refreshFleet = useCallback ( async ( ) => {
114+ if ( ! hasApi || ! window . api . computeAgent ) return ;
115+ try {
116+ const [ identity , status ] = await Promise . all ( [ window . api . computeAgent . getIdentity ( ) , window . api . computeAgent . getStatus ( ) ] ) ;
117+ setFleetFingerprint ( identity . fingerprint256 ) ;
118+ setFleetStatus ( status ) ;
119+ } catch { /* fleet enrollment is opt-in and best-effort to surface */ }
120+ } , [ hasApi ] ) ;
121+ const saveFleetSettings = useCallback ( async ( next : { enabled : boolean ; nodeId : string } ) => {
122+ if ( ! hasApi ) return ;
123+ setFleetSaving ( true ) ;
124+ try {
125+ await window . api . settings . save ( { computeAgentEnabled : next . enabled , computeNodeId : next . nodeId . trim ( ) || undefined } ) ;
126+ await refreshFleet ( ) ;
127+ toast . success ( tr ? "Filo ayarları kaydedildi." : "Fleet settings saved." ) ;
128+ } catch ( reason ) {
129+ toast . error ( ( reason as Error ) . message ) ;
130+ } finally {
131+ setFleetSaving ( false ) ;
132+ }
133+ } , [ hasApi , refreshFleet , tr , toast ] ) ;
110134 const refreshGpuTelemetry = useCallback ( async ( ) => { if ( ! hasApi || ! window . api . gpu ) return ; try { setGpuTelemetry ( await window . api . gpu . getTelemetry ( ) ) ; } catch { /* telemetry is best-effort */ } } , [ hasApi ] ) ;
111135 const refreshResourceTelemetry = useCallback ( async ( ) => { if ( ! hasApi || ! window . api . resource ) return ; try { setResourceTelemetry ( await window . api . resource . getTelemetry ( ) ) ; } catch { /* telemetry is best-effort */ } } , [ hasApi ] ) ;
112136 // Item 6/7's "Models" panel: per-installed-model compatibility, reusing
@@ -145,7 +169,7 @@ export default function RuntimeManager() {
145169 }
146170 } , [ hasApi , refreshGpuTelemetry , toast , tr ] ) ;
147171
148- useEffect ( ( ) => { const timer = window . setTimeout ( ( ) => { void refreshStatuses ( ) ; void refreshEnvironments ( ) ; void refreshModels ( ) ; void refreshGpuTelemetry ( ) ; } , 0 ) ; return ( ) => clearTimeout ( timer ) ; } , [ refreshStatuses , refreshEnvironments , refreshModels , refreshGpuTelemetry ] ) ;
172+ useEffect ( ( ) => { const timer = window . setTimeout ( ( ) => { void refreshStatuses ( ) ; void refreshEnvironments ( ) ; void refreshModels ( ) ; void refreshGpuTelemetry ( ) ; void refreshFleet ( ) ; } , 0 ) ; return ( ) => clearTimeout ( timer ) ; } , [ refreshStatuses , refreshEnvironments , refreshModels , refreshGpuTelemetry , refreshFleet ] ) ;
149173 useEffect ( ( ) => { const timer = window . setInterval ( ( ) => void refreshStatuses ( ) , 5_000 ) ; const visible = ( ) => { if ( document . visibilityState === "visible" ) void refreshStatuses ( ) ; } ; document . addEventListener ( "visibilitychange" , visible ) ; return ( ) => { clearInterval ( timer ) ; document . removeEventListener ( "visibilitychange" , visible ) ; } ; } , [ refreshStatuses ] ) ;
150174 // The main process already caches/dedupes/pauses the underlying nvidia-smi
151175 // / rocm-smi probes (gpu-telemetry.ts) — this just re-reads that shared
@@ -221,7 +245,7 @@ export default function RuntimeManager() {
221245
222246 return < main className = "min-h-full bg-background p-4 md:p-6" > < div className = "mx-auto max-w-7xl space-y-5" >
223247 < header className = "flex flex-wrap items-center justify-between gap-3" > < div className = "flex items-center gap-3" > < Button variant = "ghost" size = "icon" onClick = { ( ) => navigate ( - 1 ) } aria-label = { tr ? "Geri" : "Back" } > < ArrowLeft className = "size-4" /> </ Button > < div > < h1 className = "text-2xl font-semibold tracking-tight" > { tr ? "Çalışma Zamanı Yöneticisi" : "Runtime Manager" } </ h1 > < p className = "text-sm text-muted-foreground" > { tr ? "Yerel çıkarım süreçleri, ortamlar ve tanılama" : "Local inference processes, environments, and diagnostics" } </ p > </ div > </ div > < Button variant = "outline" disabled = { hardwareRefreshing } onClick = { ( ) => void Promise . all ( [ refreshStatuses ( ) , refreshEnvironments ( ) , refreshModels ( ) , refreshGpuTopology ( ) ] ) } > < RefreshCw className = { `mr-2 size-4 ${ environmentLoading || hardwareRefreshing ? "animate-spin motion-reduce:animate-none" : "" } ` } /> { tr ? "Yenile" : "Refresh" } </ Button > </ header >
224- < Tabs value = { tab } onValueChange = { ( value ) => setTab ( String ( value ) ) } > < TabsList variant = "line" className = "max-w-full overflow-x-auto" > < TabsTrigger value = "overview" > { tr ? "Genel Bakış" : "Overview" } </ TabsTrigger > < TabsTrigger value = "runtimes" > { tr ? "Çalışma Zamanları" : "Runtimes" } </ TabsTrigger > < TabsTrigger value = "workloads" > { tr ? "İş yükleri" : "Workloads" } </ TabsTrigger > < TabsTrigger value = "models" > { tr ? "Modeller" : "Models" } </ TabsTrigger > < TabsTrigger value = "environments" > { tr ? "Ortamlar" : "Environments" } </ TabsTrigger > < TabsTrigger value = "resource-settings" > { tr ? "Kaynak ayarları" : "Resource settings" } </ TabsTrigger > < TabsTrigger value = "logs" > { tr ? "Günlükler ve tanılama" : "Logs & diagnostics" } </ TabsTrigger > </ TabsList >
248+ < Tabs value = { tab } onValueChange = { ( value ) => setTab ( String ( value ) ) } > < TabsList variant = "line" className = "max-w-full overflow-x-auto" > < TabsTrigger value = "overview" > { tr ? "Genel Bakış" : "Overview" } </ TabsTrigger > < TabsTrigger value = "runtimes" > { tr ? "Çalışma Zamanları" : "Runtimes" } </ TabsTrigger > < TabsTrigger value = "workloads" > { tr ? "İş yükleri" : "Workloads" } </ TabsTrigger > < TabsTrigger value = "models" > { tr ? "Modeller" : "Models" } </ TabsTrigger > < TabsTrigger value = "environments" > { tr ? "Ortamlar" : "Environments" } </ TabsTrigger > < TabsTrigger value = "resource-settings" > { tr ? "Kaynak ayarları" : "Resource settings" } </ TabsTrigger > < TabsTrigger value = "fleet" > { tr ? "Filo" : "Fleet" } </ TabsTrigger > < TabsTrigger value = " logs"> { tr ? "Günlükler ve tanılama" : "Logs & diagnostics" } </ TabsTrigger > </ TabsList >
225249 < TabsContent value = "overview" className = "space-y-5 pt-4" >
226250 < section className = "grid overflow-hidden rounded-xl border bg-card sm:grid-cols-2 lg:grid-cols-4" > < Metric label = { tr ? "Çalışan" : "Running" } value = { `${ running . length } / ${ statuses . length } ` } detail = { unhealthy . length ? `${ unhealthy . length } ${ tr ? "sağlıksız" : "unhealthy" } ` : tr ? "Tümü sağlıklı" : "All healthy" } /> < Metric label = { tr ? "Etkin istekler" : "Active requests" } value = { String ( activeRequests ) } detail = { running . map ( ( item ) => item . model ) . filter ( Boolean ) . join ( ", " ) || "—" } /> < Metric label = "RAM / VRAM" value = { `${ memory ( totalRam ) } / ${ memory ( totalVram ) } ` } detail = { accelerator } /> < Metric label = { tr ? "İşlem gerekli" : "Needs attention" } value = { String ( repairCount ) } detail = { `${ tr ? "Son denetim" : "Last check" } : ${ fmtTime ( statuses . map ( ( item ) => item . lastHealthCheckAt ) . filter ( Boolean ) . sort ( ) . at ( - 1 ) ?? null ) } ` } /> </ section >
227251 < section className = "rounded-xl border bg-card" > < div className = "border-b px-4 py-3" > < h2 className = "font-medium" > { tr ? "Çalışma zamanı özeti" : "Runtime summary" } </ h2 > </ div > < div className = "divide-y" > { statuses . map ( ( status ) => < button key = { status . backend } className = "grid w-full gap-2 px-4 py-3 text-left hover:bg-muted/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring sm:grid-cols-[1.2fr_.8fr_1fr_auto] sm:items-center" onClick = { ( ) => { setTab ( "runtimes" ) ; } } > < span > < span className = "font-medium" > { RUNTIMES [ status . backend ] . name } </ span > < span className = "block text-xs text-muted-foreground" > { status . model || RUNTIMES [ status . backend ] . purpose } </ span > </ span > < StatusBadge state = { displayState ( status ) } /> < span className = "text-xs text-muted-foreground" > { status . activeRequests } { tr ? "istek" : "requests" } · { memory ( status . ramMB ) } RAM</ span > < span className = "text-xs text-muted-foreground" > { fmtTime ( status . lastHealthCheckAt ) } </ span > </ button > ) } </ div > </ section >
@@ -232,6 +256,7 @@ export default function RuntimeManager() {
232256 < TabsContent value = "models" className = "space-y-4 pt-4" > < ModelsPanel assessments = { modelAssessments } loadedPaths = { loadedModelPaths } loading = { modelAssessmentsLoading } tr = { tr } /> </ TabsContent >
233257 < TabsContent value = "environments" className = "space-y-3 pt-4" > < div className = "rounded-xl border bg-card" > < div className = "border-b px-4 py-3" > < h2 className = "font-medium" > { tr ? "Yönetilen Python ortamları" : "Managed Python environments" } </ h2 > < p className = "text-xs text-muted-foreground" > { tr ? "Hiçbir kurulum komutu açık onay olmadan çalıştırılmaz." : "No installation command runs without explicit approval." } </ p > </ div > < div className = "divide-y" > { environments . map ( ( environment ) => < EnvironmentRow key = { environment . family } environment = { environment } tr = { tr } onPlan = { ( ) => setConfirmation ( { kind : "environment" , environment } ) } /> ) } </ div > </ div > </ TabsContent >
234258 < TabsContent value = "resource-settings" className = "space-y-4 pt-4" > < ResourceSettingsPanel settings = { resourceSettings } saving = { resourceSettingsSaving } onSave = { ( next ) => void saveResourceSettings ( next ) } tr = { tr } /> </ TabsContent >
259+ < TabsContent value = "fleet" className = "space-y-4 pt-4" > < FleetAgentPanel fingerprint = { fleetFingerprint } status = { fleetStatus } saving = { fleetSaving } onSave = { ( next ) => void saveFleetSettings ( next ) } tr = { tr } /> </ TabsContent >
235260 < TabsContent value = "logs" className = "space-y-3 pt-4" > < section className = "overflow-hidden rounded-xl border bg-card" > < div className = "flex flex-wrap items-center gap-2 border-b p-3" > < label className = "sr-only" htmlFor = "runtime-log-selector" > Runtime</ label > < select id = "runtime-log-selector" className = "h-8 rounded-lg border bg-background px-2 text-sm" value = { logBackend } onChange = { ( event ) => setLogBackend ( event . target . value as Backend ) } > { Object . values ( RUNTIMES ) . map ( ( runtime ) => < option key = { runtime . id } value = { runtime . id } > { runtime . name } </ option > ) } </ select > < select aria-label = { tr ? "Günlük kaynağı" : "Log source" } className = "h-8 rounded-lg border bg-background px-2 text-sm" value = { logSource } onChange = { ( event ) => setLogSource ( event . target . value ) } > < option value = "all" > { tr ? "Tüm kaynaklar" : "All sources" } </ option > < option value = "manager" > Manager</ option > < option value = "stdout" > stdout</ option > < option value = "stderr" > stderr</ option > </ select > < div className = "relative min-w-48 flex-1" > < Search className = "absolute left-2.5 top-2 size-4 text-muted-foreground" /> < Input className = "h-8 pl-8" value = { logSearch } onChange = { ( event ) => setLogSearch ( event . target . value ) } placeholder = { tr ? "Günlüklerde ara" : "Search logs" } /> </ div > < label className = "flex items-center gap-2 text-xs" > < input type = "checkbox" checked = { follow } onChange = { ( event ) => setFollow ( event . target . checked ) } /> { tr ? "Çıktıyı izle" : "Follow output" } </ label > < Button size = "sm" variant = "outline" onClick = { ( ) => { void navigator . clipboard . writeText ( visibleLogs . join ( "\n" ) ) ; toast . success ( tr ? "Günlükler kopyalandı." : "Logs copied." ) ; } } > < Clipboard className = "mr-1.5 size-3.5" /> { tr ? "Kopyala" : "Copy" } </ Button > < Button size = "sm" variant = "outline" onClick = { ( ) => window . api . localBackends . exportLogs ( logBackend ) } > < Download className = "mr-1.5 size-3.5" /> { tr ? "Dışa aktar" : "Export" } </ Button > < Button size = "sm" variant = "ghost" onClick = { ( ) => setConfirmation ( { kind : "clear-logs" , backend : logBackend } ) } > < Trash2 className = "mr-1.5 size-3.5" /> { tr ? "Temizle" : "Clear" } </ Button > </ div > < div className = "h-[min(60vh,34rem)] overflow-auto bg-zinc-950 p-4 font-mono text-xs leading-relaxed text-zinc-200" role = "log" aria-live = "polite" > { visibleLogs . length ? visibleLogs . map ( ( line , index ) => < div key = { `${ index } -${ line . slice ( 0 , 30 ) } ` } className = { line . includes ( "[stderr]" ) ? "text-amber-300" : "" } > { line } </ div > ) : < div className = "flex h-full items-center justify-center text-zinc-500" > { tr ? "Bu filtre için günlük yok." : "No logs match this filter." } </ div > } < div ref = { logEnd } /> </ div > </ section > </ TabsContent >
236261 </ Tabs >
237262 { ! hasApi && < div className = "rounded-xl border border-dashed p-8 text-center text-muted-foreground" > { tr ? "Çalışma zamanı yönetimi masaüstü uygulamasında kullanılabilir." : "Runtime management is available in the desktop application." } </ div > }
@@ -428,6 +453,51 @@ function ResourceSettingsPanel({ settings, saving, onSave, tr }: { settings: Res
428453 </ section > ;
429454}
430455
456+ // Requires an already-connected shared backend (Audit & Privacy's own
457+ // enrollment panel) — enabling this without one configured saves cleanly
458+ // (compute-agent.ts's own runCycle() just no-ops each cycle until then)
459+ // but never actually starts heartbeating, so the copy below says so up
460+ // front rather than let a user wonder why nothing happens.
461+ function FleetAgentPanel ( { fingerprint, status, saving, onSave, tr } : { fingerprint : string | null ; status : { enabled : boolean ; nodeId : string | null ; running : boolean } | null ; saving : boolean ; onSave ( next : { enabled : boolean ; nodeId : string } ) : void ; tr : boolean } ) {
462+ const [ enabled , setEnabled ] = useState ( status ?. enabled ?? false ) ;
463+ const [ nodeId , setNodeId ] = useState ( status ?. nodeId ?? "" ) ;
464+ useEffect ( ( ) => { setEnabled ( status ?. enabled ?? false ) ; setNodeId ( status ?. nodeId ?? "" ) ; } , [ status ] ) ;
465+ const dirty = enabled !== ( status ?. enabled ?? false ) || nodeId !== ( status ?. nodeId ?? "" ) ;
466+ const [ copied , setCopied ] = useState ( false ) ;
467+
468+ return < section className = "max-w-2xl space-y-4 overflow-hidden rounded-xl border bg-card p-4" >
469+ < div >
470+ < h2 className = "font-medium" > { tr ? "Kurumsal işlem filosu" : "Enterprise compute fleet" } </ h2 >
471+ < p className = "mt-1 text-xs text-muted-foreground" > { tr ? "Bu cihazı, kurumun paylaşılan arka ucuna zaten bağlıysa (Denetim ve Gizlilik) bir işlem havuzuna kaydedin. Yalnızca havuzun taahhüt ettiği kapasiteyi korur — burada hiçbir iş çalıştırılmaz." : "Enroll this device into a compute pool, if it's already connected to your organization's shared backend (Audit & Privacy). This only protects the capacity the fleet has committed — no work is dispatched to run here yet." } </ p >
472+ </ div >
473+ < div className = "space-y-1.5" >
474+ < p className = "text-xs font-medium text-muted-foreground" > { tr ? "Cihaz parmak izi" : "Device fingerprint" } </ p >
475+ < div className = "flex items-center gap-2" >
476+ < code className = "min-w-0 flex-1 overflow-x-auto whitespace-nowrap rounded-lg border bg-background px-3 py-2 font-mono text-[11px]" > { fingerprint ?? "…" } </ code >
477+ < Button variant = "outline" size = "sm" disabled = { ! fingerprint } onClick = { ( ) => { if ( fingerprint ) { void navigator . clipboard . writeText ( fingerprint ) ; setCopied ( true ) ; window . setTimeout ( ( ) => setCopied ( false ) , 2000 ) ; } } } >
478+ < Clipboard className = "mr-1.5 size-3.5" /> { copied ? ( tr ? "Kopyalandı" : "Copied" ) : ( tr ? "Kopyala" : "Copy" ) }
479+ </ Button >
480+ </ div >
481+ < p className = "text-[11px] text-muted-foreground" > { tr ? "Bu cihazı bir işlem havuzuna eklemesi için kuruluşunuzun işlem yöneticisine verin." : "Give this to your organization's compute admin to register this device to a pool." } </ p >
482+ </ div >
483+ < div className = "grid gap-3 sm:grid-cols-[1fr_auto]" >
484+ < label className = "flex flex-col gap-1 text-xs text-muted-foreground" >
485+ { tr ? "Düğüm kimliği (yönetici tarafından verilir)" : "Node ID (given by your admin)" }
486+ < Input value = { nodeId } onChange = { ( event ) => setNodeId ( event . target . value ) } placeholder = { tr ? "kayıttan sonra buraya yapıştırın" : "paste it here after registration" } />
487+ </ label >
488+ < label className = "flex items-end gap-2 pb-0.5 text-sm" >
489+ < input type = "checkbox" checked = { enabled } onChange = { ( event ) => setEnabled ( event . target . checked ) } />
490+ { tr ? "Filo aracısını etkinleştir" : "Enable fleet agent" }
491+ </ label >
492+ </ div >
493+ { status && < p className = "text-[11px] text-muted-foreground" > { status . running ? ( tr ? "Aracı çalışıyor ve düzenli olarak nabız gönderiyor." : "The agent is running and heartbeating regularly." ) : status . enabled ? ( tr ? "Etkin ancak henüz çalışmıyor — bir düğüm kimliği gerekiyor olabilir." : "Enabled but not running yet — a node ID may still be needed." ) : ( tr ? "Şu anda devre dışı." : "Currently disabled." ) } </ p > }
494+ < div className = "flex justify-end gap-2 border-t pt-3" >
495+ < Button variant = "outline" disabled = { ! dirty || saving } onClick = { ( ) => { setEnabled ( status ?. enabled ?? false ) ; setNodeId ( status ?. nodeId ?? "" ) ; } } > { tr ? "Sıfırla" : "Reset" } </ Button >
496+ < Button disabled = { ! dirty || saving } onClick = { ( ) => onSave ( { enabled, nodeId } ) } > { saving ? ( tr ? "Kaydediliyor…" : "Saving…" ) : ( tr ? "Kaydet" : "Save" ) } </ Button >
497+ </ div >
498+ </ section > ;
499+ }
500+
431501function RuntimeCard ( { status, descriptor, model, setModel, config, setConfig, ggufModels, recentModels, tensorParallelAvailable, gpus, operation, error, tr, onAction, onLogs } : { status : LocalRuntimeStatus ; descriptor : RuntimeDescriptor ; model : string ; setModel ( value : string ) : void ; config : RuntimeStartupConfig ; setConfig ( value : RuntimeStartupConfig ) : void ; ggufModels : LocalGgufModel [ ] ; recentModels : string [ ] ; tensorParallelAvailable : boolean ; gpus : GpuInfo [ ] ; operation ?: string ; error ?: string ; tr : boolean ; onAction ( backend : Backend , action : "start" | "stop" | "restart" ) : void ; onLogs ( ) : void } ) {
432502 const state = displayState ( status ) ; const busy = ! ! operation || ! ! status . operation ; const primary = ! status . compatible || status . startupError ? "error" : ! status . installed ? "install" : status . state === "running" || status . state === "unhealthy" ? "stop" : "start" ;
433503 const candidates = selectableGpus ( gpus , status . backend ) ;
0 commit comments