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

- … + … +
+ +
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

- … + … +
+ +
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)