mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-18 04:36:44 +02:00
La pastille « chargement… » restait identique de longues minutes sur un gros GGUF — indiscernable d'un plantage. llama-server n'expose aucun progrès, mais charger c'est LIRE le fichier : on rapporte les octets lus par le process (/proc/<pid>/io, shards compris) à la taille du modèle et la pastille affiche « chargement 43 % » (rafraîchie toutes les 5 s, plafonnée à 99 — c'est /health qui dit prêt). Carte Moteur : bouton « vérifier la version » retiré — « mettre à jour » fait sa propre vérification et dit s'il n'y a rien de neuf. Éditeur de preset : en conteneur, l'option de moteur « Personnalisé » disparaît (rien à compiler dans l'image, la mise à jour passe par la carte Moteur) ; un BIN non reconnu retombe sur le moteur de l'image. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
232 lines
12 KiB
JavaScript
232 lines
12 KiB
JavaScript
async function loadStatus(){
|
|
const s=await jget('/api/status');
|
|
const el=document.getElementById('status-svc');
|
|
// Trois états : service coupé (err) · service actif mais modèle pas encore
|
|
// chargé (loading, llama-server renvoie 503) · modèle prêt (ok).
|
|
// Pastille COURTE et honnête. Le détail (cause exacte) va dans #model-err
|
|
// dessous : la pastille ne doit pas affirmer « incompatible » quand l'échec
|
|
// peut être tout autre chose.
|
|
let cls='err', txt='arrêté';
|
|
if(s.active && s.health){ cls='ok'; txt='prêt'; }
|
|
else if(s.load_error){ cls='err'; txt='erreur'; }
|
|
else if(s.active){
|
|
cls='loading'; txt='chargement…';
|
|
// Pourcentage estimé (octets du modèle déjà lus) : voir « ça avance »
|
|
// plutôt qu'un « chargement… » figé qui ressemble à un plantage.
|
|
if(typeof s.load_pct==='number' && s.load_pct>=0) txt='chargement '+s.load_pct+' %';
|
|
}
|
|
el.className='statuspill '+cls;
|
|
el.innerHTML='<span class="dot"></span>'+txt;
|
|
MODEL_READY = !!(s.active && s.health);
|
|
// Le bouton d'envoi suit l'état du moteur : inutile de pouvoir envoyer un
|
|
// message à un modèle qui n'est pas encore chargé (voir syncSendBtn).
|
|
STATUS_SEEN = true;
|
|
if(typeof syncSendBtn==='function') syncSendBtn();
|
|
if(s.ctx){ CTX_MAX=s.ctx; updateCtxMeter(); }
|
|
if(s.version){
|
|
document.getElementById('ver').textContent='v'+s.version;
|
|
}
|
|
// Avertissement de lancement (App Translocation macOS) : rare, mais il explique
|
|
// des symptomes tres deroutants, donc on l'affiche en permanence tant qu'il dure.
|
|
const wb=document.getElementById('app-warn');
|
|
if(wb){
|
|
if(s.warn){ wb.textContent='⚠ '+s.warn; wb.style.display=''; }
|
|
else { wb.style.display='none'; }
|
|
}
|
|
// Modèle qui ne charge pas (souvent un moteur incompatible) : message explicite
|
|
// plutôt qu'un « chargement… » perpétuel ou un crash-loop muet.
|
|
const me=document.getElementById('model-err');
|
|
if(me){
|
|
if(s.load_error){ me.textContent='⚠ '+s.load_error; me.style.display=''; }
|
|
else { me.style.display='none'; }
|
|
}
|
|
}
|
|
// Journal du moteur — section repliable comme les autres. La pastille d'état
|
|
// reste un raccourci ; le chargement du journal est déclenché par l'événement
|
|
// `toggle` du <details>, qu'on l'ouvre par la pastille ou par son chevron.
|
|
function toggleSvcLog(){
|
|
const box=document.getElementById('svc-log-box');
|
|
if(!box) return;
|
|
// openDetails déplie aussi « Réglages » : sans lui, la section s'ouvrait à
|
|
// l'intérieur d'un repli fermé — donc nulle part, du point de vue de l'écran.
|
|
if(box.open) box.open = false; else openDetails(box);
|
|
}
|
|
async function loadSvcLog(){
|
|
const el=document.getElementById('svc-log');
|
|
if(!el) return;
|
|
el.textContent='chargement du journal…';
|
|
try{
|
|
const r=await jget('/api/service/log?n=120');
|
|
el.textContent = (r && r.log && r.log.trim()) ? r.log : 'journal vide — le moteur n\'a encore rien écrit.';
|
|
el.scrollTop = el.scrollHeight;
|
|
}catch(e){ el.textContent='journal indisponible : '+e; }
|
|
}
|
|
async function checkUpdate(){
|
|
const b=document.getElementById('upd-check'), msg=document.getElementById('upd-msg');
|
|
b.disabled=true; msg.textContent='Vérification…';
|
|
try{
|
|
const r=await jget('/api/update');
|
|
// En conteneur, la mise à jour passe par l'image : on l'explique au lieu de
|
|
// proposer un remplacement de binaire impossible ici.
|
|
if(r.note){ msg.textContent=r.note; }
|
|
else if(r.error){ msg.textContent='Erreur : '+r.error; }
|
|
else if(r.available){
|
|
msg.innerHTML='Nouvelle version <b>v'+r.latest+'</b> disponible. ';
|
|
const btn=document.createElement('button'); btn.textContent='Mettre à jour'; btn.onclick=applyUpdate;
|
|
msg.appendChild(btn);
|
|
} else { msg.textContent='Loki est à jour ✓'; }
|
|
}catch(e){ msg.textContent='Erreur réseau'; }
|
|
b.disabled=false;
|
|
}
|
|
// Emplacements — affichés avec le journal du moteur : c'est le panneau qu'on
|
|
// ouvre quand on cherche à comprendre l'état de son installation.
|
|
async function showPaths(){
|
|
const el=document.getElementById('paths-msg');
|
|
if(!el) return;
|
|
el.textContent='…';
|
|
try{
|
|
const p=await jget('/api/paths');
|
|
const rows=[['Données',p.home],['Base (config, préférences, conversation)',p.database],['Modèles',p.models],['Presets',p.presets],['Mémoire',p.memory],['Fichiers créés par l\'IA',p.workspace],['Moteur llama.cpp',p.backends],['Programme',p.exe]];
|
|
el.innerHTML=rows.map(r=>'<div style="margin-bottom:4px">'+r[0]+'<br><code style="word-break:break-all">'+escHtml(r[1]||'')+'</code></div>').join('');
|
|
}catch(e){ el.textContent='Erreur'; }
|
|
}
|
|
async function applyUpdate(){
|
|
const msg=document.getElementById('upd-msg');
|
|
msg.textContent='Téléchargement et installation…';
|
|
try{
|
|
// Signal dédié : le timeout par défaut (30 s) coupe le téléchargement du
|
|
// binaire sur une connexion lente et fait croire à un échec alors que la
|
|
// mise à jour aboutit côté serveur.
|
|
const ac=new AbortController(); const t=setTimeout(()=>ac.abort(), 10*60*1000);
|
|
let r;
|
|
try{ r=await (await jfetch('/api/update/apply',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}',signal:ac.signal})).json(); }
|
|
finally{ clearTimeout(t); }
|
|
if(r.ok){
|
|
msg.innerHTML='✓ Installé en <b>v'+r.version+'</b>.<br>'+(r.restart||'');
|
|
// Redémarrage auto du service côté serveur : le flux va se couper puis
|
|
// reconnecter tout seul (connectStream boucle). On rafraîchit l'état après.
|
|
if(r.restarting){ toast('mise à jour appliquée — reconnexion…'); setTimeout(loadAll, 6000); }
|
|
}
|
|
else { msg.textContent='Échec : '+(r.error||'inconnu'); }
|
|
}catch(e){ msg.textContent='Erreur pendant la mise à jour (réessaie).'; }
|
|
}
|
|
// Compteur de contexte : CTX_USED estimé via les stats serveur (prefill+decode
|
|
// du dernier tour ≈ taille du prochain prompt). À 90% on propose de compacter.
|
|
let CTX_MAX=0, CTX_USED=0, MODEL_READY=false;
|
|
// STATUS_SEEN : /api/status a répondu au moins une fois. Avant ça (ou s'il ne
|
|
// répond pas), on ne verrouille RIEN — mieux vaut un envoi qui échoue qu'un chat
|
|
// bloqué par un état inconnu.
|
|
let STATUS_SEEN=false;
|
|
function setCtxUsed(n){ CTX_USED=n||0; updateCtxMeter(); }
|
|
function updateCtxMeter(){
|
|
if(!CTX_MAX) return;
|
|
const pct=Math.min(100, Math.round(CTX_USED*100/CTX_MAX));
|
|
const fill=document.getElementById('ctx-fill');
|
|
fill.style.width=pct+'%';
|
|
fill.style.background = pct>=90 ? 'var(--err,#c44)' : pct>=70 ? 'var(--warn,#c93)' : 'var(--ok,#3a7)';
|
|
// Les chiffres SANS le mot « contexte » : la jauge juste en dessous dit déjà de
|
|
// quoi on parle, et le pied de carte est étroit. Le libellé complet reste en
|
|
// infobulle.
|
|
const ct=document.getElementById('ctx-text');
|
|
ct.textContent=CTX_USED+' / '+CTX_MAX+' ('+pct+'%)';
|
|
ct.title='contexte utilisé';
|
|
// Bouton de compaction MANUELLE : visible dès la moitié du contexte pour qu'on
|
|
// puisse compacter à la demande avant que l'auto-compaction (75%) ne s'en charge.
|
|
document.getElementById('ctx-compact').style.display = (pct>=50 && CTX_USED>0) ? 'inline-block' : 'none';
|
|
}
|
|
// ─── Moniteur machine (pied de barre latérale) ───────────────────────────────
|
|
// Une jauge par ressource : nom, pourcentage, barre, puis le détail chiffré en
|
|
// dessous. C'est la hiérarchie de la maquette — on lit la charge d'un coup
|
|
// d'œil, et le détail (Gio, température) reste là pour qui le cherche.
|
|
//
|
|
// Construit EN DOM et non en innerHTML : les noms de GPU viennent du pilote,
|
|
// donc d'ailleurs. Même règle que pour les noms de fichiers et les titres de
|
|
// discussions.
|
|
function perfRow(label, pct, title){
|
|
pct = Math.max(0, Math.min(100, Math.round(pct||0)));
|
|
const row=document.createElement('div'); row.className='perf-row';
|
|
if(title) row.title=title;
|
|
const l=document.createElement('div'); l.className='perf-l';
|
|
const b=document.createElement('b'); b.textContent=label;
|
|
const v=document.createElement('span'); v.className='perf-pct'; v.textContent=pct+'%';
|
|
l.append(b,v);
|
|
const track=document.createElement('div'); track.className='perf-track';
|
|
const fill=document.createElement('div');
|
|
// Sauge jusqu'à 75 %, ambre au-delà, rouille à 90 : la couleur ne sert à rien
|
|
// tant que la machine respire, elle doit sauter aux yeux quand elle sature.
|
|
fill.className='perf-fill'+(pct>=90?' max':pct>=75?' hot':'');
|
|
fill.style.width=pct+'%';
|
|
track.appendChild(fill);
|
|
row.append(l,track);
|
|
return row;
|
|
}
|
|
async function loadVram(){
|
|
const gpus=await jget('/api/vram');
|
|
const box=document.getElementById('vram');
|
|
if(!box) return;
|
|
box.textContent='';
|
|
if(!gpus || !gpus.length){
|
|
const d=document.createElement('div'); d.className='perf-sub'; d.textContent='(pas de GPU détecté)';
|
|
box.appendChild(d);
|
|
return;
|
|
}
|
|
// Deux mesures par carte, deux jauges — une seule barre ne peut pas porter la
|
|
// charge du processeur ET le remplissage de la mémoire vidéo, et pour du LLM
|
|
// c'est la VRAM qui décide si un modèle tient. Sur une machine à UNE carte on
|
|
// retombe exactement sur les trois jauges de la maquette (GPU, VRAM, RAM).
|
|
const many = gpus.length > 1;
|
|
gpus.forEach((g,i)=>{
|
|
const vram=(g.used/1024).toFixed(1)+' / '+(g.total/1024).toFixed(1)+' Gio';
|
|
box.appendChild(perfRow('GPU '+i, g.util, g.name+' · '+vram+' · '+g.temp+' °C'));
|
|
box.appendChild(perfRow(many ? 'VRAM '+i : 'VRAM', g.used*100/g.total,
|
|
g.name+' · '+vram));
|
|
});
|
|
}
|
|
async function loadRam(){
|
|
const m=await jget('/api/ram');
|
|
const box=document.getElementById('ram-details');
|
|
if(!box) return;
|
|
if(!m || !m.total){ box.style.display='none'; return; }
|
|
box.style.display='';
|
|
const host=document.getElementById('ram');
|
|
host.textContent='';
|
|
host.appendChild(perfRow('Mémoire vive', m.used*100/m.total,
|
|
(m.used/1024).toFixed(1)+' / '+(m.total/1024).toFixed(1)+' Gio utilisés'));
|
|
}
|
|
async function loadCfg(){
|
|
// /api/llamacpp en parallèle : il indique si le BIN de la config correspond au
|
|
// précompilé (prebuilt.in_use) ou compilé ici (in_use) — sinon c'est un fork perso.
|
|
const [c, lc] = await Promise.all([jget('/api/config'), jget('/api/llamacpp').catch(()=>null)]);
|
|
const row=(k,v,title)=>'<div class="kv"><span>'+k+'</span><span title="'+String(title!=null?title:v).replace(/"/g,'"')+'">'+String(v)+'</span></div>';
|
|
const rows=[];
|
|
if(c.BIN){
|
|
// Moteur : précompilé / compilé / personnalisé (avec le chemin). Le title garde
|
|
// toujours le chemin complet, quel que soit le libellé.
|
|
let v;
|
|
// En conteneur, le moteur vient de l'image : le dire, plutôt que de le
|
|
// ranger dans « personnalisé » qui laisse croire à un bricolage.
|
|
if(lc && lc.provided && sameBinPath(c.BIN, lc.provided_bin)) v='llama.cpp de l\'image';
|
|
// Moteur mis à jour depuis l'image officielle (Réglages → Moteur) : c'est la
|
|
// voie normale en conteneur, pas un « personnalisé ».
|
|
else if(lc && lc.engine && lc.engine.in_use){
|
|
const b = (lc.engine.tag||'').match(/b\d+$/);
|
|
v = b ? 'llama.cpp '+b[0]+' (mis à jour)' : 'llama.cpp mis à jour';
|
|
}
|
|
else if(lc && lc.prebuilt && lc.prebuilt.in_use) v='llama.cpp précompilé';
|
|
else if(lc && lc.in_use) v='llama.cpp compilé';
|
|
else v='llama.cpp personnalisé : '+c.BIN;
|
|
rows.push(row('MOTEUR', v, c.BIN));
|
|
}
|
|
// Le modèle chargé alimente aussi le sélecteur de l'en-tête : c'est la seule
|
|
// source qui le connaisse quand aucun preset ne correspond à la configuration.
|
|
if(typeof setLoadedModel === 'function') setLoadedModel(c.MODEL || '');
|
|
['MODEL','CTX','BATCH','UBATCH','NGL'].filter(k=>c[k]).forEach(k=>{
|
|
let v=c[k]; if(k==='MODEL') v=v.split('/').pop();
|
|
rows.push(row(k, v));
|
|
});
|
|
// n-cpu-moe : affiché seulement s'il est réellement présent dans EXTRA_ARGS.
|
|
const m=(c.EXTRA_ARGS||'').match(/--n-cpu-moe\s+(\d+)/);
|
|
if(m) rows.push(row('N-CPU-MOE', m[1]));
|
|
document.getElementById('cfg').innerHTML = rows.join('');
|
|
}
|