v0.9.3 : statut moteur plus précis + réglages d'interface

- statut : la pastille ne dit plus toujours « modèle incompatible » (prêt /
  chargement… / erreur / arrêté) ; le détail n'accuse l'incompatibilité que
  quand le journal le montre (quant/architecture), sinon renvoie au journal
- panneau Actions : bouton « refresh » retiré ; mise à jour + exporter sur une
  ligne, bench + clear chat sur l'autre
- modale preset : titre sans le mot « Preset » redondant (juste le nom)
- mémoire : « auto (proactive) » → « auto »
This commit is contained in:
nathaninline committed 2026-08-14 11:20:18 +02:00
1 parent e3957fa749
commit d4989f2991
10 files changed
+54 -55

No files matched your search

+7 -13
View File
@@ -1,19 +1,13 @@
Les postes distants se pilotent maintenant depuis le chat, et l'envoi de fichiers à travers ajean.link est réparé.
Quelques réglages d'interface pour une lecture plus claire, et un statut du moteur enfin honnête.
## Les postes distants à portée de main
## Un statut du moteur plus juste
Un nouveau bouton apparaît dans la barre de saisie, juste à côté du trombone. Son icône dit **sur quelle machine l'IA agit** : un écran quand c'est ce serveur, des ondes wifi quand c'est un poste distant. Un clic ouvre une **fenêtre dédiée** où tu choisis la cible, vois tes postes appairés (avec leurs capacités et leur dossier autorisé) et en ajoutes de nouveaux.
La pastille d'état affichait « modèle incompatible » dès qu'un chargement échouait — même quand la vraie cause était ailleurs. Désormais elle reste courte et neutre (**prêt**, **chargement…**, **erreur**, **arrêté**), et le message détaillé en dessous n'accuse le moteur d'incompatibilité **que lorsque le journal le montre vraiment** (quantification ou architecture non reconnue). Pour tout autre échec, il renvoie simplement au journal du moteur au lieu de donner une fausse piste.
Plus besoin d'aller fouiller dans les réglages : tout est là, au moment où on en a besoin.
## Panneau « Actions » réorganisé
## Envoi de fichiers réparé sur ajean.link
Le bouton « refresh », qui ne servait à rien, est retiré. Les actions sont regroupées deux par deux : **mise à jour** et **exporter** sur une ligne, **bench** et **effacer la conversation** sur l'autre.
Depuis l'accès distant, joindre un fichier à un message échouait. La cause : l'envoi ne passait pas par le tunnel chiffré de bout en bout, contrairement au téléchargement. C'est corrigé — l'envoi emprunte désormais le même chemin, et un gigaoctet traverse toujours par morceaux, sans que le relais ne voie rien.
## Petits nettoyages
## Installer un poste, en une commande ré-exécutable
`ajean remote install …` peut maintenant être relancé tel quel : fournir un code d'appairage force toujours le (ré)appairage, et la commande retire proprement un service déjà en place au lieu d'échouer sur un fichier verrouillé. Fini le `logout` manuel entre deux essais.
## Mise à jour depuis l'interface, même sans root
Sur un serveur où le service tourne sous un compte non-root, le bouton « mettre à jour » de l'interface redémarre désormais correctement le service (via sudo non interactif) au lieu d'échouer en silence.
Le titre de l'éditeur de preset n'affiche plus le mot « Preset » redondant (juste le nom), et le mode mémoire automatique s'appelle simplement « auto ».
Binary file not shown.
Binary file not shown.
+3 -3
View File
@@ -3,13 +3,13 @@
"FileVersion": {
"Major": 0,
"Minor": 9,
"Patch": 2,
"Patch": 3,
"Build": 0
},
"ProductVersion": {
"Major": 0,
"Minor": 9,
"Patch": 2,
"Patch": 3,
"Build": 0
},
"FileFlagsMask": "3f",
@@ -25,7 +25,7 @@
"LegalCopyright": "Copyright (c) 2026 AJEAN contributors. MIT License.",
"OriginalFilename": "ajean.exe",
"ProductName": "AJEAN",
"ProductVersion": "0.9.2",
"ProductVersion": "0.9.3",
"Comments": "https://github.com/nathaninline/ajean — projet open source (MIT)"
},
"VarFileInfo": {
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"strings"
)
const Version = "0.9.2"
const Version = "0.9.3"
// Main est le vrai main() du binaire (cmd/ajean ne fait que l'appeler).
func Main() {
+15 -18
View File
@@ -1249,7 +1249,7 @@ button:hover{border-color:var(--dim);color:var(--text)}
<div class="slist">
<label class="switchrow"><span>mode</span>
<select id="mem-mode" onchange="setMemMode()" class="sctl">
<option value="always">auto (proactive)</option>
<option value="always">auto</option>
<option value="ondemand">sur demande</option>
<option value="off">désactivée</option>
</select>
@@ -1460,23 +1460,17 @@ button:hover{border-color:var(--dim);color:var(--text)}
</details>
<details id="upd-details"><summary><span class="stitle">Actions</span></summary>
<div class="slist"><div class="rowbody">
<!-- Ligne 1 : mise à jour + export du fil. Depuis la 0.8 la conversation vit
dans ajean.db (bbolt, verrouillé tant que le service tourne) : sans le
bouton exporter, plus personne ne peut relire/archiver ses échanges. -->
<div class="row" style="margin:0">
<button id="upd-check" onclick="checkUpdate()">↥ Vérifier les mises à jour</button>
</div>
<div id="upd-msg" class="muted"></div>
<!-- Export du fil. Depuis la 0.8 la conversation vit dans ajean.db (bbolt,
binaire et verrouillé tant que le service tourne) : sans ce bouton,
plus personne ne peut relire ni archiver ses échanges, ce que
permettait l'ancien conversation.json. -->
<div class="row" style="margin-bottom:0">
<button onclick="loadAll()">↻ refresh</button>
<button id="btn-bench" onclick="runBenchUI()">bench</button>
<button onclick="openExportModal()" title="choisir le format et ce qu'on emporte">⤓ exporter…</button>
</div>
<!-- « clear chat » est en DERNIER, et seul sur sa ligne : c'est la seule
action destructive du panneau, elle ne doit pas se trouver sous le
doigt qui visait « refresh ». -->
<div id="upd-msg" class="muted"></div>
<!-- Ligne 2 : bench + clear chat (seule action destructive, gardée en dernier). -->
<div class="row" style="margin-bottom:0">
<button id="btn-bench" onclick="runBenchUI()">bench</button>
<button onclick="resetChat()">⌫ clear chat</button>
</div>
</div></div>
@@ -2280,9 +2274,12 @@ async function loadStatus(){
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).
let cls='err', txt=s.state;
// 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='modèle incompatible'; }
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;
@@ -3020,7 +3017,7 @@ const KINDS = {
// skills keep name-as-identity (param 'name').
// newLabel / nameHint : « Nouveau Page » et « Nom du preset » dans l'éditeur de
// mémoire venaient d'un libellé unique décliné mécaniquement.
preset: {label:'Preset', newLabel:'Nouveau preset', nameHint:'Nom du preset',
preset: {label:'', newLabel:'Nouveau preset', nameHint:'Nom du preset',
param:'id', getUrl:'/api/preset', saveUrl:'/api/preset/save', delUrl:'/api/preset/delete', reload:()=>loadPresets()},
mem: {label:'Page', newLabel:'Nouvelle page', nameHint:'Nom de la page',
param:'name', getUrl:'/api/mem', saveUrl:'/api/mem/save', delUrl:'/api/mem/delete', reload:()=>loadMem()},
@@ -3039,7 +3036,7 @@ async function openItem(kind, key){
const K = KINDS[kind];
const seq = ++openSeq;
editingKind = kind; editingKey = key || '';
document.getElementById('modal-title').textContent = key ? (K.label + ' · ' + key) : K.newLabel;
document.getElementById('modal-title').textContent = key ? (K.label ? K.label + ' · ' + key : key) : K.newLabel;
document.getElementById('m-name').value = key || '';
document.getElementById('m-name').placeholder = K.nameHint;
document.getElementById('m-content').value = '';
@@ -3091,7 +3088,7 @@ async function openItem(kind, key){
const d = await r.json();
if(seq !== openSeq) return; // une autre ouverture a pris la main
const display = d.name || key || '';
document.getElementById('modal-title').textContent = key ? (K.label + ' · ' + display) : K.newLabel;
document.getElementById('modal-title').textContent = key ? (K.label ? K.label + ' · ' + display : display) : K.newLabel;
document.getElementById('m-name').value = display;
document.getElementById('m-content').value = d.content || '';
if(kind === 'preset'){
+7 -13
View File
@@ -79,7 +79,7 @@
<div class="slist">
<label class="switchrow"><span>mode</span>
<select id="mem-mode" onchange="setMemMode()" class="sctl">
<option value="always">auto (proactive)</option>
<option value="always">auto</option>
<option value="ondemand">sur demande</option>
<option value="off">désactivée</option>
</select>
@@ -290,23 +290,17 @@
</details>
<details id="upd-details"><summary><span class="stitle">Actions</span></summary>
<div class="slist"><div class="rowbody">
<!-- Ligne 1 : mise à jour + export du fil. Depuis la 0.8 la conversation vit
dans ajean.db (bbolt, verrouillé tant que le service tourne) : sans le
bouton exporter, plus personne ne peut relire/archiver ses échanges. -->
<div class="row" style="margin:0">
<button id="upd-check" onclick="checkUpdate()">↥ Vérifier les mises à jour</button>
</div>
<div id="upd-msg" class="muted"></div>
<!-- Export du fil. Depuis la 0.8 la conversation vit dans ajean.db (bbolt,
binaire et verrouillé tant que le service tourne) : sans ce bouton,
plus personne ne peut relire ni archiver ses échanges, ce que
permettait l'ancien conversation.json. -->
<div class="row" style="margin-bottom:0">
<button onclick="loadAll()">↻ refresh</button>
<button id="btn-bench" onclick="runBenchUI()">bench</button>
<button onclick="openExportModal()" title="choisir le format et ce qu'on emporte">⤓ exporter…</button>
</div>
<!-- « clear chat » est en DERNIER, et seul sur sa ligne : c'est la seule
action destructive du panneau, elle ne doit pas se trouver sous le
doigt qui visait « refresh ». -->
<div id="upd-msg" class="muted"></div>
<!-- Ligne 2 : bench + clear chat (seule action destructive, gardée en dernier). -->
<div class="row" style="margin-bottom:0">
<button id="btn-bench" onclick="runBenchUI()">bench</button>
<button onclick="resetChat()">⌫ clear chat</button>
</div>
</div></div>
+5 -2
View File
@@ -3,9 +3,12 @@ async function loadStatus(){
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).
let cls='err', txt=s.state;
// 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='modèle incompatible'; }
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;
+3 -3
View File
@@ -74,7 +74,7 @@ const KINDS = {
// skills keep name-as-identity (param 'name').
// newLabel / nameHint : « Nouveau Page » et « Nom du preset » dans l'éditeur de
// mémoire venaient d'un libellé unique décliné mécaniquement.
preset: {label:'Preset', newLabel:'Nouveau preset', nameHint:'Nom du preset',
preset: {label:'', newLabel:'Nouveau preset', nameHint:'Nom du preset',
param:'id', getUrl:'/api/preset', saveUrl:'/api/preset/save', delUrl:'/api/preset/delete', reload:()=>loadPresets()},
mem: {label:'Page', newLabel:'Nouvelle page', nameHint:'Nom de la page',
param:'name', getUrl:'/api/mem', saveUrl:'/api/mem/save', delUrl:'/api/mem/delete', reload:()=>loadMem()},
@@ -93,7 +93,7 @@ async function openItem(kind, key){
const K = KINDS[kind];
const seq = ++openSeq;
editingKind = kind; editingKey = key || '';
document.getElementById('modal-title').textContent = key ? (K.label + ' · ' + key) : K.newLabel;
document.getElementById('modal-title').textContent = key ? (K.label ? K.label + ' · ' + key : key) : K.newLabel;
document.getElementById('m-name').value = key || '';
document.getElementById('m-name').placeholder = K.nameHint;
document.getElementById('m-content').value = '';
@@ -145,7 +145,7 @@ async function openItem(kind, key){
const d = await r.json();
if(seq !== openSeq) return; // une autre ouverture a pris la main
const display = d.name || key || '';
document.getElementById('modal-title').textContent = key ? (K.label + ' · ' + display) : K.newLabel;
document.getElementById('modal-title').textContent = key ? (K.label ? K.label + ' · ' + display : display) : K.newLabel;
document.getElementById('m-name').value = display;
document.getElementById('m-content').value = d.content || '';
if(kind === 'preset'){
+13 -2
View File
@@ -102,8 +102,19 @@ func modelLoadError() string {
if loaded || reason == "" {
return ""
}
return "Le modèle n'a pas pu être chargé : " + reason +
". Il est probablement incompatible avec le moteur sélectionné — choisis un autre backend pour ce modèle (édite le modèle → section Moteur)."
// Message PRÉCIS selon la cause : n'affirmer l'incompatibilité que quand le
// journal la montre vraiment (quant/architecture). Un échec générique de
// chargement peut avoir bien d'autres causes (mémoire, fichier, tenseur) — on
// renvoie alors vers le journal plutôt que d'accuser à tort le moteur.
switch reason {
case "format de quantification non reconnu par ce moteur",
"type/architecture de modèle inconnu de ce moteur":
return "Modèle incompatible avec le moteur : " + reason +
". Choisis un autre backend (édite le modèle → Moteur)."
default:
return "Le modèle n'a pas pu être chargé (" + reason +
"). Ouvre le journal du moteur pour le détail."
}
}
// handleServiceLog (GET /api/service/log) : dernières lignes du journal du