mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
llama.cpp publie plusieurs versions par jour ; l'image de Loki ne se reconstruit qu'à une mise à jour de Loki. Le moteur y était donc figé à la date du dernier build, et le rattraper imposait un rebuild complet de 2,6 Go pour un composant qui en pèse 170 Mo. Le panneau « Moteur » ne le disait même pas : il annonçait « rien à mettre à jour ici ». Réglages → Moteur affiche maintenant la version qui tourne (bXXXXX et son commit, lus dans la bannière du binaire — jusqu'ici invisibles ailleurs que dans le journal du moteur) et la met à jour en un clic. Où le moteur est pris. Pas dans les releases GitHub de llama.cpp : elles ne contiennent AUCUN binaire CUDA pour Linux, et le mode « précompilé » retomberait sur Vulkan, donc sur une régression pour une carte NVIDIA. La seule distribution CUDA/Linux officielle et précompilée est l'image de conteneur — celle-là même dont l'image de Loki hérite. On lit son manifeste OCI et on ne télécharge que les couches qui portent /app, en descendant du sommet : le runtime CUDA (2 Go) et la base système sont déjà là. S'arrêter au binaire ne suffit pas — llama.cpp le range dans une couche et ses .so dans la précédente — d'où une règle d'arrêt sur « binaire + libggml-base + libllama », et un garde-fou de taille qui interdit de descendre jusqu'au runtime. Le moteur atterrit dans /data/engine/<version>/, donc sur le volume de données : il survit à un docker compose pull. La variante (CUDA, Vulkan, SYCL, MUSA, CPU) est déduite des backends ggml posés à côté du moteur courant — le conteneur ne sait pas de quelle image il vient, et faire retenir « server-cuda » à l'utilisateur serait un piège. Le risque, et ce qui le couvre. La mise à jour apporte llama.cpp, pas le runtime CUDA, qui reste celui de l'image : un llama.cpp compilé pour un CUDA plus récent ne chargerait pas son backend GPU. Le symptôme serait silencieux — tout marche, mais sur le processeur. Le nouveau moteur est donc lancé à blanc avant toute bascule ; il est refusé s'il ne démarre pas, ET s'il ne voit plus aucune carte alors que le moteur courant en voyait. Dans les deux cas le moteur courant n'est pas touché, et celui de l'image reste intact : « revenir au moteur de l'image » y ramène en un clic, sans réseau. Vérifié de bout en bout contre le vrai ghcr.io (166 Mo, 7 s, toutes les bibliothèques et leurs liens de version présents) et, pour les chemins d'échec, contre un faux registre. LOKI_OCI_REGISTRY permet de viser un miroir quand ghcr.io n'est pas joignable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPyFxVHNAN9u5pVzSYMwjd
227 lines
12 KiB
JavaScript
227 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…'; }
|
|
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('');
|
|
}
|