diff --git a/docker-compose.unraid.yml b/docker-compose.unraid.yml
index 8b8884e..4163fc9 100644
--- a/docker-compose.unraid.yml
+++ b/docker-compose.unraid.yml
@@ -34,6 +34,11 @@ services:
# lancement). Ex. LOKI_MODEL=Qwen3-14B-Q4_K_M.gguf après dépôt du
# fichier dans /mnt/user/appdata/loki/models.
- LOKI_MODEL=
+ # Accès par NOM DE DOMAINE (reverse proxy, ex. loki.mondomaine.fr) sans
+ # clé de pilotage : liste ici le(s) nom(s), séparés par des virgules —
+ # sinon l'API répond 403 (protection contre les sites tiers / DNS
+ # rebinding). Inutile pour un accès par IP ou nom local.
+ - LOKI_TRUSTED_HOSTS=
volumes:
# /data : config, base, mémoire, workspace, modèles téléchargés par l'UI
- /mnt/user/appdata/loki/data:/data
diff --git a/docker-compose.yml b/docker-compose.yml
index 8aab18d..c6fce55 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -22,6 +22,9 @@ services:
- LOKI_MODEL=${LOKI_MODEL:-}
- LOKI_CTX=${LOKI_CTX:-}
- LOKI_NGL=${LOKI_NGL:-}
+ # Accès par nom de domaine (reverse proxy) sans clé de pilotage : nom(s)
+ # autorisé(s), séparés par des virgules — sinon l'API répond 403.
+ - LOKI_TRUSTED_HOSTS=${LOKI_TRUSTED_HOSTS:-}
volumes:
# /data = LOKI_HOME : config, base bbolt, mémoire, workspace, modèles téléchargés
- ./data:/data
diff --git a/internal/loki/ui/index.html b/internal/loki/ui/index.html
index 62c8aa1..b3650f4 100644
--- a/internal/loki/ui/index.html
+++ b/internal/loki/ui/index.html
@@ -3919,8 +3919,31 @@ async function jfetch(u, opts){
const k = await askKeyOnce();
if(k){ opts.headers = authHeaders(opts.headers); r = await fetch(u, opts); }
}
+ // 403 de la garde anti-site-tiers (web_auth.go, crossSiteReject) : sans ce
+ // bandeau, l'interface restait vide sans dire pourquoi — chaque appel échouait
+ // en silence dans la console.
+ if(r.status === 403 && !accessBlockedShown){
+ r.clone().json().then(j=>{ if(j && j.error) showAccessBlocked(j.error); }).catch(()=>{});
+ }
return r;
}
+
+// showAccessBlocked : bandeau fixe expliquant le refus d'accès et comment le
+// lever (nom de domaine derrière un reverse proxy, sans clé de pilotage).
+let accessBlockedShown = false;
+function showAccessBlocked(msg){
+ if(accessBlockedShown) return;
+ accessBlockedShown = true;
+ const d = document.createElement('div');
+ d.id = 'access-blocked';
+ d.setAttribute('role', 'alert');
+ d.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:9999;padding:12px 16px;background:var(--err,#b3564d);color:#fff;font-size:13px;line-height:1.5';
+ const host = location.hostname;
+ d.innerHTML = 'Accès refusé par Loki — ' + escHtml(msg) +
+ '
Derrière un reverse proxy : ajoute LOKI_TRUSTED_HOSTS=' + escHtml(host) +
+ ' aux variables du conteneur puis recrée-le, ou définis une clé de pilotage (docker exec -it loki loki set-web-key).';
+ document.body.appendChild(d);
+}
// askKeyOnce : UNE seule demande de clé à la fois, partagée par tous les appels.
//
// Le chargement de l'UI lance une dizaine d'appels /api/* en parallèle, et le
diff --git a/internal/loki/ui/src/js/04-api.js b/internal/loki/ui/src/js/04-api.js
index da2b611..d7cbdd7 100644
--- a/internal/loki/ui/src/js/04-api.js
+++ b/internal/loki/ui/src/js/04-api.js
@@ -33,8 +33,31 @@ async function jfetch(u, opts){
const k = await askKeyOnce();
if(k){ opts.headers = authHeaders(opts.headers); r = await fetch(u, opts); }
}
+ // 403 de la garde anti-site-tiers (web_auth.go, crossSiteReject) : sans ce
+ // bandeau, l'interface restait vide sans dire pourquoi — chaque appel échouait
+ // en silence dans la console.
+ if(r.status === 403 && !accessBlockedShown){
+ r.clone().json().then(j=>{ if(j && j.error) showAccessBlocked(j.error); }).catch(()=>{});
+ }
return r;
}
+
+// showAccessBlocked : bandeau fixe expliquant le refus d'accès et comment le
+// lever (nom de domaine derrière un reverse proxy, sans clé de pilotage).
+let accessBlockedShown = false;
+function showAccessBlocked(msg){
+ if(accessBlockedShown) return;
+ accessBlockedShown = true;
+ const d = document.createElement('div');
+ d.id = 'access-blocked';
+ d.setAttribute('role', 'alert');
+ d.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:9999;padding:12px 16px;background:var(--err,#b3564d);color:#fff;font-size:13px;line-height:1.5';
+ const host = location.hostname;
+ d.innerHTML = 'Accès refusé par Loki — ' + escHtml(msg) +
+ '
Derrière un reverse proxy : ajoute LOKI_TRUSTED_HOSTS=' + escHtml(host) +
+ ' aux variables du conteneur puis recrée-le, ou définis une clé de pilotage (docker exec -it loki loki set-web-key).';
+ document.body.appendChild(d);
+}
// askKeyOnce : UNE seule demande de clé à la fois, partagée par tous les appels.
//
// Le chargement de l'UI lance une dizaine d'appels /api/* en parallèle, et le