diff --git a/internal/jean/sys_platform_unix.go b/internal/jean/sys_platform_unix.go
index 7645c62..8a1aa99 100644
--- a/internal/jean/sys_platform_unix.go
+++ b/internal/jean/sys_platform_unix.go
@@ -106,13 +106,28 @@ func totalRAMGB() float64 {
// live under /usr/local/cuda*/lib64 and are often absent from the global ld
// cache — without them llama-server fails to load the GPU backend (or runs
// degraded), costing a large chunk of throughput.
+// Sur macOS, dyld IGNORE LD_LIBRARY_PATH : la variable équivalente est
+// DYLD_LIBRARY_PATH. Les binaires macOS des releases llama.cpp sont livrés avec
+// leurs libllama/libggml*.dylib À CÔTÉ de l'exécutable — sans cette variable,
+// llama-server meurt instantanément sur « Library not loaded », donc sans le
+// moindre message dans l'UI.
func setLibraryPath(dir string) {
parts := []string{dir}
parts = append(parts, cudaLibDirs()...)
- if existing := os.Getenv("LD_LIBRARY_PATH"); existing != "" {
+ envVar := "LD_LIBRARY_PATH"
+ if runtime.GOOS == "darwin" {
+ envVar = "DYLD_LIBRARY_PATH"
+ }
+ if existing := os.Getenv(envVar); existing != "" {
parts = append(parts, existing)
}
- _ = os.Setenv("LD_LIBRARY_PATH", strings.Join(parts, ":"))
+ joined := strings.Join(parts, ":")
+ _ = os.Setenv(envVar, joined)
+ if runtime.GOOS == "darwin" {
+ // Filet de sécurité : DYLD_FALLBACK_LIBRARY_PATH est consulté en dernier
+ // recours et survit à certains contextes où DYLD_LIBRARY_PATH est purgé.
+ _ = os.Setenv("DYLD_FALLBACK_LIBRARY_PATH", joined+":/usr/local/lib:/usr/lib")
+ }
}
// cudaLibDirs returns the CUDA runtime lib directories present on the machine,
diff --git a/internal/jean/sys_service_darwin.go b/internal/jean/sys_service_darwin.go
index c2d046b..dc26195 100644
--- a/internal/jean/sys_service_darwin.go
+++ b/internal/jean/sys_service_darwin.go
@@ -268,3 +268,7 @@ func tailFile(path string, n int) string {
}
return strings.Join(lines, "\n") + "\n"
}
+
+// serviceLogTail renvoie les n dernières lignes du journal du service (pour
+// l'UI web). Le fichier est le même en mode launchd et en mode utilisateur.
+func serviceLogTail(n int) string { return tailFile(logFilePath(), n) }
diff --git a/internal/jean/sys_service_linux.go b/internal/jean/sys_service_linux.go
index 54dbf86..ede3a77 100644
--- a/internal/jean/sys_service_linux.go
+++ b/internal/jean/sys_service_linux.go
@@ -6,6 +6,7 @@ import (
"fmt"
"os"
"os/exec"
+ "strconv"
"strings"
"time"
)
@@ -70,3 +71,13 @@ func serviceIsActive() bool {
out, _ := exec.Command("systemctl", "is-active", serviceName()).Output()
return strings.TrimSpace(string(out)) == "active"
}
+
+// serviceLogTail renvoie les n dernières lignes du journal du service (pour
+// l'UI web). Linux : journalctl.
+func serviceLogTail(n int) string {
+ out, err := exec.Command("journalctl", "-u", serviceName(), "-n", strconv.Itoa(n), "--no-pager").CombinedOutput()
+ if err != nil && len(out) == 0 {
+ return "journalctl indisponible : " + err.Error()
+ }
+ return string(out)
+}
diff --git a/internal/jean/sys_service_windows.go b/internal/jean/sys_service_windows.go
index e212365..be98fe8 100644
--- a/internal/jean/sys_service_windows.go
+++ b/internal/jean/sys_service_windows.go
@@ -221,3 +221,7 @@ func tailFile(path string, n int) string {
}
return strings.Join(lines, "\n") + "\n"
}
+
+// serviceLogTail renvoie les n dernières lignes du journal du service (pour
+// l'UI web).
+func serviceLogTail(n int) string { return tailFile(logFilePath(), n) }
diff --git a/internal/jean/ui/index.html b/internal/jean/ui/index.html
index c0863a5..e9cebc1 100644
--- a/internal/jean/ui/index.html
+++ b/internal/jean/ui/index.html
@@ -68,7 +68,7 @@ h1{margin:0;font-size:18px;color:var(--accent)}
#ver{font-size:10px;font-weight:400;color:var(--dim);margin-left:5px;letter-spacing:0}
.sidehead{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:18px}
/* pastille d'état service, en haut à droite de la barre latérale */
-.statuspill{display:inline-flex;align-items:center;gap:6px;font-size:11px;padding:3px 10px;border-radius:999px;border:1px solid var(--border);background:var(--panel);color:var(--dim);font-weight:600;white-space:nowrap}
+.statuspill{display:inline-flex;align-items:center;gap:6px;font-size:11px;padding:3px 10px;border-radius:999px;border:1px solid var(--border);background:var(--panel);color:var(--dim);font-weight:600;white-space:nowrap;cursor:pointer}
.statuspill.ok{color:var(--ok);border-color:color-mix(in srgb,var(--ok) 45%,transparent);background:color-mix(in srgb,var(--ok) 12%,transparent)}
.statuspill.err{color:var(--err);border-color:color-mix(in srgb,var(--err) 45%,transparent);background:color-mix(in srgb,var(--err) 12%,transparent)}
.statuspill.loading{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 45%,transparent);background:color-mix(in srgb,var(--warn) 12%,transparent)}
@@ -399,7 +399,16 @@ button:disabled{opacity:.5;cursor:not-allowed}
ajean
- …
+ …
+
+
+
+
+ Journal du moteur
+
+
+
GPU
…
@@ -913,6 +922,24 @@ async function loadStatus(){
document.getElementById('ver').textContent='v'+s.version;
}
}
+// Journal du moteur — replié par défaut, on l'ouvre en cliquant la pastille.
+function toggleSvcLog(){
+ const box=document.getElementById('svc-log-box');
+ if(!box) return;
+ const show = box.style.display==='none';
+ box.style.display = show ? '' : 'none';
+ if(show) loadSvcLog();
+}
+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…';
diff --git a/internal/jean/ui/src/index.tmpl.html b/internal/jean/ui/src/index.tmpl.html
index 4b79619..7f44c84 100644
--- a/internal/jean/ui/src/index.tmpl.html
+++ b/internal/jean/ui/src/index.tmpl.html
@@ -24,7 +24,16 @@
ajean
- …
+ …
+
+
+
+
+ Journal du moteur
+
+
+
GPU
…
diff --git a/internal/jean/ui/src/js/05-status.js b/internal/jean/ui/src/js/05-status.js
index 3c86ae8..be2d1b3 100644
--- a/internal/jean/ui/src/js/05-status.js
+++ b/internal/jean/ui/src/js/05-status.js
@@ -14,6 +14,24 @@ async function loadStatus(){
document.getElementById('ver').textContent='v'+s.version;
}
}
+// Journal du moteur — replié par défaut, on l'ouvre en cliquant la pastille.
+function toggleSvcLog(){
+ const box=document.getElementById('svc-log-box');
+ if(!box) return;
+ const show = box.style.display==='none';
+ box.style.display = show ? '' : 'none';
+ if(show) loadSvcLog();
+}
+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…';
diff --git a/internal/jean/ui/src/styles.css b/internal/jean/ui/src/styles.css
index 873ee4a..2247b57 100644
--- a/internal/jean/ui/src/styles.css
+++ b/internal/jean/ui/src/styles.css
@@ -49,7 +49,7 @@ h1{margin:0;font-size:18px;color:var(--accent)}
#ver{font-size:10px;font-weight:400;color:var(--dim);margin-left:5px;letter-spacing:0}
.sidehead{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:18px}
/* pastille d'état service, en haut à droite de la barre latérale */
-.statuspill{display:inline-flex;align-items:center;gap:6px;font-size:11px;padding:3px 10px;border-radius:999px;border:1px solid var(--border);background:var(--panel);color:var(--dim);font-weight:600;white-space:nowrap}
+.statuspill{display:inline-flex;align-items:center;gap:6px;font-size:11px;padding:3px 10px;border-radius:999px;border:1px solid var(--border);background:var(--panel);color:var(--dim);font-weight:600;white-space:nowrap;cursor:pointer}
.statuspill.ok{color:var(--ok);border-color:color-mix(in srgb,var(--ok) 45%,transparent);background:color-mix(in srgb,var(--ok) 12%,transparent)}
.statuspill.err{color:var(--err);border-color:color-mix(in srgb,var(--err) 45%,transparent);background:color-mix(in srgb,var(--err) 12%,transparent)}
.statuspill.loading{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 45%,transparent);background:color-mix(in srgb,var(--warn) 12%,transparent)}
diff --git a/internal/jean/web_api.go b/internal/jean/web_api.go
index cd761f3..b0d3cf0 100644
--- a/internal/jean/web_api.go
+++ b/internal/jean/web_api.go
@@ -44,6 +44,20 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
})
}
+// handleServiceLog (GET /api/service/log) : dernières lignes du journal du
+// service, pour que l'UI puisse montrer POURQUOI le modèle ne se charge pas
+// (fichier .gguf illisible, VRAM insuffisante, bibliothèque manquante…) au lieu
+// d'un « chargement… » perpétuel sans explication.
+func handleServiceLog(w http.ResponseWriter, r *http.Request) {
+ n := 80
+ if v := r.URL.Query().Get("n"); v != "" {
+ if k, err := strconv.Atoi(v); err == nil && k > 0 && k <= 500 {
+ n = k
+ }
+ }
+ sendJSON(w, 200, map[string]any{"log": serviceLogTail(n)})
+}
+
func handleVram(w http.ResponseWriter, r *http.Request) {
out, err := hideCmd(exec.Command("nvidia-smi",
"--query-gpu=name,memory.used,memory.total,utilization.gpu,temperature.gpu",
diff --git a/internal/jean/web_server.go b/internal/jean/web_server.go
index 2b50d48..15b0bb6 100644
--- a/internal/jean/web_server.go
+++ b/internal/jean/web_server.go
@@ -78,6 +78,7 @@ func newWebMux() *http.ServeMux {
api := func(path string, h http.HandlerFunc) { mux.HandleFunc(path, requireWebAuth(h)) }
api("/api/ping", handlePing)
api("/api/status", handleStatus)
+ api("/api/service/log", handleServiceLog) // journal du service pour diagnostiquer un modèle qui ne charge pas
api("/api/vram", handleVram)
api("/api/ram", handleRam)
api("/api/config", handleConfigEnv)