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:
Michael committed 2026-07-01 14:56:50 +02:00
1 parent b9bccac4ba
commit ecadf55743
8 files changed
+120 -7

No files matched your search

+1 -1
View File
@@ -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
View File
@@ -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">
+20
View File
@@ -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;
+20
View File
@@ -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]"
+13
View File
@@ -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 {