mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Add real-time system stats (CPU/RAM/GPU/VRAM) and fix favicon
Favicon now matches the LokiMark topbar icon instead of an unrelated arrow glyph. New /api/system/stats endpoint (psutil + nvidia-smi) polled every 2s and shown in the topbar next to the Ollama status.
This commit is contained in:
1 parent
b9bccac4ba
commit
ecadf55743
8 files changed
+120
-7
No files matched your search
+2
-1
@@ -14,7 +14,7 @@ from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from . import db
|
||||
from .config import settings
|
||||
from .routes import chat, config, files, models, sessions, shell
|
||||
from .routes import chat, config, files, models, sessions, shell, system
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -42,6 +42,7 @@ app.include_router(chat.router)
|
||||
app.include_router(files.router)
|
||||
app.include_router(config.router)
|
||||
app.include_router(shell.router)
|
||||
app.include_router(system.router)
|
||||
|
||||
|
||||
@app.get("/api/health")
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Statistiques système temps réel : CPU, RAM, GPU/VRAM (barre supérieure)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import shutil
|
||||
|
||||
import psutil
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter(prefix="/api/system", tags=["system"])
|
||||
|
||||
_NVIDIA_SMI = shutil.which("nvidia-smi")
|
||||
|
||||
|
||||
async def _gpu_stats() -> dict | None:
|
||||
"""Utilisation GPU/VRAM via nvidia-smi ; None si absent (pas de GPU NVIDIA)."""
|
||||
if not _NVIDIA_SMI:
|
||||
return None
|
||||
try:
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
_NVIDIA_SMI,
|
||||
"--query-gpu=utilization.gpu,memory.used,memory.total,name",
|
||||
"--format=csv,noheader,nounits",
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
out, _ = await asyncio.wait_for(proc.communicate(), timeout=3)
|
||||
line = out.decode().strip().splitlines()[0]
|
||||
util, used, total, name = (p.strip() for p in line.split(","))
|
||||
return {
|
||||
"name": name,
|
||||
"util_pct": float(util),
|
||||
"vram_used_mb": float(used),
|
||||
"vram_total_mb": float(total),
|
||||
}
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
async def stats() -> dict:
|
||||
"""CPU %, RAM et GPU/VRAM courants."""
|
||||
cpu_pct = psutil.cpu_percent(interval=None)
|
||||
mem = psutil.virtual_memory()
|
||||
gpu = await _gpu_stats()
|
||||
return {
|
||||
"cpu_pct": cpu_pct,
|
||||
"ram_used_go": round(mem.used / 1_000_000_000, 1),
|
||||
"ram_total_go": round(mem.total / 1_000_000_000, 1),
|
||||
"ram_pct": mem.percent,
|
||||
"gpu": gpu,
|
||||
}
|
||||
@@ -3,3 +3,4 @@ uvicorn[standard]==0.34.0
|
||||
httpx==0.28.1
|
||||
pydantic==2.10.4
|
||||
pydantic-settings==2.7.1
|
||||
psutil==6.1.1
|
||||
@@ -1,6 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<rect width="64" height="64" rx="10" fill="#18181b"/>
|
||||
<rect x="14" y="14" width="38" height="38" rx="7" fill="#ff5436"/>
|
||||
<rect x="11" y="11" width="38" height="38" rx="7" fill="#ff5436" stroke="#ffffff" stroke-width="6"/>
|
||||
<rect x="25" y="25" width="12" height="12" fill="#ffffff"/>
|
||||
<path d="M18 54h38V16" fill="none" stroke="#ff5436" stroke-width="5" stroke-linecap="square"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 386 B After Width: | Height: | Size: 358 B |
+11
-5
@@ -10,16 +10,22 @@ import { useStore } from "./store/useStore";
|
||||
|
||||
export default function App() {
|
||||
const [view, setView] = useState<View>("chat");
|
||||
const { refreshStatus, refreshModels, refreshConfig } = useStore();
|
||||
const { refreshStatus, refreshSystemStats, refreshModels, refreshConfig } =
|
||||
useStore();
|
||||
|
||||
// Au démarrage : statut Ollama + modèles + config. Poll du statut.
|
||||
// Au démarrage : statut Ollama + modèles + config. Poll du statut et des stats système.
|
||||
useEffect(() => {
|
||||
refreshStatus();
|
||||
refreshSystemStats();
|
||||
refreshModels();
|
||||
refreshConfig();
|
||||
const id = setInterval(refreshStatus, 10000);
|
||||
return () => clearInterval(id);
|
||||
}, [refreshStatus, refreshModels, refreshConfig]);
|
||||
const statusId = setInterval(refreshStatus, 10000);
|
||||
const statsId = setInterval(refreshSystemStats, 2000);
|
||||
return () => {
|
||||
clearInterval(statusId);
|
||||
clearInterval(statsId);
|
||||
};
|
||||
}, [refreshStatus, refreshSystemStats, refreshModels, refreshConfig]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-base text-ink">
|
||||
|
||||
@@ -21,6 +21,26 @@ export async function getStatus(): Promise<OllamaStatus> {
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export interface GpuStats {
|
||||
name: string;
|
||||
util_pct: number;
|
||||
vram_used_mb: number;
|
||||
vram_total_mb: number;
|
||||
}
|
||||
|
||||
export interface SystemStats {
|
||||
cpu_pct: number;
|
||||
ram_used_go: number;
|
||||
ram_total_go: number;
|
||||
ram_pct: number;
|
||||
gpu: GpuStats | null;
|
||||
}
|
||||
|
||||
export async function getSystemStats(): Promise<SystemStats> {
|
||||
const res = await fetch("/api/system/stats");
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function getModels(): Promise<{
|
||||
models: OllamaModel[];
|
||||
default: string;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ModelSelector } from "./ModelSelector";
|
||||
/** Barre supérieure sombre : logo, fil d'Ariane, statut Ollama, sélecteur. */
|
||||
export function TopBar() {
|
||||
const status = useStore((s) => s.status);
|
||||
const stats = useStore((s) => s.systemStats);
|
||||
const connected = status?.connected ?? false;
|
||||
|
||||
return (
|
||||
@@ -24,6 +25,25 @@ export function TopBar() {
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* Stats système temps réel : CPU, RAM, GPU/VRAM */}
|
||||
{stats && (
|
||||
<div className="flex h-8 items-center gap-2.5 border-[3px] border-chrome-3 bg-chrome-2 px-[11px] text-[13px] text-on-dark">
|
||||
<span>CPU {stats.cpu_pct.toFixed(0)}%</span>
|
||||
<span className="text-on-dark-3">·</span>
|
||||
<span>RAM {stats.ram_pct.toFixed(0)}%</span>
|
||||
{stats.gpu && (
|
||||
<>
|
||||
<span className="text-on-dark-3">·</span>
|
||||
<span>GPU {stats.gpu.util_pct.toFixed(0)}%</span>
|
||||
<span className="text-on-dark-3">
|
||||
VRAM {(stats.gpu.vram_used_mb / 1024).toFixed(1)}/
|
||||
{(stats.gpu.vram_total_mb / 1024).toFixed(1)}G
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Statut Ollama */}
|
||||
<div
|
||||
className="flex h-8 items-center gap-2 border-[3px] border-chrome-3 bg-chrome-2 px-[11px]"
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
getModels,
|
||||
getSession,
|
||||
getStatus,
|
||||
getSystemStats,
|
||||
fileContent,
|
||||
listFiles,
|
||||
listSessions,
|
||||
@@ -18,11 +19,13 @@ import {
|
||||
type OllamaModel,
|
||||
type OllamaStatus,
|
||||
type Session,
|
||||
type SystemStats,
|
||||
type ToolCall,
|
||||
} from "../api/client";
|
||||
|
||||
interface LokiState {
|
||||
status: OllamaStatus | null;
|
||||
systemStats: SystemStats | null;
|
||||
models: OllamaModel[];
|
||||
selectedModel: string;
|
||||
loadingModels: boolean;
|
||||
@@ -54,6 +57,7 @@ interface LokiState {
|
||||
openPreview: (path: string) => Promise<void>;
|
||||
setSelectedModel: (name: string) => void;
|
||||
refreshStatus: () => Promise<void>;
|
||||
refreshSystemStats: () => Promise<void>;
|
||||
refreshModels: () => Promise<void>;
|
||||
refreshFiles: () => Promise<void>;
|
||||
|
||||
@@ -69,6 +73,7 @@ let activeStreamController: AbortController | null = null;
|
||||
|
||||
export const useStore = create<LokiState>((set, get) => ({
|
||||
status: null,
|
||||
systemStats: null,
|
||||
models: [],
|
||||
selectedModel: "",
|
||||
loadingModels: false,
|
||||
@@ -154,6 +159,14 @@ export const useStore = create<LokiState>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
refreshSystemStats: async () => {
|
||||
try {
|
||||
set({ systemStats: await getSystemStats() });
|
||||
} catch {
|
||||
set({ systemStats: null });
|
||||
}
|
||||
},
|
||||
|
||||
refreshModels: async () => {
|
||||
set({ loadingModels: true });
|
||||
try {
|
||||
|
||||
Reference in new issue
Block a user