, 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 v'+r.latest+' 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=>''+r[0]+'
'+escHtml(r[1]||'')+'
').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 v'+r.version+'.
'+(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)=>''+k+''+String(v)+'
';
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('');
}