diff --git a/core/pom.xml b/core/pom.xml
index 2d55dde..a64c44c 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -14,7 +14,7 @@
com.loremind
loremind-core
- 0.6.9
+ 0.6.10
LoreMind Core
Backend Core - Architecture Hexagonale
diff --git a/core/src/main/java/com/loremind/infrastructure/web/controller/SettingsController.java b/core/src/main/java/com/loremind/infrastructure/web/controller/SettingsController.java
index c0ecfe0..a6b7557 100644
--- a/core/src/main/java/com/loremind/infrastructure/web/controller/SettingsController.java
+++ b/core/src/main/java/com/loremind/infrastructure/web/controller/SettingsController.java
@@ -21,12 +21,10 @@ import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBo
import java.io.IOException;
import java.io.InputStream;
-import java.io.OutputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
-import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Map;
@@ -46,13 +44,16 @@ public class SettingsController {
private final RestTemplate restTemplate;
private final String brainBaseUrl;
+ private final String brainInternalSecret;
private final boolean demoMode;
public SettingsController(RestTemplate restTemplate,
@Value("${brain.base-url}") String brainBaseUrl,
+ @Value("${brain.internal-secret}") String brainInternalSecret,
@Value("${app.demo-mode:false}") boolean demoMode) {
this.restTemplate = restTemplate;
this.brainBaseUrl = brainBaseUrl;
+ this.brainInternalSecret = brainInternalSecret;
this.demoMode = demoMode;
}
@@ -93,12 +94,18 @@ public class SettingsController {
HttpClient http = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
- HttpRequest req = HttpRequest.newBuilder()
+ // Le RestTemplate auto-injecte X-Internal-Secret via un interceptor,
+ // mais on bypass RestTemplate pour le streaming -> on doit ajouter
+ // l'entete a la main, sinon le Brain repond 401.
+ HttpRequest.Builder reqBuilder = HttpRequest.newBuilder()
.uri(URI.create(brainBaseUrl + "/models/ollama/pull"))
.timeout(Duration.ofMinutes(60))
.header("Content-Type", "application/json")
- .POST(HttpRequest.BodyPublishers.ofString(toJson(body)))
- .build();
+ .POST(HttpRequest.BodyPublishers.ofString(toJson(body)));
+ if (brainInternalSecret != null && !brainInternalSecret.isBlank()) {
+ reqBuilder.header("X-Internal-Secret", brainInternalSecret);
+ }
+ HttpRequest req = reqBuilder.build();
try {
HttpResponse resp = http.send(req, HttpResponse.BodyHandlers.ofInputStream());
try (InputStream in = resp.body()) {
diff --git a/installers/install.ps1 b/installers/install.ps1
index 6a2144a..dd42200 100644
--- a/installers/install.ps1
+++ b/installers/install.ps1
@@ -40,7 +40,7 @@
Auteur : ietm64
Licence : AGPL-3.0
Projet : LoreMindMJ - assistant pour Maitres de Jeu de JDR
- Version : 0.6.9
+ Version : 0.6.10
.LINK
https://git.igmlcreation.fr/ietm64/loremind
diff --git a/web/package-lock.json b/web/package-lock.json
index c9dbf2a..2c1faa0 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "loremind-web",
- "version": "0.6.9",
+ "version": "0.6.10",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "loremind-web",
- "version": "0.6.9",
+ "version": "0.6.10",
"dependencies": {
"@angular/animations": "^17.0.0",
"@angular/common": "^17.0.0",
diff --git a/web/package.json b/web/package.json
index 11ea7cd..e8348b7 100644
--- a/web/package.json
+++ b/web/package.json
@@ -1,6 +1,6 @@
{
"name": "loremind-web",
- "version": "0.6.9",
+ "version": "0.6.10",
"description": "LoreMind Frontend - Angular",
"scripts": {
"ng": "ng",
diff --git a/web/src/app/services/settings.service.ts b/web/src/app/services/settings.service.ts
index 49b9705..05ac9d4 100644
--- a/web/src/app/services/settings.service.ts
+++ b/web/src/app/services/settings.service.ts
@@ -1,4 +1,4 @@
-import { Injectable } from '@angular/core';
+import { Injectable, NgZone } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@@ -43,7 +43,7 @@ export class SettingsService {
// suivants en cross-origin (dev Angular sur :4200 -> core sur :8080).
private readonly authOptions = { withCredentials: true };
- constructor(private http: HttpClient) {}
+ constructor(private http: HttpClient, private zone: NgZone) {}
getSettings(): Observable {
return this.http.get(this.apiUrl, this.authOptions);
@@ -78,8 +78,16 @@ export class SettingsService {
* bufferise les reponses XHR, ce qui empeche le streaming en temps reel.
*/
pullOllamaModel(name: string): Observable {
+ // fetch() s'execute hors zone Angular -> les emissions du subscriber
+ // doivent etre forcees dans la zone via NgZone.run() pour que le change
+ // detection se declenche et que la barre de progression se mette a jour
+ // en temps reel.
return new Observable((subscriber) => {
const controller = new AbortController();
+ const emit = (ev: OllamaPullEvent) => this.zone.run(() => subscriber.next(ev));
+ const fail = (e: unknown) => this.zone.run(() => subscriber.error(e));
+ const done = () => this.zone.run(() => subscriber.complete());
+
(async () => {
try {
const response = await fetch(`${this.apiUrl}/models/ollama/pull`, {
@@ -89,16 +97,22 @@ export class SettingsService {
body: JSON.stringify({ name }),
signal: controller.signal,
});
- if (!response.ok || !response.body) {
- subscriber.error(new Error(`HTTP ${response.status}`));
+ if (!response.ok) {
+ const text = await response.text();
+ fail(new Error(`HTTP ${response.status} : ${text || 'reponse vide'}`));
+ return;
+ }
+ if (!response.body) {
+ fail(new Error('Reponse sans corps : streaming non supporte par le navigateur'));
return;
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
+ let eventsReceived = 0;
while (true) {
- const { value, done } = await reader.read();
- if (done) break;
+ const { value, done: streamDone } = await reader.read();
+ if (streamDone) break;
buffer += decoder.decode(value, { stream: true });
// Decoupage NDJSON : chaque ligne est un objet JSON complet.
let nl: number;
@@ -107,15 +121,27 @@ export class SettingsService {
buffer = buffer.slice(nl + 1);
if (!line) continue;
try {
- subscriber.next(JSON.parse(line) as OllamaPullEvent);
+ emit(JSON.parse(line) as OllamaPullEvent);
+ eventsReceived++;
} catch {
// Ligne non JSON (rare) : on l'ignore.
}
}
}
- subscriber.complete();
+ // Reste eventuel dans le buffer (derniere ligne sans '\n').
+ if (buffer.trim()) {
+ try {
+ emit(JSON.parse(buffer.trim()) as OllamaPullEvent);
+ eventsReceived++;
+ } catch { /* ignore */ }
+ }
+ if (eventsReceived === 0) {
+ fail(new Error('Aucun evenement recu du serveur. Verifiez les logs du Brain (docker logs loremind-brain).'));
+ return;
+ }
+ done();
} catch (err) {
- if ((err as Error).name !== 'AbortError') subscriber.error(err);
+ if ((err as Error).name !== 'AbortError') fail(err);
}
})();
return () => controller.abort();