diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index c52fbb0..073de7d 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,19 +1,19 @@ -Le poste distant est maintenant intégré à AJEAN : plus de binaire séparé, plus de téléchargement à part. +Les postes distants se pilotent maintenant depuis le chat, et l'envoi de fichiers à travers ajean.link est réparé. -## Une seule application +## Les postes distants à portée de main -Jusqu'ici, piloter un autre PC demandait d'installer un second binaire, « ajean-remote ». C'était une application de plus à télécharger, à suivre et à mettre à jour. On l'a supprimée : tout vit désormais dans **AJEAN**. +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. -Sur le PC à piloter, tu installes simplement AJEAN, puis tu lances : +Plus besoin d'aller fouiller dans les réglages : tout est là, au moment où on en a besoin. -``` -ajean remote install https://ajean.link --machine --key --code --allow shell,read,write,list -``` +## Envoi de fichiers réparé sur ajean.link -La commande s'installe en service — invisible, au démarrage, reconnexion automatique — exactement comme avant. Rien ne change au chiffrement : l'accès reste **chiffré de bout en bout**, le relais ajean.link ne voit toujours rien. +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. -## Rien à changer dans ton usage +## Installer un poste, en une commande ré-exécutable -Dans **Réglages → Postes distants**, générer un code affiche la commande prête à copier — à distance (via ajean.link) ou en réseau local. Seul le nom de la commande change : `ajean remote install …` au lieu de `ajean-remote install …`. +`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. -Les postes déjà appairés continuent de fonctionner sans réappairage. +## 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. diff --git a/cmd/ajean/resource_windows_amd64.syso b/cmd/ajean/resource_windows_amd64.syso index 8cc6175..9b53b00 100644 Binary files a/cmd/ajean/resource_windows_amd64.syso and b/cmd/ajean/resource_windows_amd64.syso differ diff --git a/cmd/ajean/resource_windows_arm64.syso b/cmd/ajean/resource_windows_arm64.syso index c8b30d3..f481cc0 100644 Binary files a/cmd/ajean/resource_windows_arm64.syso and b/cmd/ajean/resource_windows_arm64.syso differ diff --git a/cmd/ajean/versioninfo.json b/cmd/ajean/versioninfo.json index 2864961..ec0b6c1 100644 --- a/cmd/ajean/versioninfo.json +++ b/cmd/ajean/versioninfo.json @@ -3,13 +3,13 @@ "FileVersion": { "Major": 0, "Minor": 9, - "Patch": 1, + "Patch": 2, "Build": 0 }, "ProductVersion": { "Major": 0, "Minor": 9, - "Patch": 1, + "Patch": 2, "Build": 0 }, "FileFlagsMask": "3f", @@ -25,7 +25,7 @@ "LegalCopyright": "Copyright (c) 2026 AJEAN contributors. MIT License.", "OriginalFilename": "ajean.exe", "ProductName": "AJEAN", - "ProductVersion": "0.9.1", + "ProductVersion": "0.9.2", "Comments": "https://github.com/nathaninline/ajean — projet open source (MIT)" }, "VarFileInfo": { diff --git a/internal/ajean/chat_compact.go b/internal/ajean/chat_compact.go index 14a2746..21bbf18 100644 --- a/internal/ajean/chat_compact.go +++ b/internal/ajean/chat_compact.go @@ -135,7 +135,7 @@ func compactWouldTrigger(msgs []Message, knownTokens int) bool { } // logCompact trace UNE ligne par décision de compaction sur la sortie d'erreur -// (donc dans `journalctl -u ajean-link`). Sans ça, une compaction qui ne se +// (donc dans `journalctl -u ajean-ui`). Sans ça, une compaction qui ne se // déclenche pas — ou qui se déclenche et n'enlève rien — est invisible : côté // UI on ne voit qu'une jauge qui reste haute, sans savoir si le seuil n'a pas // été atteint ou si la réduction a été refusée. diff --git a/internal/ajean/cli_app.go b/internal/ajean/cli_app.go index 9b74f44..cf53a78 100644 --- a/internal/ajean/cli_app.go +++ b/internal/ajean/cli_app.go @@ -40,7 +40,7 @@ func cmdApp(args []string) error { return openBrowser(url) } - // UN SEUL process propriétaire de la conversation, comme le service ajean-link + // UN SEUL process propriétaire de la conversation, comme le service ajean-ui // sous Linux : l'app sert l'UI locale ET le tunnel avec le MÊME mux. Deux // process qui servent la conversation (objet en mémoire persisté dans // conversation.json) donnent deux fils divergents entre le local et diff --git a/internal/ajean/relay_link.go b/internal/ajean/relay_link.go index 76ab676..0fb8783 100644 --- a/internal/ajean/relay_link.go +++ b/internal/ajean/relay_link.go @@ -315,7 +315,7 @@ func uiPIDPath() string { return filepath.Join(AjeanHome(), uiUnitName+".pid") } func uiLogPath() string { return filepath.Join(AjeanHome(), uiUnitName+".log") } // uiServiceCtl pilote le worker de lien (start/stop/restart). Sous Linux c'est -// l'unité systemd ajean-link (avec sudo non interactif si on n'est pas root) ; +// l'unité systemd ajean-ui (avec sudo non interactif si on n'est pas root) ; // ailleurs — macOS et Windows, où il n'y a ni systemd ni droits root — on lance // « ajean link serve » en processus détaché suivi par un fichier PID, comme le // fait déjà le service principal. Sans ça, l'accès distant restait définitivement diff --git a/internal/ajean/remote.go b/internal/ajean/remote.go index ff2fe85..9326162 100644 --- a/internal/ajean/remote.go +++ b/internal/ajean/remote.go @@ -82,7 +82,13 @@ Options : func remoteConnect(args []string, install bool) error { f := parseRemoteFlags(args) cfg, lerr := nodeclient.LoadConfig() - needEnroll := lerr != nil || cfg.Priv == "" + // Un --code est un jeton d'appairage à usage unique : le fournir veut TOUJOURS + // dire « (ré)appaire-moi maintenant ». Sans ce « || f.code != "" », une config + // résiduelle (test précédent, ancien binaire ajean-remote au même emplacement + // %ProgramData%\ajean-remote\config.json) faisait sauter l'appairage en silence : + // le service démarrait avec de vieilles clés que le serveur ignore et le poste + // ne remontait jamais, alors que tout affichait « [ok] ». + needEnroll := lerr != nil || cfg.Priv == "" || f.code != "" if f.url != "" { cfg.ServerURL = f.url } diff --git a/internal/ajean/run.go b/internal/ajean/run.go index 0638d9a..dbc77ec 100644 --- a/internal/ajean/run.go +++ b/internal/ajean/run.go @@ -10,7 +10,7 @@ import ( "strings" ) -const Version = "0.9.1" +const Version = "0.9.2" // Main est le vrai main() du binaire (cmd/ajean ne fait que l'appeler). func Main() { diff --git a/internal/ajean/sys_update.go b/internal/ajean/sys_update.go index 3b5da85..aa7b8e0 100644 --- a/internal/ajean/sys_update.go +++ b/internal/ajean/sys_update.go @@ -434,7 +434,7 @@ func handleUpdateCheck(w http.ResponseWriter, r *http.Request) { // handleUpdateApply (POST /api/update/apply) : télécharge et installe la dernière // version. Sur un serveur Linux/systemd, relance ensuite AUTOMATIQUEMENT le service -// ajean-link (celui qui sert l'UI, le chat et le tunnel) pour appliquer la MAJ sans +// d'UI (ajean-ui : il sert l'UI, le chat et le tunnel) pour appliquer la MAJ sans // avoir à SSH — voir restartAfterUpdate. Renvoie la version + le message de statut. func handleUpdateApply(w http.ResponseWriter, r *http.Request) { newVer, err := applyUpdate() @@ -450,15 +450,15 @@ func handleUpdateApply(w http.ResponseWriter, r *http.Request) { sendJSON(w, 200, map[string]any{"ok": true, "version": newVer, "restart": msg, "restarting": restarting}) } -// restartAfterUpdate relance le service ajean-link juste après une MAJ déclenchée -// depuis l'UI, pour éviter le SSH manuel. Conditions : Linux/systemd + ajean-link -// actif (le service tourne en root → systemctl passe sans sudo). On NE touche PAS à -// ajean.service (llama-server) pour ne pas recharger le modèle (long et inattendu -// depuis un bouton « mettre à jour »). Le redémarrage est DIFFÉRÉ (la réponse HTTP -// doit partir d'abord) et lancé en --no-block : systemd enregistre le job puis nous -// arrête/relance ; la clé E2E (.e2e_key) survit au restart, donc pas de ré-appairage -// et l'UI se reconnecte toute seule (boucle de reconnexion du flux). Renvoie -// (déclenché, message). Non-serveur (poste client, Windows) → (false, ""). +// restartAfterUpdate relance le service d'UI (uiServiceName, « ajean-ui » : il sert +// l'interface, le chat et le tunnel) juste après une MAJ déclenchée depuis l'UI, pour +// éviter le SSH manuel. Conditions : Linux/systemd + service actif. On NE touche PAS à +// ajean-engine (llama-server) pour ne pas recharger le modèle (long et inattendu depuis +// un bouton « mettre à jour »). Le redémarrage est DIFFÉRÉ (la réponse HTTP doit partir +// d'abord) et lancé en --no-block : systemd enregistre le job puis nous arrête/relance ; +// la clé E2E (.e2e_key) survit au restart, donc pas de ré-appairage et l'UI se reconnecte +// toute seule (boucle de reconnexion du flux). Renvoie (déclenché, message). Non-serveur +// (poste client, Windows) → (false, ""). func restartAfterUpdate() (bool, string) { // Windows : pas de superviseur pour nous relancer, on delegue a un // accompagnateur detache (voir sys_restart_windows.go). C'est aussi ce qui @@ -472,9 +472,16 @@ func restartAfterUpdate() (bool, string) { } go func() { time.Sleep(1500 * time.Millisecond) // laisser la réponse HTTP atteindre le client + // Le service tourne souvent en User=nathan (pas root) : systemctl brut + // échouerait alors (polkit). On repli sur « sudo -n systemctl » comme + // uiServiceCtl, les sudoers /etc/sudoers.d/ajean-ui autorisant le restart. // --no-block : on enregistre le job puis on rend la main ; systemd exécute le // stop/start même si ce process (et le client systemctl) sont tués entre-temps. - _ = exec.Command("systemctl", "--no-block", "restart", uiServiceName()).Run() + bin, args := "systemctl", []string{"--no-block", "restart", uiServiceName()} + if os.Geteuid() != 0 { + bin, args = "sudo", append([]string{"-n", "systemctl"}, args...) + } + _ = exec.Command(bin, args...).Run() }() return true, "Service " + uiServiceName() + " redémarré automatiquement — la page va se reconnecter seule (le modèle n'est pas rechargé)." } @@ -483,7 +490,8 @@ func restartHintText() string { if runtime.GOOS == "windows" { return "Redémarre AJEAN (quitte puis relance) pour appliquer la mise à jour." } - return "Redémarre les services pour appliquer : sudo systemctl restart ajean ajean-link" + return "Redémarre pour appliquer : sudo systemctl restart " + uiServiceName() + + " (ajoute " + serviceName() + " si la mise à jour touche le moteur)." } func printRestartHint() { fmt.Println(restartHintText()) } diff --git a/internal/ajean/ui/index.html b/internal/ajean/ui/index.html index 0353741..91ba5a7 100644 --- a/internal/ajean/ui/index.html +++ b/internal/ajean/ui/index.html @@ -1052,6 +1052,20 @@ html[data-hide-stats="1"] #chat .msg .statline{display:none} #attach:hover{background:var(--row-bg-on);border-color:transparent;color:var(--text)} #attach:active{transform:scale(.9)} #attach svg{display:block} +/* Bouton « Postes distants » du composeur : même gabarit que le trombone. L'icône + dépend de la cible active — écran (local) par défaut, ondes wifi quand un poste + distant est sélectionné, alors teinté en accent pour signaler « l'IA agit + ailleurs ». Les deux SVG coexistent ; on n'en montre qu'un via la classe. */ +#node-btn{width:18px;height:18px;min-height:18px;flex:none;margin:0;padding:0; + display:flex;align-items:center;justify-content:center;border-radius:999px; + background:transparent;border:1px solid transparent;color:var(--dim)} +#node-btn:hover{background:var(--row-bg-on);border-color:transparent;color:var(--text)} +#node-btn:active{transform:scale(.9)} +#node-btn svg{display:block} +#node-btn .ic-remote{display:none} +#node-btn.remote{color:var(--accent)} +#node-btn.remote .ic-here{display:none} +#node-btn.remote .ic-remote{display:block} /* Fichiers en attente : de petites bulles POSÉES AU-DESSUS de la carte. Le composeur réserve déjà 34 px transparents au-dessus de lui (la bande de dégradé qui fond vers le fil) : les pastilles s'y logent, et on retire d'autant @@ -1302,15 +1316,6 @@ button:hover{border-color:var(--dim);color:var(--text)} -
-
Postes distants?Pilote un autre PC depuis l'IA. installe ajean sur le PC secondaire et lance ajean remote install <url> --code CODE : il se connecte en sortant (aucun port à ouvrir) et expose à l'IA un jeu d'outils restreint (shell, fichiers). ⚠ tu décides des capacités et du dossier autorisé pour chaque poste ; le poste peut aussi demander confirmation localement.
-
-
-
(aucun poste — appaire un PC pour que l'IA puisse agir dessus)
- -
-
-
Paramètres
@@ -1504,6 +1509,13 @@ button:hover{border-color:var(--dim);color:var(--text)} 0 / 0 + +