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();