Compare commits

..

10 Commits

Author SHA1 Message Date
5bc038acd8 Suppression de la sélection du langage au démarrage de l'appli : maintenant c'est le système qui décide de la langue.
Some checks failed
Tests unitaires / Brain (Python · pytest + couverture) (push) Successful in 20s
Tests unitaires / Web (Angular · vitest + couverture) (push) Successful in 33s
Tests unitaires / Core (Java · mvn test + JaCoCo) (push) Successful in 2m30s
Build & Push Images / build (brain) (push) Successful in 1m22s
E2E Tests / e2e (push) Failing after 5m47s
Build & Push Images / build (core) (push) Successful in 3m29s
Build & Push Images / build-switcher (push) Successful in 48s
Build & Push Images / build (web) (push) Successful in 1m55s
Changement relicat Loremind -> DM Loremind
Passage en 0.18.0 stable publique
2026-06-22 16:59:55 +02:00
f347dcd357 Passage à la 0.17.3 et ajout d'un splash screen pour le démarrage de l'application
Some checks failed
E2E Tests / e2e (push) Has been cancelled
Tests unitaires / Brain (Python · pytest + couverture) (push) Successful in 18s
Tests unitaires / Web (Angular · vitest + couverture) (push) Successful in 28s
Build & Push Images / build (brain) (push) Successful in 1m11s
Tests unitaires / Core (Java · mvn test + JaCoCo) (push) Successful in 2m9s
Build & Push Images / build (web) (push) Successful in 1m48s
Build & Push Images / build-switcher (push) Successful in 44s
Build & Push Images / build (core) (push) Successful in 3m2s
2026-06-22 11:34:55 +02:00
46fea8d53c On arrête de mettre appimagetool dans la release - actif après la release 0.17.2
Some checks failed
E2E Tests / e2e (push) Has been cancelled
Tests unitaires / Brain (Python · pytest + couverture) (push) Successful in 22s
Tests unitaires / Web (Angular · vitest + couverture) (push) Successful in 25s
Tests unitaires / Core (Java · mvn test + JaCoCo) (push) Successful in 1m55s
2026-06-22 11:15:41 +02:00
e05b26563f passage v0.17.2 release officielle
Some checks failed
Tests unitaires / Brain (Python · pytest + couverture) (push) Successful in 20s
Tests unitaires / Web (Angular · vitest + couverture) (push) Successful in 28s
Tests unitaires / Core (Java · mvn test + JaCoCo) (push) Successful in 2m22s
Build & Push Images / build (brain) (push) Successful in 1m20s
E2E Tests / e2e (push) Has been cancelled
Build & Push Images / build (web) (push) Successful in 1m52s
Build & Push Images / build (core) (push) Successful in 3m20s
Build & Push Images / build-switcher (push) Successful in 43s
2026-06-22 10:37:00 +02:00
dc66199177 Passage v0.17.2-beta suite au changement de l'installeur
All checks were successful
Build & Push Images / build (brain) (push) Successful in 1m25s
Build & Push Images / build (core) (push) Successful in 3m20s
Build & Push Images / build (web) (push) Successful in 1m56s
Build & Push Images / build-switcher (push) Successful in 19s
2026-06-22 09:51:26 +02:00
b133d0f16d Correction dans l'installeur : ajout d'une case pour pouvoir lancer le programme automatiquement à la fin de l'installation (sous windows) afin d'éviter que l'utilisateur ne soit perturbé 2026-06-22 09:49:33 +02:00
024d37dea6 Correction des worflow pour linux
All checks were successful
Build & Push Images / build (brain) (push) Successful in 1m20s
Build & Push Images / build (web) (push) Successful in 1m44s
Build & Push Images / build (core) (push) Successful in 3m12s
Build & Push Images / build-switcher (push) Successful in 16s
2026-06-20 11:16:46 +02:00
997aadf5b5 Mise en place de la distribution en .appimage pour les distro linux
All checks were successful
Build & Push Images / build (brain) (push) Successful in 1m21s
Build & Push Images / build (web) (push) Successful in 1m49s
Build & Push Images / build (core) (push) Successful in 3m22s
Build & Push Images / build-switcher (push) Successful in 19s
2026-06-20 11:04:15 +02:00
560c07d5c3 Changement du logo pour l'application et changement du nom en "DM Loremind" pour le tittre de l'application.
Some checks failed
Tests unitaires / Brain (Python · pytest + couverture) (push) Successful in 20s
Tests unitaires / Web (Angular · vitest + couverture) (push) Successful in 29s
Tests unitaires / Core (Java · mvn test + JaCoCo) (push) Successful in 2m17s
Build & Push Images / build (brain) (push) Successful in 1m13s
E2E Tests / e2e (push) Failing after 5m22s
Build & Push Images / build (core) (push) Successful in 3m25s
Build & Push Images / build-switcher (push) Successful in 51s
Build & Push Images / build (web) (push) Successful in 1m57s
2026-06-19 10:00:13 +02:00
5aa08a3a27 Correction du worflow release : on ne fait plus les tests (évite le doublons de test avec le ci.yml).
Some checks failed
Tests unitaires / Brain (Python · pytest + couverture) (push) Successful in 21s
Tests unitaires / Web (Angular · vitest + couverture) (push) Successful in 30s
Tests unitaires / Core (Java · mvn test + JaCoCo) (push) Successful in 2m22s
Build & Push Images / build (brain) (push) Successful in 1m16s
E2E Tests / e2e (push) Failing after 5m24s
Build & Push Images / build (core) (push) Successful in 3m5s
Build & Push Images / build-switcher (push) Successful in 50s
Build & Push Images / build (web) (push) Successful in 1m56s
Correction de tests coté java + config
Suppression de classes inutilisées coté Angular
2026-06-19 00:36:30 +02:00
62 changed files with 750 additions and 302 deletions

View File

@@ -12,71 +12,10 @@ env:
GHCR_NAMESPACE: igmlcreation GHCR_NAMESPACE: igmlcreation
jobs: jobs:
# GATE : aucune image n'est build/push tant que les 3 suites unitaires # NB : pas de job de test ici. Le gate qualité vit dans ci.yml (push main + PR) ;
# (Java / Python / Angular) ne passent pas. Un tag posé sur un commit aux # avec la branch protection Gitea (checks requis sur main), on ne tague donc que
# tests rouges ne publiera donc PAS d'images. # du code déjà vert. release.yml ne fait que builder/pousser les images.
tests:
runs-on: ubuntu-latest
# Base PostgreSQL réelle pour les tests Core (joignable par le nom `postgres`
# depuis le conteneur du job Gitea).
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: loremind_test
POSTGRES_USER: loremind_test
POSTGRES_PASSWORD: loremind_test
options: >-
--health-cmd "pg_isready -U loremind_test -d loremind_test"
--health-interval 10s
--health-timeout 5s
--health-retries 10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
cache: maven
- name: Core — mvn test (+ JaCoCo check)
working-directory: core
env:
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/loremind_test
SPRING_DATASOURCE_USERNAME: loremind_test
SPRING_DATASOURCE_PASSWORD: loremind_test
run: |
chmod +x ./mvnw
./mvnw -B test
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: pip
cache-dependency-path: brain/requirements-dev.txt
- name: Brain — pytest (+ couverture)
working-directory: brain
run: |
pip install -r requirements-dev.txt
pytest --cov=app --cov-report=term-missing --cov-fail-under=50
- name: Set up Node 20
uses: actions/setup-node@v4
with:
node-version: '20'
cache: npm
cache-dependency-path: web/package-lock.json
- name: Web — vitest (+ couverture)
working-directory: web
run: |
npm ci --no-audit --no-fund
npm run test:unit:coverage
build: build:
needs: tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false fail-fast: false
@@ -157,7 +96,6 @@ jobs:
# donc uniquement sur les releases stables — pas la peine de re-publier # donc uniquement sur les releases stables — pas la peine de re-publier
# une variante beta du switcher, c'est une infrastructure neutre. # une variante beta du switcher, c'est une infrastructure neutre.
build-switcher: build-switcher:
needs: tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout

View File

@@ -1,12 +1,12 @@
name: Desktop installers name: Desktop installers
# Produit les installeurs de BUREAU (.msi Windows pour l'instant) et les publie # Produit les installeurs de BUREAU (.msi Windows + AppImage Linux) et les publie
# en tant qu'assets d'une GitHub Release, sur tag `v*`. # en tant qu'assets d'une GitHub Release, sur tag `v*`.
# #
# Complementaire au pipeline Gitea Actions (.gitea/workflows/release.yml) qui, # Complementaire au pipeline Gitea Actions (.gitea/workflows/release.yml) qui,
# lui, build et pousse les IMAGES Docker. Ici on est sur GitHub car jpackage et # lui, build et pousse les IMAGES Docker. Ici on est sur GitHub car jpackage ne
# PyInstaller ne savent PAS cross-compiler : le .msi DOIT etre construit sur un # sait PAS cross-compiler : le .msi DOIT etre construit sur un runner Windows et
# runner Windows, et GitHub en fournit gratuitement (windows-latest). # l'AppImage sur un runner Linux — GitHub fournit les deux gratuitement.
# #
# Prerequis : le depot Gitea doit etre mirrore vers GitHub (push mirror, tags # Prerequis : le depot Gitea doit etre mirrore vers GitHub (push mirror, tags
# inclus) pour que le tag declenche ce workflow. # inclus) pour que le tag declenche ce workflow.
@@ -191,6 +191,72 @@ jobs:
path: core/target/dist-out/*.msi path: core/target/dist-out/*.msi
retention-days: 90 retention-days: 90
# TODO (plus tard) : job `linux` sur ubuntu-latest produisant un AppImage # LINUX : AppImage (1 fichier, toutes distros) attache a la MEME release.
# (jpackage --type app-image + appimagetool) + PyInstaller Linux du Brain, # Tourne sur ubuntu-latest (jpackage ne cross-compile pas -> build natif Linux).
# attache a la MEME release. Reutilise la meme matrice / les memes etapes. # Brain empaquete via python-build-standalone (pas de Python embeddable Linux
# officiel ; pas de PyInstaller -> pas de faux positif AV). Memes regles de
# publication que windows : stable -> release publique ; beta -> artefact prive.
linux:
needs: tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'workflow_dispatch' && format('v{0}', inputs.version) || github.ref }}
# Apporte jpackage (empaquetage natif) dans le PATH.
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: '20'
# Pas de setup-python : le Brain utilise SON python-build-standalone
# (telecharge par le script, avec son propre pip). Le host n'en a pas besoin.
# Version (numerique X.Y.Z) + tag + isbeta, comme le job windows.
- name: Derive version
id: ver
run: |
if [ '${{ github.event_name }}' = 'workflow_dispatch' ]; then
raw='${{ inputs.version }}'
else
raw='${{ github.ref_name }}'
fi
raw="${raw#v}" # 0.15.0 ou 0.15.0-beta
num="${raw%%-*}" # 0.15.0
case "$raw" in *-beta*) isbeta=true ;; *) isbeta=false ;; esac
echo "version=$num" >> "$GITHUB_OUTPUT"
echo "tag=v$raw" >> "$GITHUB_OUTPUT"
echo "isbeta=$isbeta" >> "$GITHUB_OUTPUT"
- name: Build Linux AppImage
env:
# Authentifie l'appel API GitHub (resolution python-build-standalone) :
# evite le rate limit 60/h anonyme des runners partages.
GITHUB_TOKEN: ${{ github.token }}
run: bash ./installers/desktop/build-linux.sh --version ${{ steps.ver.outputs.version }}
# STABLE uniquement : attache l'AppImage a la release publique (meme tag que
# le .msi -> les deux installeurs sur la meme release).
- name: Publish AppImage to GitHub Release (stable)
if: ${{ steps.ver.outputs.isbeta == 'false' }}
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.ver.outputs.tag }}
files: core/target/dist-out/DM_Loremind*.AppImage
fail_on_unmatched_files: true
# BETA uniquement : artefact PRIVE (pas de release publique).
- name: Upload AppImage as private artifact (beta)
if: ${{ steps.ver.outputs.isbeta == 'true' }}
uses: actions/upload-artifact@v4
with:
name: loremind-beta-${{ steps.ver.outputs.version }}-appimage
path: core/target/dist-out/DM_Loremind*.AppImage
retention-days: 90

View File

@@ -26,7 +26,7 @@ from app.infrastructure.ollama_model_installer import ensure_ollama_embedding_mo
app = FastAPI( app = FastAPI(
title="LoreMind Brain", title="LoreMind Brain",
description="Backend IA pour la génération de contenu narratif.", description="Backend IA pour la génération de contenu narratif.",
version="0.16.2", version="0.18.0",
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@@ -23,7 +23,11 @@ sys.path.insert(0, _HERE)
# AVANT que l'app n'importe le pdf_extractor (qui détecte la version au chargement). # AVANT que l'app n'importe le pdf_extractor (qui détecte la version au chargement).
# tessdata (fra+eng) est embarqué dans tesseract/tessdata. Sans ce bloc, l'OCR # tessdata (fra+eng) est embarqué dans tesseract/tessdata. Sans ce bloc, l'OCR
# reste désactivé en dégradation gracieuse (PDF born-digital OK, scans signalés). # reste désactivé en dégradation gracieuse (PDF born-digital OK, scans signalés).
_TESS = os.path.join(_HERE, "tesseract", "tesseract.exe") # Binaire selon l'OS : tesseract.exe (Windows embeddable) ou tesseract (Linux/Mac).
# Si aucun Tesseract n'est bundlé (cas Linux/AppImage par défaut), le bloc est
# sauté et pytesseract retombe sur le tesseract SYSTÈME du PATH (ex. apt install
# tesseract-ocr) — sinon OCR désactivé en dégradation gracieuse.
_TESS = os.path.join(_HERE, "tesseract", "tesseract.exe" if os.name == "nt" else "tesseract")
if os.path.exists(_TESS): if os.path.exists(_TESS):
os.environ.setdefault("TESSDATA_PREFIX", os.path.join(_HERE, "tesseract")) os.environ.setdefault("TESSDATA_PREFIX", os.path.join(_HERE, "tesseract"))
try: try:

View File

@@ -14,7 +14,7 @@
<groupId>com.loremind</groupId> <groupId>com.loremind</groupId>
<artifactId>loremind-core</artifactId> <artifactId>loremind-core</artifactId>
<version>0.16.2</version> <version>0.18.0</version>
<name>LoreMind Core</name> <name>LoreMind Core</name>
<description>Backend Core - Architecture Hexagonale</description> <description>Backend Core - Architecture Hexagonale</description>
@@ -152,6 +152,45 @@
<build> <build>
<plugins> <plugins>
<!-- Declare EXPLICITEMENT le processor d'annotations Lombok : sinon javac
avertit que l'annotation processing implicite (processeur trouve sur le
classpath sans etre declare) sera desactivee dans un futur JDK. -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<!-- Expose le chemin du jar mockito-core dans la propriete
${org.mockito:mockito-core:jar}, consommee par surefire ci-dessous. -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>properties</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Charge Mockito comme un VRAI -javaagent (au lieu de l'auto-attachement
dynamique de byte-buddy, qui imprime un avertissement et sera interdit
dans un futur JDK). `@{argLine}` preserve l'argLine pose par JaCoCo. -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>@{argLine} -javaagent:${org.mockito:mockito-core:jar} -Xshare:off</argLine>
</configuration>
</plugin>
<plugin> <plugin>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId> <artifactId>spring-boot-maven-plugin</artifactId>

View File

@@ -225,7 +225,7 @@ public class BrainRulesImportClient implements RulesPdfImporter {
private Map<String, String> toStringMap(JsonNode object) { private Map<String, String> toStringMap(JsonNode object) {
Map<String, String> out = new LinkedHashMap<>(); Map<String, String> out = new LinkedHashMap<>();
if (object != null && object.isObject()) { if (object != null && object.isObject()) {
object.fields().forEachRemaining(e -> out.put(e.getKey(), e.getValue().asText())); object.properties().forEach(e -> out.put(e.getKey(), e.getValue().asText()));
} }
return out; return out;
} }

View File

@@ -37,8 +37,8 @@ public class RestTemplateConfig {
@Value("${brain.timeout-seconds}") long timeoutSeconds, @Value("${brain.timeout-seconds}") long timeoutSeconds,
@Value("${brain.internal-secret}") String internalSecret) { @Value("${brain.internal-secret}") String internalSecret) {
return builder return builder
.setConnectTimeout(Duration.ofSeconds(10)) .connectTimeout(Duration.ofSeconds(10))
.setReadTimeout(Duration.ofSeconds(timeoutSeconds)) .readTimeout(Duration.ofSeconds(timeoutSeconds))
.additionalInterceptors((request, body, execution) -> { .additionalInterceptors((request, body, execution) -> {
if (internalSecret != null && !internalSecret.isBlank()) { if (internalSecret != null && !internalSecret.isBlank()) {
request.getHeaders().set(INTERNAL_SECRET_HEADER, internalSecret); request.getHeaders().set(INTERNAL_SECRET_HEADER, internalSecret);
@@ -61,8 +61,8 @@ public class RestTemplateConfig {
@Value("${brain.import-timeout-seconds:600}") long importTimeoutSeconds, @Value("${brain.import-timeout-seconds:600}") long importTimeoutSeconds,
@Value("${brain.internal-secret}") String internalSecret) { @Value("${brain.internal-secret}") String internalSecret) {
return builder return builder
.setConnectTimeout(Duration.ofSeconds(10)) .connectTimeout(Duration.ofSeconds(10))
.setReadTimeout(Duration.ofSeconds(importTimeoutSeconds)) .readTimeout(Duration.ofSeconds(importTimeoutSeconds))
.additionalInterceptors((request, body, execution) -> { .additionalInterceptors((request, body, execution) -> {
if (internalSecret != null && !internalSecret.isBlank()) { if (internalSecret != null && !internalSecret.isBlank()) {
request.getHeaders().set(INTERNAL_SECRET_HEADER, internalSecret); request.getHeaders().set(INTERNAL_SECRET_HEADER, internalSecret);

View File

@@ -7,6 +7,8 @@ import org.springframework.context.annotation.Profile;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.awt.SplashScreen;
/** /**
* En mode bureau (profil "local"), ouvre le navigateur par defaut sur * En mode bureau (profil "local"), ouvre le navigateur par defaut sur
* l'application des que le serveur est pret. L'app n'ayant pas de fenetre * l'application des que le serveur est pret. L'app n'ayant pas de fenetre
@@ -25,7 +27,26 @@ public class DesktopBrowserOpener {
@EventListener(ApplicationReadyEvent.class) @EventListener(ApplicationReadyEvent.class)
public void onReady() { public void onReady() {
// Ferme le splash natif (affiche par la JVM via -splash des le double-clic)
// juste avant d'ouvrir le navigateur : le relais visuel est assure.
closeSplash();
log.info("[Desktop] Application prete — ouverture du navigateur."); log.info("[Desktop] Application prete — ouverture du navigateur.");
DesktopSingleInstance.openAppInBrowser(); DesktopSingleInstance.openAppInBrowser();
} }
/**
* Ferme l'ecran de demarrage si l'app a ete lancee avec {@code -splash:...}
* (cas des installeurs jpackage). No-op silencieux sinon (lancement dev sans
* splash, ou environnement sans affichage).
*/
private void closeSplash() {
try {
SplashScreen splash = SplashScreen.getSplashScreen();
if (splash != null) {
splash.close();
}
} catch (Exception e) {
log.debug("[Desktop] Pas de splash a fermer : {}", e.getMessage());
}
}
} }

View File

@@ -50,8 +50,8 @@ public class DesktopUpdateService {
@Value("${desktop.update.releases-api-url:https://api.github.com/repos/IGMLcreation/LoreMind/releases/latest}") String releasesApiUrl, @Value("${desktop.update.releases-api-url:https://api.github.com/repos/IGMLcreation/LoreMind/releases/latest}") String releasesApiUrl,
@Nullable BuildProperties buildProperties) { @Nullable BuildProperties buildProperties) {
this.http = builder this.http = builder
.setConnectTimeout(Duration.ofSeconds(5)) .connectTimeout(Duration.ofSeconds(5))
.setReadTimeout(Duration.ofSeconds(10)) .readTimeout(Duration.ofSeconds(10))
.build(); .build();
this.enabled = enabled; this.enabled = enabled;
this.releasesApiUrl = releasesApiUrl; this.releasesApiUrl = releasesApiUrl;

View File

@@ -20,6 +20,9 @@ import java.awt.RenderingHints;
import java.awt.SystemTray; import java.awt.SystemTray;
import java.awt.TrayIcon; import java.awt.TrayIcon;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import javax.imageio.ImageIO;
/** /**
* Icone dans la zone de notification (barre des taches) en mode bureau * Icone dans la zone de notification (barre des taches) en mode bureau
@@ -63,7 +66,7 @@ public class SystemTrayManager {
try { try {
popup = new PopupMenu(); popup = new PopupMenu();
MenuItem open = new MenuItem("Ouvrir LoreMind"); MenuItem open = new MenuItem("Ouvrir DM Loremind");
open.addActionListener(e -> DesktopSingleInstance.openAppInBrowser()); open.addActionListener(e -> DesktopSingleInstance.openAppInBrowser());
popup.add(open); popup.add(open);
@@ -79,17 +82,17 @@ public class SystemTrayManager {
popup.add(editConfig); popup.add(editConfig);
// Acces au dossier de donnees (~/.loremind : base, images, config…). // Acces au dossier de donnees (~/.loremind : base, images, config…).
MenuItem openFolder = new MenuItem("Ouvrir le dossier LoreMind"); MenuItem openFolder = new MenuItem("Ouvrir le dossier DM Loremind");
openFolder.addActionListener(e -> DesktopSingleInstance.openFolder(DesktopUserConfig.getHomeDir())); openFolder.addActionListener(e -> DesktopSingleInstance.openFolder(DesktopUserConfig.getHomeDir()));
popup.add(openFolder); popup.add(openFolder);
popup.addSeparator(); popup.addSeparator();
MenuItem quit = new MenuItem("Quitter LoreMind"); MenuItem quit = new MenuItem("Quitter DM Loremind");
quit.addActionListener(e -> quit()); quit.addActionListener(e -> quit());
popup.add(quit); popup.add(quit);
trayIcon = new TrayIcon(createIcon(), "LoreMind", popup); trayIcon = new TrayIcon(createIcon(), "DM Loremind", popup);
trayIcon.setImageAutoSize(true); trayIcon.setImageAutoSize(true);
// Double-clic sur l'icone : ouvre l'application dans le navigateur. // Double-clic sur l'icone : ouvre l'application dans le navigateur.
trayIcon.addActionListener(e -> DesktopSingleInstance.openAppInBrowser()); trayIcon.addActionListener(e -> DesktopSingleInstance.openAppInBrowser());
@@ -122,7 +125,7 @@ public class SystemTrayManager {
popup.insertSeparator(1); popup.insertSeparator(1);
trayIcon.displayMessage( trayIcon.displayMessage(
"LoreMind — mise a jour disponible", "DM Loremind — mise a jour disponible",
"Version " + info.latestVersion() + " disponible (vous avez " + info.currentVersion() "Version " + info.latestVersion() + " disponible (vous avez " + info.currentVersion()
+ "). Menu de l'icone → Telecharger.", + "). Menu de l'icone → Telecharger.",
TrayIcon.MessageType.INFO); TrayIcon.MessageType.INFO);
@@ -151,19 +154,35 @@ public class SystemTrayManager {
} }
/** /**
* Genere une petite icone (carre arrondi violet « L ») sans dependre d'un * Icone du systray : charge l'image de marque {@code /tray-icon.png} depuis le
* fichier image — robuste quel que soit l'empaquetage. * classpath (embarquee dans le jar). Repli sur une icone dessinee si le fichier
* est absent ou illisible — l'application reste fonctionnelle dans tous les cas.
*/ */
private Image createIcon() { private Image createIcon() {
try (InputStream in = getClass().getResourceAsStream("/tray-icon.png")) {
if (in != null) {
BufferedImage loaded = ImageIO.read(in);
if (loaded != null) {
return loaded;
}
}
} catch (IOException e) {
// ignore : on retombe sur l'icone dessinee ci-dessous
}
return drawFallbackIcon();
}
/** Icone de repli (carre arrondi violet « DM ») si l'image embarquee manque. */
private Image drawFallbackIcon() {
int size = 16; int size = 16;
BufferedImage img = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); BufferedImage img = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = img.createGraphics(); Graphics2D g = img.createGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setColor(new Color(0x8B, 0x5C, 0xF6)); // violet de marque LoreMind g.setColor(new Color(0x8B, 0x5C, 0xF6)); // violet de marque
g.fillRoundRect(0, 0, size, size, 5, 5); g.fillRoundRect(0, 0, size, size, 5, 5);
g.setColor(Color.WHITE); g.setColor(Color.WHITE);
g.setFont(new Font("SansSerif", Font.BOLD, 12)); g.setFont(new Font("SansSerif", Font.BOLD, 9));
g.drawString("L", 4, 13); g.drawString("DM", 1, 12);
g.dispose(); g.dispose();
return img; return img;
} }

View File

@@ -40,8 +40,8 @@ public class HttpLicenseRelay implements LicenseRelay {
RestTemplateBuilder builder, RestTemplateBuilder builder,
@Value("${licensing.relay.base-url:}") String baseUrl) { @Value("${licensing.relay.base-url:}") String baseUrl) {
this.http = builder this.http = builder
.setConnectTimeout(Duration.ofSeconds(5)) .connectTimeout(Duration.ofSeconds(5))
.setReadTimeout(Duration.ofSeconds(15)) .readTimeout(Duration.ofSeconds(15))
.build(); .build();
this.baseUrl = stripTrailingSlash(baseUrl); this.baseUrl = stripTrailingSlash(baseUrl);
} }

View File

@@ -7,6 +7,7 @@ import com.loremind.domain.licensing.RegistryCredentials;
import com.loremind.domain.licensing.ports.DockerConfigWriter; import com.loremind.domain.licensing.ports.DockerConfigWriter;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -25,6 +26,8 @@ import java.util.Optional;
* la plupart du temps. * la plupart du temps.
*/ */
@Component @Component
// Desactivable (ex: tests) : sans cette propriete (prod), le daemon tourne.
@ConditionalOnProperty(name = "licensing.refresh.enabled", matchIfMissing = true)
public class LicenseRefreshDaemon { public class LicenseRefreshDaemon {
private static final Logger log = LoggerFactory.getLogger(LicenseRefreshDaemon.class); private static final Logger log = LoggerFactory.getLogger(LicenseRefreshDaemon.class);

View File

@@ -4,6 +4,8 @@ import io.minio.BucketExistsArgs;
import io.minio.MakeBucketArgs; import io.minio.MakeBucketArgs;
import io.minio.MinioClient; import io.minio.MinioClient;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@@ -24,6 +26,8 @@ import org.springframework.context.annotation.Configuration;
@ConditionalOnProperty(name = "storage.backend", havingValue = "minio", matchIfMissing = true) @ConditionalOnProperty(name = "storage.backend", havingValue = "minio", matchIfMissing = true)
public class MinioConfig { public class MinioConfig {
private static final Logger log = LoggerFactory.getLogger(MinioConfig.class);
@Value("${minio.endpoint}") @Value("${minio.endpoint}")
private String endpoint; private String endpoint;
@@ -66,12 +70,11 @@ public class MinioConfig {
boolean exists = client.bucketExists(BucketExistsArgs.builder().bucket(bucket).build()); boolean exists = client.bucketExists(BucketExistsArgs.builder().bucket(bucket).build());
if (!exists) { if (!exists) {
client.makeBucket(MakeBucketArgs.builder().bucket(bucket).build()); client.makeBucket(MakeBucketArgs.builder().bucket(bucket).build());
System.out.println("[MinIO] Bucket '" + bucket + "' cree."); log.info("[MinIO] Bucket '{}' cree.", bucket);
} }
} catch (Exception e) { } catch (Exception e) {
System.err.println("[MinIO] Initialisation impossible (endpoint=" + endpoint log.warn("[MinIO] Initialisation impossible (endpoint={}). Les uploads d'images "
+ "). Les uploads d'images echoueront tant que MinIO n'est pas joignable. " + "echoueront tant que MinIO n'est pas joignable. Cause : {}", endpoint, e.getMessage());
+ "Cause : " + e.getMessage());
} }
} }
} }

View File

@@ -74,8 +74,8 @@ public class UpdateCheckService {
LicenseService licenseService, LicenseService licenseService,
@Nullable BuildProperties buildProperties) { @Nullable BuildProperties buildProperties) {
this.http = builder this.http = builder
.setConnectTimeout(Duration.ofSeconds(5)) .connectTimeout(Duration.ofSeconds(5))
.setReadTimeout(Duration.ofSeconds(15)) .readTimeout(Duration.ofSeconds(15))
.build(); .build();
this.registry = normalizeRegistry(registry); this.registry = normalizeRegistry(registry);
this.images = parseImages(imagesCsv); this.images = parseImages(imagesCsv);

View File

@@ -29,6 +29,9 @@ spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=validate spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=true spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true spring.jpa.properties.hibernate.format_sql=true
# Fixe explicitement open-in-view (defaut Spring = true) : meme comportement,
# mais supprime l'avertissement "spring.jpa.open-in-view is enabled by default".
spring.jpa.open-in-view=true
# ============================================================================ # ============================================================================
# Flyway : migrations de schema versionnees (src/main/resources/db/migration). # Flyway : migrations de schema versionnees (src/main/resources/db/migration).

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@@ -15,7 +15,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.core.task.TaskExecutor; import org.springframework.core.task.TaskExecutor;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
@@ -53,10 +53,10 @@ class AiChatControllerTest {
@Autowired private MockMvc mockMvc; @Autowired private MockMvc mockMvc;
@Autowired private ObjectMapper objectMapper; @Autowired private ObjectMapper objectMapper;
@MockBean private StreamChatForLoreUseCase loreUseCase; @MockitoBean private StreamChatForLoreUseCase loreUseCase;
@MockBean private StreamChatForCampaignUseCase campaignUseCase; @MockitoBean private StreamChatForCampaignUseCase campaignUseCase;
@MockBean private StreamChatForSessionUseCase sessionUseCase; @MockitoBean private StreamChatForSessionUseCase sessionUseCase;
@MockBean(name = "applicationTaskExecutor") private TaskExecutor taskExecutor; @MockitoBean(name = "applicationTaskExecutor") private TaskExecutor taskExecutor;
@BeforeEach @BeforeEach
void setUp() { void setUp() {

View File

@@ -6,7 +6,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.core.task.TaskExecutor; import org.springframework.core.task.TaskExecutor;
import org.springframework.mock.web.MockMultipartFile; import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
@@ -42,8 +42,8 @@ class CampaignAdaptControllerTest {
@Autowired private MockMvc mockMvc; @Autowired private MockMvc mockMvc;
@MockBean private CampaignAdaptService campaignAdaptService; @MockitoBean private CampaignAdaptService campaignAdaptService;
@MockBean(name = "applicationTaskExecutor") private TaskExecutor taskExecutor; @MockitoBean(name = "applicationTaskExecutor") private TaskExecutor taskExecutor;
@BeforeEach @BeforeEach
void setUp() { void setUp() {

View File

@@ -10,7 +10,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.core.task.TaskExecutor; import org.springframework.core.task.TaskExecutor;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile; import org.springframework.mock.web.MockMultipartFile;
@@ -58,8 +58,8 @@ class CampaignImportControllerTest {
@Autowired private MockMvc mockMvc; @Autowired private MockMvc mockMvc;
@Autowired private ObjectMapper objectMapper; @Autowired private ObjectMapper objectMapper;
@MockBean private CampaignImportService campaignImportService; @MockitoBean private CampaignImportService campaignImportService;
@MockBean(name = "applicationTaskExecutor") private TaskExecutor taskExecutor; @MockitoBean(name = "applicationTaskExecutor") private TaskExecutor taskExecutor;
private static final String CAMPAIGN_ID = "camp-1"; private static final String CAMPAIGN_ID = "camp-1";

View File

@@ -5,7 +5,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -25,7 +25,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
class ConfigControllerTest { class ConfigControllerTest {
@Autowired private MockMvc mockMvc; @Autowired private MockMvc mockMvc;
@MockBean private UpdateCheckService updates; @MockitoBean private UpdateCheckService updates;
@Test @Test
void getPublicConfig_returns200_updateCheckEnabledTrue() throws Exception { void getPublicConfig_returns200_updateCheckEnabledTrue() throws Exception {

View File

@@ -11,7 +11,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -41,7 +41,7 @@ class ConversationControllerTest {
@Autowired private ObjectMapper objectMapper; @Autowired private ObjectMapper objectMapper;
@Autowired private ConversationRepository conversationRepository; @Autowired private ConversationRepository conversationRepository;
@MockBean private ConversationTitleGenerator titleGenerator; @MockitoBean private ConversationTitleGenerator titleGenerator;
@Test @Test
void create_withLoreAnchor_returns200() throws Exception { void create_withLoreAnchor_returns200() throws Exception {

View File

@@ -12,7 +12,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.core.task.TaskExecutor; import org.springframework.core.task.TaskExecutor;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile; import org.springframework.mock.web.MockMultipartFile;
@@ -52,8 +52,8 @@ class GameSystemControllerTest {
@Autowired private MockMvc mockMvc; @Autowired private MockMvc mockMvc;
@Autowired private ObjectMapper objectMapper; @Autowired private ObjectMapper objectMapper;
@MockBean private RulesPdfImporter rulesPdfImporter; @MockitoBean private RulesPdfImporter rulesPdfImporter;
@MockBean(name = "applicationTaskExecutor") private TaskExecutor taskExecutor; @MockitoBean(name = "applicationTaskExecutor") private TaskExecutor taskExecutor;
@BeforeEach @BeforeEach
void setUp() { void setUp() {

View File

@@ -6,7 +6,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.mock.web.MockMultipartFile; import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
@@ -42,7 +42,7 @@ class ImageControllerTest {
@Autowired private MockMvc mockMvc; @Autowired private MockMvc mockMvc;
@MockBean private ImageService imageService; @MockitoBean private ImageService imageService;
private Image sampleImage() { private Image sampleImage() {
return Image.builder() return Image.builder()

View File

@@ -15,7 +15,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -49,7 +49,7 @@ class ItemCatalogControllerTest {
@Autowired private ItemCatalogRepository catalogRepository; @Autowired private ItemCatalogRepository catalogRepository;
@Autowired private CampaignRepository campaignRepository; @Autowired private CampaignRepository campaignRepository;
@MockBean private ItemCatalogGenerator generator; @MockitoBean private ItemCatalogGenerator generator;
private String campaignId; private String campaignId;

View File

@@ -10,7 +10,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
@@ -54,8 +54,8 @@ class LicenseControllerTest {
@Autowired private MockMvc mockMvc; @Autowired private MockMvc mockMvc;
@MockBean private LicenseService licenseService; @MockitoBean private LicenseService licenseService;
@MockBean private ChannelSwitcherService channelSwitcher; @MockitoBean private ChannelSwitcherService channelSwitcher;
private LicenseSnapshot validSnapshot() { private LicenseSnapshot validSnapshot() {
return new LicenseSnapshot( return new LicenseSnapshot(

View File

@@ -14,7 +14,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.core.task.TaskExecutor; import org.springframework.core.task.TaskExecutor;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile; import org.springframework.mock.web.MockMultipartFile;
@@ -59,9 +59,9 @@ class NotebookControllerTest {
@Autowired private NotebookRepository notebookRepository; @Autowired private NotebookRepository notebookRepository;
@Autowired private CampaignRepository campaignRepository; @Autowired private CampaignRepository campaignRepository;
@MockBean private NotebookIndexer indexer; @MockitoBean private NotebookIndexer indexer;
@MockBean private NotebookChatStreamer chatStreamer; @MockitoBean private NotebookChatStreamer chatStreamer;
@MockBean(name = "applicationTaskExecutor") private TaskExecutor taskExecutor; @MockitoBean(name = "applicationTaskExecutor") private TaskExecutor taskExecutor;
private String campaignId; private String campaignId;

View File

@@ -6,7 +6,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import java.util.Map; import java.util.Map;
@@ -36,7 +36,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
class PageGenerationControllerTest { class PageGenerationControllerTest {
@Autowired private MockMvc mockMvc; @Autowired private MockMvc mockMvc;
@MockBean private GeneratePageValuesUseCase generatePageValuesUseCase; @MockitoBean private GeneratePageValuesUseCase generatePageValuesUseCase;
@Test @Test
void generate_returns200_withSuggestions() throws Exception { void generate_returns200_withSuggestions() throws Exception {

View File

@@ -16,7 +16,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -50,7 +50,7 @@ class RandomTableControllerTest {
@Autowired private RandomTableRepository tableRepository; @Autowired private RandomTableRepository tableRepository;
@Autowired private CampaignRepository campaignRepository; @Autowired private CampaignRepository campaignRepository;
@MockBean private RandomTableGenerator generator; @MockitoBean private RandomTableGenerator generator;
private String campaignId; private String campaignId;

View File

@@ -5,7 +5,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
@@ -36,7 +36,7 @@ class UpdatesControllerDemoModeTest {
.encodeToString("test-admin:test-admin-password".getBytes(StandardCharsets.UTF_8)); .encodeToString("test-admin:test-admin-password".getBytes(StandardCharsets.UTF_8));
@Autowired private MockMvc mockMvc; @Autowired private MockMvc mockMvc;
@MockBean private UpdateCheckService updates; @MockitoBean private UpdateCheckService updates;
@Test @Test
void check_returns403_inDemoMode() throws Exception { void check_returns403_inDemoMode() throws Exception {

View File

@@ -9,7 +9,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
@@ -42,7 +42,7 @@ class UpdatesControllerTest {
.encodeToString("test-admin:test-admin-password".getBytes(StandardCharsets.UTF_8)); .encodeToString("test-admin:test-admin-password".getBytes(StandardCharsets.UTF_8));
@Autowired private MockMvc mockMvc; @Autowired private MockMvc mockMvc;
@MockBean private UpdateCheckService updates; @MockitoBean private UpdateCheckService updates;
private UpdateStatus sampleUpdate() { private UpdateStatus sampleUpdate() {
return new UpdateStatus(true, true, false, "1.0.0", return new UpdateStatus(true, true, false, "1.0.0",

View File

@@ -10,7 +10,6 @@ spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:loremind_test}
spring.datasource.driver-class-name=org.postgresql.Driver spring.datasource.driver-class-name=org.postgresql.Driver
# Configuration JPA pour les tests : schema recree a chaque run, pas de logs SQL. # Configuration JPA pour les tests : schema recree a chaque run, pas de logs SQL.
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=false spring.jpa.show-sql=false
@@ -19,7 +18,7 @@ spring.jpa.show-sql=false
spring.flyway.enabled=false spring.flyway.enabled=false
# Pool Hikari volontairement minuscule en test : la suite cree de nombreux # Pool Hikari volontairement minuscule en test : la suite cree de nombreux
# contextes Spring distincts (combinaisons de @MockBean / @TestPropertySource), # contextes Spring distincts (combinaisons de @MockitoBean / @TestPropertySource),
# tous gardes en cache simultanement. Avec le pool par defaut (10), on epuisait # tous gardes en cache simultanement. Avec le pool par defaut (10), on epuisait
# les connexions Postgres ("remaining connection slots are reserved"). 2 par # les connexions Postgres ("remaining connection slots are reserved"). 2 par
# contexte suffit (tests sequentiels) et borne le total bien sous max_connections. # contexte suffit (tests sequentiels) et borne le total bien sous max_connections.
@@ -40,3 +39,27 @@ minio.endpoint=http://localhost:9000
minio.access-key=test minio.access-key=test
minio.secret-key=test minio.secret-key=test
minio.bucket=test-bucket minio.bucket=test-bucket
# open-in-view fixe explicitement (defaut true) : supprime l'avertissement au boot.
spring.jpa.open-in-view=true
# Le daemon de refresh de licence (@Scheduled) n'a rien a faire pendant les tests :
# son tick echouait en arriere-plan (table licenses droppee par le create-drop
# partage entre contextes) => stacktraces parasites. On le desactive ici.
licensing.refresh.enabled=false
# --- Sortie de test LISIBLE ---------------------------------------------------
# Pas de banniere Spring (repetee a chaque contexte), et seuls les WARN/ERROR
# remontent : la masse d'INFO de demarrage (Spring/Hibernate/Hikari, "Started
# XxxTest", seeder...) n'apporte rien et noyait les vrais signaux.
spring.main.banner-mode=off
logging.level.root=WARN
# Tests de CHEMIN D'ERREUR : les controleurs loggent VOLONTAIREMENT l'echec (ex:
# Watchtower injoignable -> 502, Brain down -> 502) AVANT de renvoyer le bon statut
# HTTP teste. L'exception est attrapee, jamais propagee : ce sont des logs attendus.
logging.level.com.loremind.infrastructure.web.controller=OFF
# WARN attendus sur des tests de robustesse (registry injoignable, suppression best-effort).
logging.level.com.loremind.infrastructure.updates.UpdateCheckService=ERROR
logging.level.com.loremind.infrastructure.ai.BrainNotebookIndexClient=ERROR
# Le warning MinIO au @PostConstruct (serveur absent en test) est ATTENDU.
logging.level.com.loremind.infrastructure.storage=ERROR

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -0,0 +1,261 @@
#!/usr/bin/env bash
#
# Construit l'application de BUREAU Linux de DM Loremind sous forme d'AppImage,
# sans Docker. Equivalent Linux de installers/desktop/build-windows.ps1.
#
# Pipeline complet "local-first" :
# 1. Build du front Angular (web/ -> web/dist/web)
# 2. Prep du Brain (Python standalone) (brain/ -> dist-embed-linux : python relocatable + deps + sources)
# 3. Build du Core en fat jar + front (core/ -> target/*.jar, profil Maven "desktop")
# 4. Assemblage de la charge utile (jar + brain) dans un dossier d'entree jpackage
# 5. jpackage --type app-image -> image applicative + JRE embarque
# 6. appimagetool -> DM_Loremind-x86_64.AppImage (1 fichier, toutes distros)
#
# L'app resultante se lance d'un double-clic (chmod +x puis ./*.AppImage) : le Core
# demarre en profil Spring "local" (H2 fichier + stockage filesystem) et lance lui-meme
# le Brain en sidecar. Aucune dependance externe (ni Docker, ni Java, ni Python).
#
# PREREQUIS (machine de BUILD Linux uniquement, PAS chez l'utilisateur final) :
# - JDK 21+ avec jpackage dans le PATH (Temurin OK).
# - Node.js + npm (build Angular) ; Maven via le wrapper du repo.
# - curl, tar, et FUSE *ou* (sur CI sans FUSE) on lance appimagetool en
# --appimage-extract-and-run (gere automatiquement ci-dessous).
# - Internet (telecharge python-build-standalone + appimagetool).
#
# IMPORTANT : jpackage NE cross-compile PAS. Ce script DOIT tourner sur Linux
# (machine, WSL, ou runner ubuntu-latest). Il ne produira rien d'utile sous Windows.
#
# Projet : DM Loremind — assistant pour Maitres de Jeu de JDR — Licence AGPL-3.0
set -euo pipefail
# --- Args ------------------------------------------------------------------
VERSION=""
SKIP_FRONT=0; SKIP_BRAIN=0; SKIP_JAR=0
while [[ $# -gt 0 ]]; do
case "$1" in
--version) VERSION="$2"; shift 2 ;;
--skip-front) SKIP_FRONT=1; shift ;;
--skip-brain) SKIP_BRAIN=1; shift ;;
--skip-jar) SKIP_JAR=1; shift ;;
*) echo "Option inconnue : $1" >&2; exit 2 ;;
esac
done
step() { printf '\033[36m==> %s\033[0m\n' "$1"; }
ok() { printf '\033[32m OK %s\033[0m\n' "$1"; }
err() { printf '\033[31m XX %s\033[0m\n' "$1" >&2; }
# --- Chemins ---------------------------------------------------------------
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
WEB_DIR="$REPO_ROOT/web"
BRAIN_DIR="$REPO_ROOT/brain"
CORE_DIR="$REPO_ROOT/core"
STAGE_DIR="$CORE_DIR/target/dist-input" # charge utile jpackage
OUT_DIR="$CORE_DIR/target/dist-out" # image applicative + AppImage produits
BRAIN_EMBED="$BRAIN_DIR/dist-embed-linux" # staging du brain empaquete (Linux)
ICON_PNG="$SCRIPT_DIR/app-icon.png" # icone de l'app (jpackage Linux exige un PNG)
APP_NAME="DM Loremind"
# python-build-standalone (Astral) : equivalent Linux du Python embeddable de
# python.org (qui n'existe que sous Windows). Build "install_only" = Python complet
# RELOCATABLE (pip inclus), donc on installe les deps directement dedans.
PY_MINOR="3.12" # doit matcher python:3.12 du Docker
# --- Version (numerique) ---------------------------------------------------
if [[ -z "$VERSION" ]]; then
# Viser la version DU PROJET (artifactId loremind-core), pas le <version> du parent.
VERSION="$(grep -oPz '<artifactId>loremind-core</artifactId>\s*<version>\K[0-9]+\.[0-9]+\.[0-9]+' \
"$CORE_DIR/pom.xml" | tr -d '\0' | head -1 || true)"
[[ -z "$VERSION" ]] && VERSION="0.0.0"
fi
echo
printf '\033[35m============================================================\033[0m\n'
printf '\033[35m DM Loremind - Build AppImage Linux (v%s)\033[0m\n' "$VERSION"
printf '\033[35m============================================================\033[0m\n'
# --- Verif outils ----------------------------------------------------------
command -v jpackage >/dev/null 2>&1 || { err "jpackage introuvable dans le PATH. Installez un JDK 21+ (Temurin)."; exit 1; }
# --- 1. Front Angular ------------------------------------------------------
if [[ $SKIP_FRONT -eq 0 ]]; then
step "Build du front Angular"
( cd "$WEB_DIR"
[[ -d node_modules ]] || npm ci
npm run build )
ok "Front construit (web/dist/web)"
else step "Front : saute (--skip-front)"; fi
# --- 2. Brain (Python standalone relocatable, PAS d'exe gele) --------------
if [[ $SKIP_BRAIN -eq 0 ]]; then
step "Preparation du Brain (python-build-standalone $PY_MINOR)"
rm -rf "$BRAIN_EMBED"; mkdir -p "$BRAIN_EMBED"
# a) Resoudre l'asset install_only linux-gnu pour $PY_MINOR dans la derniere
# release PBS (la date du tag change a chaque release -> on la decouvre).
step " Resolution de l'archive python-build-standalone"
# URL EPINGLEE de secours (pas d'API) : version figee comme le build Windows,
# utilisee si la resolution dynamique echoue (rate limit, API HS, regex sans match).
PY_PIN='3.12.8'
PBS_DATE='20241219'
pbs_fallback="https://github.com/astral-sh/python-build-standalone/releases/download/${PBS_DATE}/cpython-${PY_PIN}+${PBS_DATE}-x86_64-unknown-linux-gnu-install_only.tar.gz"
# Appel API AUTHENTIFIE si un token est dispo (le runner partage est rate-limite
# a 60 req/h en anonyme -> 403 ; avec token : 1000/h). Tolerant : tout echec
# bascule sur l'URL epinglee (pas d'abort silencieux du a set -e).
auth=()
[[ -n "${GITHUB_TOKEN:-}" ]] && auth=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
api_json="$(curl -fsSL "${auth[@]}" \
https://api.github.com/repos/astral-sh/python-build-standalone/releases/latest 2>/dev/null || true)"
# NB: dans browser_download_url, le '+' (version+date) est URL-encode en %2B
# -> on accepte les deux. On vise le x86_64 BASELINE (pas _v2/_v3/_v4, qui
# exigent un CPU plus recent) et install_only (pas _stripped).
asset_url="$(printf '%s' "$api_json" \
| grep -m1 -oE "https://[^\"]*cpython-${PY_MINOR//./\\.}\.[0-9]+(%2B|\+)[0-9]+-x86_64-unknown-linux-gnu-install_only\.tar\.gz" \
|| true)"
if [[ -z "$asset_url" ]]; then
echo " (resolution API indisponible -> URL epinglee de secours)"
asset_url="$pbs_fallback"
fi
echo " Telechargement $asset_url"
curl -fsSL "$asset_url" -o "$BRAIN_EMBED/python.tar.gz"
# L'archive install_only s'extrait en un dossier 'python/' (bin/python3, lib/...).
tar -xzf "$BRAIN_EMBED/python.tar.gz" -C "$BRAIN_EMBED"
rm -f "$BRAIN_EMBED/python.tar.gz"
PYBIN="$BRAIN_EMBED/python/bin/python3"
[[ -x "$PYBIN" ]] || { err "python3 introuvable apres extraction ($PYBIN)."; exit 1; }
# b) Installer les deps DANS le python standalone (site-packages interne).
# Python complet => pip fonctionne directement, pas de bricolage ._pth/--target.
"$PYBIN" -m pip install --upgrade pip >/dev/null
"$PYBIN" -m pip install -r "$BRAIN_DIR/requirements.txt"
# c) Copier les sources du Brain + le point d'entree.
cp -r "$BRAIN_DIR/app" "$BRAIN_EMBED/app"
cp "$BRAIN_DIR/run_local.py" "$BRAIN_EMBED/run_local.py"
# d) OCR (Tesseract) : pas bundle pour l'instant sous Linux (libs partagees
# lourdes a embarquer proprement). Degradation gracieuse : si l'utilisateur a
# 'tesseract-ocr' (apt/dnf), pytesseract le trouve sur le PATH et l'OCR des
# scans marche ; sinon PDF born-digital OK, scans signales. Cf. run_local.py.
ok "Brain prepare (brain/dist-embed-linux : python standalone + deps + sources)"
else step "Brain : saute (--skip-brain)"; fi
# --- 3. Core (fat jar avec front embarque) ---------------------------------
if [[ $SKIP_JAR -eq 0 ]]; then
step "Build du Core (fat jar, profil desktop)"
( cd "$CORE_DIR"
chmod +x ./mvnw
# -Pdesktop : copie web/dist/web dans le jar (classpath:/static).
./mvnw -q -Pdesktop -DskipTests clean package )
ok "Core construit (core/target)"
else step "Core : saute (--skip-jar)"; fi
# --- 4. Assemblage de la charge utile --------------------------------------
step "Assemblage de la charge utile jpackage"
rm -rf "$STAGE_DIR"; mkdir -p "$STAGE_DIR"
# Le jar repackage Spring Boot (executable) — on ignore le *.jar.original.
jar="$(find "$CORE_DIR/target" -maxdepth 1 -name 'loremind-core-*.jar' ! -name '*.original' | head -1)"
[[ -z "$jar" ]] && { err "Jar introuvable dans core/target. Relancez sans --skip-jar."; exit 1; }
cp "$jar" "$STAGE_DIR/loremind-core.jar"
# Le Brain (python standalone + deps + sources) -> stage/brain/
[[ -d "$BRAIN_EMBED/python" ]] || { err "Brain introuvable (brain/dist-embed-linux). Relancez sans --skip-brain."; exit 1; }
mkdir -p "$STAGE_DIR/brain"
cp -r "$BRAIN_EMBED/." "$STAGE_DIR/brain/"
# Splash : copie dans la charge utile -> atterrit dans $APPDIR/splash.png (cf. -splash plus bas).
[[ -f "$SCRIPT_DIR/splash.png" ]] && cp "$SCRIPT_DIR/splash.png" "$STAGE_DIR/splash.png"
ok "Charge utile prete ($STAGE_DIR)"
# --- 5. jpackage -> app-image ----------------------------------------------
step "Generation de l'image applicative (app-image) via jpackage"
[[ -f "$ICON_PNG" ]] || { err "Icone manquante : $ICON_PNG (PNG requis sous Linux)."; exit 1; }
rm -rf "$OUT_DIR"; mkdir -p "$OUT_DIR"
# $APPDIR : substitue par jpackage AU LANCEMENT par le dossier applicatif
# (lib/app), qui contient le jar ET le dossier brain copie depuis --input.
# Le Brain se lance via python3 + run_local.py. brain.sidecar.command est une
# LISTE : les deux chemins separes par une virgule (aucun chemin Linux n'en
# contient) sont bindes en List<String> par Spring. NB: $APPDIR doit rester
# LITTERAL ici (quote simple) — c'est jpackage, pas bash, qui le resout.
BRAIN_CMD='$APPDIR/brain/python/bin/python3,$APPDIR/brain/run_local.py'
jpackage \
--type app-image \
--name "$APP_NAME" \
--app-version "$VERSION" \
--vendor 'IGML Creation' \
--icon "$ICON_PNG" \
--input "$STAGE_DIR" \
--main-jar loremind-core.jar \
--main-class org.springframework.boot.loader.launch.JarLauncher \
--dest "$OUT_DIR" \
--java-options '-Dspring.profiles.active=local' \
--java-options "-Dbrain.sidecar.command=$BRAIN_CMD" \
--java-options '-splash:$APPDIR/splash.png'
APPIMG_SRC="$OUT_DIR/$APP_NAME" # dossier produit par jpackage (avec espace)
[[ -d "$APPIMG_SRC" ]] || { err "app-image jpackage introuvable ($APPIMG_SRC)."; exit 1; }
ok "Image applicative prete ($APPIMG_SRC)"
# --- 6. AppImage via appimagetool ------------------------------------------
step "Empaquetage AppImage (toutes distros)"
APPDIR="$OUT_DIR/AppDir"
rm -rf "$APPDIR"; mkdir -p "$APPDIR/usr"
# a) L'app-image jpackage va sous AppDir/usr/ (le lanceur bin/* resout lib/ en
# relatif via ../lib -> reste coherent).
cp -r "$APPIMG_SRC/." "$APPDIR/usr/"
# b) Icone a la racine de l'AppDir (nom = Icon= du .desktop, SANS extension).
cp "$ICON_PNG" "$APPDIR/dm-loremind.png"
mkdir -p "$APPDIR/usr/share/icons/hicolor/256x256/apps"
cp "$ICON_PNG" "$APPDIR/usr/share/icons/hicolor/256x256/apps/dm-loremind.png"
# c) Fichier .desktop (a la racine + copie standard).
cat > "$APPDIR/dm-loremind.desktop" <<EOF
[Desktop Entry]
Type=Application
Name=DM Loremind
Comment=Assistant pour Maitres de Jeu de JDR
Exec=DM Loremind
Icon=dm-loremind
Categories=Game;Utility;
Terminal=false
EOF
mkdir -p "$APPDIR/usr/share/applications"
cp "$APPDIR/dm-loremind.desktop" "$APPDIR/usr/share/applications/dm-loremind.desktop"
# d) AppRun : point d'entree de l'AppImage -> lance le binaire jpackage.
cat > "$APPDIR/AppRun" <<'EOF'
#!/bin/sh
HERE="$(dirname "$(readlink -f "$0")")"
exec "$HERE/usr/bin/DM Loremind" "$@"
EOF
chmod +x "$APPDIR/AppRun"
# e) appimagetool (telecharge si absent). --appimage-extract-and-run : evite
# d'exiger FUSE (absent des runners CI).
# /!\ HORS de $OUT_DIR : sinon ce .AppImage (l'outil) serait pris pour un livrable
# et attache a la release a cote du vrai DM_Loremind-*.AppImage.
TOOL="$CORE_DIR/target/appimagetool-x86_64.AppImage"
if [[ ! -x "$TOOL" ]]; then
curl -fsSL -o "$TOOL" \
https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x "$TOOL"
fi
# APPIMAGE_EXTRACT_AND_RUN : permet d'EXECUTER l'AppImage appimagetool SANS FUSE
# (absent des runners CI) — il se self-extrait au lieu de se monter via fuse.
OUT_FILE="$OUT_DIR/DM_Loremind-${VERSION}-x86_64.AppImage"
ARCH=x86_64 APPIMAGE_EXTRACT_AND_RUN=1 "$TOOL" --appimage-extract-and-run "$APPDIR" "$OUT_FILE"
chmod +x "$OUT_FILE"
echo
printf '\033[32m============================================================\033[0m\n'
printf '\033[32m AppImage genere !\033[0m\n'
printf '\033[32m %s\033[0m\n' "$OUT_FILE"
printf '\033[32m============================================================\033[0m\n'

View File

@@ -57,6 +57,9 @@ $BrainDir = Join-Path $RepoRoot 'brain'
$CoreDir = Join-Path $RepoRoot 'core' $CoreDir = Join-Path $RepoRoot 'core'
$StageDir = Join-Path $CoreDir 'target\dist-input' # charge utile jpackage $StageDir = Join-Path $CoreDir 'target\dist-input' # charge utile jpackage
$OutDir = Join-Path $CoreDir 'target\dist-out' # .msi produit $OutDir = Join-Path $CoreDir 'target\dist-out' # .msi produit
$IconFile = Join-Path $PSScriptRoot 'app-icon.ico' # icone de l'app (.msi + raccourcis)
$WixDir = Join-Path $PSScriptRoot 'wix' # main.wxs surcharge (case "Lancer" en fin d'install)
$SplashFile = Join-Path $PSScriptRoot 'splash.png' # ecran de demarrage (affiche par la JVM via -splash)
# --- Version (numerique pour MSI) ------------------------------------------ # --- Version (numerique pour MSI) ------------------------------------------
if (-not $Version) { if (-not $Version) {
@@ -190,6 +193,9 @@ if (-not (Test-Path $BrainEmbed)) { Write-Err "Brain introuvable (brain/dist-emb
$stageBrain = Join-Path $StageDir 'brain' $stageBrain = Join-Path $StageDir 'brain'
New-Item -ItemType Directory -Force -Path $stageBrain | Out-Null New-Item -ItemType Directory -Force -Path $stageBrain | Out-Null
Copy-Item (Join-Path $BrainEmbed '*') $stageBrain -Recurse Copy-Item (Join-Path $BrainEmbed '*') $stageBrain -Recurse
# Splash : copie dans la charge utile -> atterrit dans $APPDIR\splash.png (cf. -splash plus bas).
if (Test-Path $SplashFile) { Copy-Item $SplashFile (Join-Path $StageDir 'splash.png') }
Write-Ok "Charge utile prete ($StageDir)" Write-Ok "Charge utile prete ($StageDir)"
# --- 5. jpackage -> .msi --------------------------------------------------- # --- 5. jpackage -> .msi ---------------------------------------------------
@@ -213,20 +219,32 @@ $brainCmd = '$APPDIR\brain\python\python.exe,$APPDIR\brain\run_local.py'
# #
# Note : pas de --win-console (app de bureau). Les logs Spring/Brain peuvent # Note : pas de --win-console (app de bureau). Les logs Spring/Brain peuvent
# etre rediriges vers un fichier via une option ulterieure si besoin de debug. # etre rediriges vers un fichier via une option ulterieure si besoin de debug.
# Icone de l'application (.msi, raccourcis bureau/menu). Doit etre un .ico Windows
# multi-resolution (16/32/48/256). Absente -> jpackage retomberait sur l'icone Java
# par defaut : on echoue tot avec un message clair plutot que de livrer ca.
if (-not (Test-Path $IconFile)) {
Write-Err "Icone manquante : $IconFile"
Write-Err "Depose l'icone de l'app (image DM, .ico multi-resolution) a cet emplacement."
exit 1
}
jpackage ` jpackage `
--type msi ` --type msi `
--name LoreMind ` --name 'DM Loremind' `
--app-version $Version ` --app-version $Version `
--vendor 'IGML Creation' ` --vendor 'IGML Creation' `
--icon $IconFile `
--resource-dir $WixDir `
--input $StageDir ` --input $StageDir `
--main-jar loremind-core.jar ` --main-jar loremind-core.jar `
--main-class org.springframework.boot.loader.launch.JarLauncher ` --main-class org.springframework.boot.loader.launch.JarLauncher `
--dest $OutDir ` --dest $OutDir `
--java-options '-Dspring.profiles.active=local' ` --java-options '-Dspring.profiles.active=local' `
--java-options "-Dbrain.sidecar.command=$brainCmd" ` --java-options "-Dbrain.sidecar.command=$brainCmd" `
--java-options '-splash:$APPDIR\splash.png' `
--win-per-user-install ` --win-per-user-install `
--win-upgrade-uuid 'a7c4e1d2-9b3f-4e6a-8d05-1f2c3b4a5e6d' ` --win-upgrade-uuid 'a7c4e1d2-9b3f-4e6a-8d05-1f2c3b4a5e6d' `
--win-menu --win-menu-group 'LoreMind' ` --win-menu --win-menu-group 'DM Loremind' `
--win-shortcut --win-dir-chooser --win-shortcut --win-dir-chooser
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -0,0 +1,164 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
main.wxs SURCHARGE (option resource-dir de jpackage) : copie du template du
JDK 21, plus une case "Lancer DM Loremind" sur l'ecran de fin (ExitDialog).
A garder synchronise avec le template du JDK du build (JDK 21). Pour re-extraire
la base : jimage extract (include regex main.wxs) depuis $JAVA_HOME/lib/modules.
Les seuls ajouts maison sont marques [DM Loremind].
Lancement : installeur per-user, donc contexte utilisateur ; une custom action
Directory + ExeCommand suffit (pas besoin de WixUtilExtension).
-->
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
<?ifdef JpIsSystemWide ?>
<?define JpInstallScope="perMachine"?>
<?else?>
<?define JpInstallScope="perUser"?>
<?endif?>
<?define JpProductLanguage=1033 ?>
<?define JpInstallerVersion=200 ?>
<?define JpCompressedMsi=yes ?>
<?include $(var.JpConfigDir)/overrides.wxi ?>
<?ifdef JpAllowUpgrades ?>
<?define JpUpgradeVersionOnlyDetectUpgrade="no"?>
<?else?>
<?define JpUpgradeVersionOnlyDetectUpgrade="yes"?>
<?endif?>
<?ifdef JpAllowDowngrades ?>
<?define JpUpgradeVersionOnlyDetectDowngrade="no"?>
<?else?>
<?define JpUpgradeVersionOnlyDetectDowngrade="yes"?>
<?endif?>
<Product
Id="$(var.JpProductCode)"
Name="$(var.JpAppName)"
Language="$(var.JpProductLanguage)"
Version="$(var.JpAppVersion)"
Manufacturer="$(var.JpAppVendor)"
UpgradeCode="$(var.JpProductUpgradeCode)">
<Package
Description="$(var.JpAppDescription)"
Manufacturer="$(var.JpAppVendor)"
InstallerVersion="$(var.JpInstallerVersion)"
Compressed="$(var.JpCompressedMsi)"
InstallScope="$(var.JpInstallScope)" Platform="x64"
/>
<Media Id="1" Cabinet="Data.cab" EmbedCab="yes" />
<Upgrade Id="$(var.JpProductUpgradeCode)">
<UpgradeVersion
OnlyDetect="$(var.JpUpgradeVersionOnlyDetectUpgrade)"
Property="JP_UPGRADABLE_FOUND"
Maximum="$(var.JpAppVersion)"
MigrateFeatures="yes"
IncludeMaximum="$(var.JpUpgradeVersionOnlyDetectUpgrade)" />
<UpgradeVersion
OnlyDetect="$(var.JpUpgradeVersionOnlyDetectDowngrade)"
Property="JP_DOWNGRADABLE_FOUND"
Minimum="$(var.JpAppVersion)"
MigrateFeatures="yes"
IncludeMinimum="$(var.JpUpgradeVersionOnlyDetectDowngrade)" />
</Upgrade>
<?ifndef JpAllowUpgrades ?>
<CustomAction Id="JpDisallowUpgrade" Error="!(loc.DisallowUpgradeErrorMessage)" />
<?endif?>
<?ifndef JpAllowDowngrades ?>
<CustomAction Id="JpDisallowDowngrade" Error="!(loc.DowngradeErrorMessage)" />
<?endif?>
<Binary Id="JpCaDll" SourceFile="wixhelper.dll"/>
<CustomAction Id="JpFindRelatedProducts" BinaryKey="JpCaDll" DllEntry="FindRelatedProductsEx" />
<!-- Standard required root -->
<Directory Id="TARGETDIR" Name="SourceDir"/>
<Feature Id="DefaultFeature" Title="!(loc.MainFeatureTitle)" Level="1">
<ComponentGroupRef Id="Shortcuts"/>
<ComponentGroupRef Id="Files"/>
<ComponentGroupRef Id="FileAssociations"/>
</Feature>
<CustomAction Id="JpSetARPINSTALLLOCATION" Property="ARPINSTALLLOCATION" Value="[INSTALLDIR]" />
<CustomAction Id="JpSetARPCOMMENTS" Property="ARPCOMMENTS" Value="$(var.JpAppDescription)" />
<CustomAction Id="JpSetARPCONTACT" Property="ARPCONTACT" Value="$(var.JpAppVendor)" />
<CustomAction Id="JpSetARPSIZE" Property="ARPSIZE" Value="$(var.JpAppSizeKb)" />
<?ifdef JpHelpURL ?>
<CustomAction Id="JpSetARPHELPLINK" Property="ARPHELPLINK" Value="$(var.JpHelpURL)" />
<?endif?>
<?ifdef JpAboutURL ?>
<CustomAction Id="JpSetARPURLINFOABOUT" Property="ARPURLINFOABOUT" Value="$(var.JpAboutURL)" />
<?endif?>
<?ifdef JpUpdateURL ?>
<CustomAction Id="JpSetARPURLUPDATEINFO" Property="ARPURLUPDATEINFO" Value="$(var.JpUpdateURL)" />
<?endif?>
<?ifdef JpIcon ?>
<Property Id="ARPPRODUCTICON" Value="JpARPPRODUCTICON"/>
<Icon Id="JpARPPRODUCTICON" SourceFile="$(var.JpIcon)"/>
<?endif?>
<UIRef Id="JpUI"/>
<!-- ===================== [DM Loremind] ===================== -->
<!-- Case « Lancer l'application » sur l'ExitDialog standard de WixUI.
WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT : libellé de la case (la rend visible).
WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 : cochée par défaut. -->
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="Lancer $(var.JpAppName)" />
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOX" Value="1" />
<!-- Lance le launcher jpackage ([INSTALLDIR]<AppName>.exe). asyncNoWait : on
n'attend pas la fin de l'app. Contexte utilisateur (install per-user). -->
<CustomAction Id="JpLaunchApplication"
Directory="INSTALLDIR"
ExeCommand="&quot;[INSTALLDIR]$(var.JpAppName).exe&quot;"
Return="asyncNoWait"
Impersonate="yes" />
<UI>
<Publish Dialog="ExitDialog" Control="Finish" Event="DoAction" Value="JpLaunchApplication" Order="1">WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed</Publish>
</UI>
<!-- =================== fin [DM Loremind] =================== -->
<InstallExecuteSequence>
<Custom Action="JpSetARPINSTALLLOCATION" After="CostFinalize">Not Installed</Custom>
<Custom Action="JpSetARPCOMMENTS" After="CostFinalize">Not Installed</Custom>
<Custom Action="JpSetARPCONTACT" After="CostFinalize">Not Installed</Custom>
<Custom Action="JpSetARPSIZE" After="CostFinalize">Not Installed</Custom>
<?ifdef JpHelpURL ?>
<Custom Action="JpSetARPHELPLINK" After="CostFinalize">Not Installed</Custom>
<?endif?>
<?ifdef JpAboutURL ?>
<Custom Action="JpSetARPURLINFOABOUT" After="CostFinalize">Not Installed</Custom>
<?endif?>
<?ifdef JpUpdateURL ?>
<Custom Action="JpSetARPURLUPDATEINFO" After="CostFinalize">Not Installed</Custom>
<?endif?>
<?ifndef JpAllowUpgrades ?>
<Custom Action="JpDisallowUpgrade" After="JpFindRelatedProducts">JP_UPGRADABLE_FOUND</Custom>
<?endif?>
<?ifndef JpAllowDowngrades ?>
<Custom Action="JpDisallowDowngrade" After="JpFindRelatedProducts">JP_DOWNGRADABLE_FOUND</Custom>
<?endif?>
<RemoveExistingProducts Before="CostInitialize"/>
<Custom Action="JpFindRelatedProducts" After="FindRelatedProducts"/>
</InstallExecuteSequence>
<InstallUISequence>
<Custom Action="JpFindRelatedProducts" After="FindRelatedProducts"/>
</InstallUISequence>
</Product>
</Wix>

View File

@@ -1,7 +1,7 @@
#Requires -Version 5.1 #Requires -Version 5.1
<# <#
.SYNOPSIS .SYNOPSIS
Installeur officiel de LoreMindMJ pour Windows 10/11. Installeur officiel de DM Loremind pour Windows 10/11.
.DESCRIPTION .DESCRIPTION
Script d'installation pas-a-pas qui : Script d'installation pas-a-pas qui :
@@ -18,7 +18,7 @@
Le code source de ce script est public et auditable a l'adresse indiquee dans .LINK. Le code source de ce script est public et auditable a l'adresse indiquee dans .LINK.
.PARAMETER InstallDir .PARAMETER InstallDir
Dossier d'installation. Defaut : %LOCALAPPDATA%\LoreMind Dossier d'installation. Defaut : %LOCALAPPDATA%\DM Loremind
.PARAMETER ComposeUrl .PARAMETER ComposeUrl
URL du fichier docker-compose.yml a recuperer. Defaut : version officielle du depot. URL du fichier docker-compose.yml a recuperer. Defaut : version officielle du depot.
@@ -39,7 +39,7 @@
.NOTES .NOTES
Auteur : ietm64 Auteur : ietm64
Licence : AGPL-3.0 Licence : AGPL-3.0
Projet : LoreMindMJ - assistant pour Maitres de Jeu de JDR Projet : DM Loremind - assistant pour Maitres de Jeu de JDR
Version : 0.8.3 Version : 0.8.3
.LINK .LINK
@@ -48,7 +48,7 @@
[CmdletBinding()] [CmdletBinding()]
param( param(
[string]$InstallDir = "$env:LOCALAPPDATA\LoreMind", [string]$InstallDir = "$env:LOCALAPPDATA\DM Loremind",
[string]$ComposeUrl = "https://raw.githubusercontent.com/IGMLcreation/LoreMind/main/docker-compose.yml", [string]$ComposeUrl = "https://raw.githubusercontent.com/IGMLcreation/LoreMind/main/docker-compose.yml",
[int]$WebPort = 8081, [int]$WebPort = 8081,
[switch]$NonInteractive [switch]$NonInteractive
@@ -142,7 +142,7 @@ if (-not (Test-Admin)) {
Write-Host "" Write-Host ""
Write-Host "============================================================" Write-Host "============================================================"
Write-Host " LoreMindMJ - Installeur Windows" -ForegroundColor Magenta Write-Host " DM Loremind - Installeur Windows" -ForegroundColor Magenta
Write-Host "============================================================" Write-Host "============================================================"
Write-Host "" Write-Host ""
@@ -250,7 +250,7 @@ if ($llmProvider -eq 'onemin' -and -not $NonInteractive) {
# pare-feu pour que Docker puisse l'atteindre sans exposer le port. # pare-feu pour que Docker puisse l'atteindre sans exposer le port.
# 2. Embarque : Ollama tourne dans un conteneur Docker dedie (profile local-ollama). # 2. Embarque : Ollama tourne dans un conteneur Docker dedie (profile local-ollama).
# 3. Aucun : on n'installe rien tout de suite. L'utilisateur configurera # 3. Aucun : on n'installe rien tout de suite. L'utilisateur configurera
# Ollama plus tard via la page Parametres de LoreMind. # Ollama plus tard via la page Parametres de DM Loremind.
$ollamaMode = 'embedded' # valeurs : 'host' | 'embedded' | 'none' $ollamaMode = 'embedded' # valeurs : 'host' | 'embedded' | 'none'
$ollamaBaseUrl = 'http://ollama:11434' $ollamaBaseUrl = 'http://ollama:11434'
if ($llmProvider -eq 'ollama') { if ($llmProvider -eq 'ollama') {
@@ -298,7 +298,7 @@ if ($llmProvider -eq 'ollama') {
# sera installe plus tard sur l'hote. L'utilisateur peut aussi changer # sera installe plus tard sur l'hote. L'utilisateur peut aussi changer
# l'URL via la page Parametres pour pointer vers un Ollama distant. # l'URL via la page Parametres pour pointer vers un Ollama distant.
$ollamaBaseUrl = 'http://host.docker.internal:11434' $ollamaBaseUrl = 'http://host.docker.internal:11434'
Write-Warn2 "Aucun Ollama ne sera installe pour le moment. Configurez-le plus tard via la page Parametres de LoreMind." Write-Warn2 "Aucun Ollama ne sera installe pour le moment. Configurez-le plus tard via la page Parametres de DM Loremind."
} }
} }
@@ -404,7 +404,7 @@ if ($ollamaMode -eq 'embedded' -and $llmProvider -eq 'ollama') {
$url = "http://localhost:$WebPort" $url = "http://localhost:$WebPort"
Write-Host "" Write-Host ""
Write-Host "============================================================" -ForegroundColor Green Write-Host "============================================================" -ForegroundColor Green
Write-Host " LoreMindMJ est lance !" -ForegroundColor Green Write-Host " DM Loremind est lance !" -ForegroundColor Green
Write-Host "============================================================" -ForegroundColor Green Write-Host "============================================================" -ForegroundColor Green
Write-Host " URL : $url" Write-Host " URL : $url"
Write-Host " Identifiant : $adminUser" Write-Host " Identifiant : $adminUser"

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# ========================================================================== # ==========================================================================
# Installeur LoreMindMJ pour Linux (Debian/Ubuntu/Fedora/Arch) # Installeur DM Loremind pour Linux (Debian/Ubuntu/Fedora/Arch)
# Usage : # Usage :
# curl -fsSL https://raw.githubusercontent.com/IGMLcreation/LoreMind/main/installers/install.sh | bash # curl -fsSL https://raw.githubusercontent.com/IGMLcreation/LoreMind/main/installers/install.sh | bash
# ========================================================================== # ==========================================================================
@@ -71,7 +71,7 @@ install_docker() {
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
echo echo
echo "============================================================" echo "============================================================"
echo -e " ${c_cyan}LoreMindMJ - Installeur Linux${c_off}" echo -e " ${c_cyan}DM Loremind - Installeur Linux${c_off}"
echo "============================================================" echo "============================================================"
echo echo
@@ -169,7 +169,7 @@ if [ "$LLM_PROVIDER" = "ollama" ]; then
# sera installe plus tard sur l'hote. L'utilisateur peut aussi # sera installe plus tard sur l'hote. L'utilisateur peut aussi
# changer l'URL via la page Parametres pour un Ollama distant. # changer l'URL via la page Parametres pour un Ollama distant.
OLLAMA_BASE_URL_VAL="http://host.docker.internal:11434" OLLAMA_BASE_URL_VAL="http://host.docker.internal:11434"
warn "Aucun Ollama ne sera installe pour le moment. Configurez-le plus tard via la page Parametres de LoreMind." warn "Aucun Ollama ne sera installe pour le moment. Configurez-le plus tard via la page Parametres de DM Loremind."
;; ;;
esac esac
fi fi
@@ -268,7 +268,7 @@ fi
URL="http://localhost:${WEB_PORT}" URL="http://localhost:${WEB_PORT}"
echo echo
echo -e "${c_green}============================================================${c_off}" echo -e "${c_green}============================================================${c_off}"
echo -e "${c_green} LoreMindMJ est lance !${c_off}" echo -e "${c_green} DM Loremind est lance !${c_off}"
echo -e "${c_green}============================================================${c_off}" echo -e "${c_green}============================================================${c_off}"
echo " URL : $URL" echo " URL : $URL"
echo " Identifiant : $ADMIN_USERNAME" echo " Identifiant : $ADMIN_USERNAME"

View File

@@ -1,10 +1,10 @@
#Requires -Version 5.1 #Requires -Version 5.1
<# <#
.SYNOPSIS .SYNOPSIS
Configuration securisee d'Ollama hote pour LoreMindMJ (Windows). Configuration securisee d'Ollama hote pour DM Loremind (Windows).
.DESCRIPTION .DESCRIPTION
But : permettre au conteneur Docker LoreMind d'atteindre l'Ollama installe But : permettre au conteneur Docker DM Loremind d'atteindre l'Ollama installe
sur l'hote, SANS exposer Ollama sur le LAN ni Internet. sur l'hote, SANS exposer Ollama sur le LAN ni Internet.
Strategie (specifique a Docker Desktop / WSL2 sur Windows) : Strategie (specifique a Docker Desktop / WSL2 sur Windows) :

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# ============================================================================ # ============================================================================
# LoreMindMJ - Configuration securisee d'Ollama hote (Linux) # DM Loremind - Configuration securisee d'Ollama hote (Linux)
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
# But : permettre au conteneur Docker de LoreMind d'atteindre l'Ollama # But : permettre au conteneur Docker de LoreMind d'atteindre l'Ollama
# installe sur l'hote, SANS l'exposer sur le LAN ni Internet. # installe sur l'hote, SANS l'exposer sur le LAN ni Internet.

View File

@@ -52,7 +52,8 @@
"development": { "development": {
"optimization": false, "optimization": false,
"sourceMap": true, "sourceMap": true,
"namedChunks": true "namedChunks": true,
"outputHashing": "none"
} }
}, },
"defaultConfiguration": "production" "defaultConfiguration": "production"

View File

@@ -35,7 +35,7 @@ test.describe('NPC edit', () => {
await page.getByLabel(/Nom du PNJ/i).fill(newName); await page.getByLabel(/Nom du PNJ/i).fill(newName);
await page.getByRole('button', { name: /^Enregistrer$/i }).click(); await page.getByRole('button', { name: /^Sauvegarder$/i }).click();
// Retour à la campagne après save // Retour à la campagne après save
await expect(page).toHaveURL(new RegExp(`/campaigns/${campaign.id}$`)); await expect(page).toHaveURL(new RegExp(`/campaigns/${campaign.id}$`));
@@ -48,7 +48,7 @@ test.describe('NPC edit', () => {
await page.goto(`/campaigns/${campaign.id}/npcs/${npc.id}/edit`); await page.goto(`/campaigns/${campaign.id}/npcs/${npc.id}/edit`);
const nameField = page.getByLabel(/Nom du PNJ/i); const nameField = page.getByLabel(/Nom du PNJ/i);
const saveBtn = page.getByRole('button', { name: /^Enregistrer$/i }); const saveBtn = page.getByRole('button', { name: /^Sauvegarder$/i });
await expect(saveBtn).toBeEnabled(); await expect(saveBtn).toBeEnabled();
await nameField.fill(''); await nameField.fill('');

View File

@@ -42,7 +42,7 @@ test.describe('GameSystem edit', () => {
await page.getByLabel(/^Nom/i).fill(newName); await page.getByLabel(/^Nom/i).fill(newName);
await page.getByLabel(/Description courte/i).fill(newDescription); await page.getByLabel(/Description courte/i).fill(newDescription);
await page.getByRole('button', { name: /^Enregistrer$/i }).click(); await page.getByRole('button', { name: /^Sauvegarder$/i }).click();
// Retour a la liste apres save. // Retour a la liste apres save.
await expect(page).toHaveURL(/\/game-systems$/); await expect(page).toHaveURL(/\/game-systems$/);
@@ -57,7 +57,7 @@ test.describe('GameSystem edit', () => {
await expect(page.getByLabel(/^Nom/i)).toHaveValue(gs.name); await expect(page.getByLabel(/^Nom/i)).toHaveValue(gs.name);
const nameField = page.getByLabel(/^Nom/i); const nameField = page.getByLabel(/^Nom/i);
const saveBtn = page.getByRole('button', { name: /^Enregistrer$/i }); const saveBtn = page.getByRole('button', { name: /^Sauvegarder$/i });
await expect(saveBtn).toBeEnabled(); await expect(saveBtn).toBeEnabled();
await nameField.fill(''); await nameField.fill('');

View File

@@ -38,7 +38,7 @@ test.describe('GameSystem rule sections editor', () => {
await card.locator('.section-content').fill(sectionContent); await card.locator('.section-content').fill(sectionContent);
// Save + retour a la liste. // Save + retour a la liste.
await page.getByRole('button', { name: /^Enregistrer$/i }).click(); await page.getByRole('button', { name: /^Sauvegarder$/i }).click();
await expect(page).toHaveURL(/\/game-systems$/); await expect(page).toHaveURL(/\/game-systems$/);
// Verification cote API : le markdown contient bien la section + son contenu. // Verification cote API : le markdown contient bien la section + son contenu.

View File

@@ -44,7 +44,7 @@ test.describe('GameSystem template fields editor (PJ / PNJ)', () => {
await expect(row.locator('.tfe-name')).toHaveValue('Histoire'); await expect(row.locator('.tfe-name')).toHaveValue('Histoire');
// Save → retour a la liste. // Save → retour a la liste.
await page.getByRole('button', { name: /^Enregistrer$/i }).click(); await page.getByRole('button', { name: /^Sauvegarder$/i }).click();
await expect(page).toHaveURL(/\/game-systems$/); await expect(page).toHaveURL(/\/game-systems$/);
// Verification API : le champ est bien dans characterTemplate. // Verification API : le champ est bien dans characterTemplate.
@@ -88,7 +88,7 @@ test.describe('GameSystem template fields editor (PJ / PNJ)', () => {
await expect(tfe(page, 'PJ').locator('.tfe-item').first().locator('.tfe-name')).toHaveValue('Histoire'); await expect(tfe(page, 'PJ').locator('.tfe-item').first().locator('.tfe-name')).toHaveValue('Histoire');
await expect(tfe(page, 'PNJ').locator('.tfe-item').first().locator('.tfe-name')).toHaveValue('Motivation'); await expect(tfe(page, 'PNJ').locator('.tfe-item').first().locator('.tfe-name')).toHaveValue('Motivation');
await page.getByRole('button', { name: /^Enregistrer$/i }).click(); await page.getByRole('button', { name: /^Sauvegarder$/i }).click();
await expect(page).toHaveURL(/\/game-systems$/); await expect(page).toHaveURL(/\/game-systems$/);
const persisted = await request.get(`/api/game-systems/${gs.id}`).then((r) => r.json()); const persisted = await request.get(`/api/game-systems/${gs.id}`).then((r) => r.json());

4
web/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "loremind-web", "name": "loremind-web",
"version": "0.16.2", "version": "0.18.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "loremind-web", "name": "loremind-web",
"version": "0.16.2", "version": "0.18.0",
"dependencies": { "dependencies": {
"@angular/animations": "^21.2.16", "@angular/animations": "^21.2.16",
"@angular/common": "^21.2.16", "@angular/common": "^21.2.16",

View File

@@ -1,6 +1,6 @@
{ {
"name": "loremind-web", "name": "loremind-web",
"version": "0.16.2", "version": "0.18.0",
"description": "LoreMind Frontend - Angular", "description": "LoreMind Frontend - Angular",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",

View File

@@ -17,6 +17,12 @@ export default defineConfig({
reporter: process.env['CI'] ? [['html', { open: 'never' }], ['list']] : 'html', reporter: process.env['CI'] ? [['html', { open: 'never' }], ['list']] : 'html',
use: { use: {
baseURL, baseURL,
// Locale FR épinglée : l'app résout sa langue via celle du navigateur
// (LanguageService.resolveInitialLang → getBrowserLang()). Sans ça, le
// Chromium de Playwright démarre en en-US → l'UI passe en anglais → tous les
// tests, écrits pour les libellés français, échouent (vert sur une machine en
// locale FR, rouge en CI Linux en-US). On fixe donc le français, déterministe.
locale: 'fr-FR',
trace: 'on-first-retry', trace: 'on-first-retry',
screenshot: 'only-on-failure', screenshot: 'only-on-failure',
video: 'retain-on-failure', video: 'retain-on-failure',

View File

@@ -108,7 +108,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@for (row of tableValues[field.name] ?? []; track $index; let ri = $index) { @for (row of tableRows(field.name); track $index; let ri = $index) {
<tr> <tr>
@for (col of field.labels; track $index) { @for (col of field.labels; track $index) {
<td> <td>

View File

@@ -240,6 +240,11 @@ export class PageEditComponent implements OnInit, OnDestroy {
this.tableValues[fieldName]?.splice(rowIndex, 1); this.tableValues[fieldName]?.splice(rowIndex, 1);
} }
/** Lignes du tableau d'un champ — toujours un tableau (jamais undefined) pour le `@for`. */
tableRows(fieldName: string): Array<Record<string, string>> {
return this.tableValues[fieldName] ?? [];
}
// --- Chat IA conversationnel (Phase b5) -------------------------------- // --- Chat IA conversationnel (Phase b5) --------------------------------
toggleChat(): void { toggleChat(): void {

View File

@@ -77,15 +77,13 @@ export class LanguageService {
} }
private resolveInitialLang(): string { private resolveInitialLang(): string {
// 1) Choix mémorisé (sélecteur in-app) → prioritaire.
const stored = this.read(); const stored = this.read();
if (stored && this.languages.some((l) => l.code === stored)) { if (stored && this.languages.some((l) => l.code === stored)) {
return stored; return stored;
} }
const browser = this.translate.getBrowserLang(); // 2) Sinon, détection du système : français → fr, toute autre langue → en.
if (browser && this.languages.some((l) => l.code === browser)) { return this.translate.getBrowserLang() === 'fr' ? 'fr' : 'en';
return browser;
}
return this.defaultLang;
} }
private read(): string | null { private read(): string | null {

View File

@@ -1,28 +0,0 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { ProgressionStatus } from './campaign.model';
/**
* Endpoints de progression des quêtes pour un Playthrough.
* Modèle "absence = NOT_STARTED" — envoyer NOT_STARTED supprime la ligne côté backend.
*/
@Injectable({ providedIn: 'root' })
export class QuestProgressionService {
constructor(private http: HttpClient) {}
/** Map chapterId -> ProgressionStatus pour le Playthrough donné. */
list(playthroughId: string): Observable<Record<string, ProgressionStatus>> {
return this.http.get<Record<string, ProgressionStatus>>(
`/api/playthroughs/${playthroughId}/quest-progressions`
);
}
setStatus(playthroughId: string, chapterId: string, status: ProgressionStatus): Observable<void> {
return this.http.put<void>(
`/api/playthroughs/${playthroughId}/quest-progressions/${chapterId}`,
{ status }
);
}
}

View File

@@ -1,4 +1,4 @@
@if (fields?.length) { @if (fields.length) {
<div class="dff"> <div class="dff">
@for (f of fields; track trackByName($index, f)) { @for (f of fields; track trackByName($index, f)) {
<div class="dff-field"> <div class="dff-field">

View File

@@ -1,6 +0,0 @@
<span [class]="cssClass" [attr.aria-label]="label" [title]="label">
<lucide-icon [img]="icon" [size]="14"></lucide-icon>
@if (!compact) {
<span class="status-label">{{ label }}</span>
}
</span>

View File

@@ -1,43 +0,0 @@
.status-badge {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.2rem 0.55rem;
border-radius: 999px;
font-size: 0.78rem;
font-weight: 500;
line-height: 1;
border: 1px solid transparent;
}
.status-label {
white-space: nowrap;
}
// Verrouillée — gris discret
.status-locked {
background: rgba(128, 128, 128, 0.12);
color: #6b6b6b;
border-color: rgba(128, 128, 128, 0.25);
}
// Disponible — vert calme (prête à être lancée)
.status-available {
background: rgba(52, 168, 83, 0.12);
color: #2f7a47;
border-color: rgba(52, 168, 83, 0.3);
}
// En cours — bleu actif
.status-in_progress {
background: rgba(66, 133, 244, 0.14);
color: #2c6cd6;
border-color: rgba(66, 133, 244, 0.35);
}
// Terminée — violet doux (clos, pas neutre)
.status-completed {
background: rgba(120, 80, 200, 0.12);
color: #6d4fb5;
border-color: rgba(120, 80, 200, 0.3);
}

View File

@@ -1,48 +0,0 @@
import { Component, Input } from '@angular/core';
import { LucideAngularModule, Lock, Circle, Play, CheckCircle2, LucideIconData } from 'lucide-angular';
import { TranslateService } from '@ngx-translate/core';
import { QuestStatus } from '../../services/campaign.model';
/**
* Badge visuel pour un QuestStatus (vue Hub).
* Composant standalone, sans dépendance métier.
*/
@Component({
selector: 'app-quest-status-badge',
imports: [LucideAngularModule],
templateUrl: './quest-status-badge.component.html',
styleUrls: ['./quest-status-badge.component.scss']
})
export class QuestStatusBadgeComponent {
@Input() status: QuestStatus | undefined | null = 'AVAILABLE';
/** Variante visuelle compacte (sans label) — utile pour les listes denses. */
@Input() compact = false;
constructor(private translate: TranslateService) {}
get icon(): LucideIconData {
switch (this.status) {
case 'LOCKED': return Lock;
case 'IN_PROGRESS': return Play;
case 'COMPLETED': return CheckCircle2;
case 'AVAILABLE':
default: return Circle;
}
}
get label(): string {
switch (this.status) {
case 'LOCKED': return this.translate.instant('questStatusBadge.locked');
case 'IN_PROGRESS': return this.translate.instant('questStatusBadge.inProgress');
case 'COMPLETED': return this.translate.instant('questStatusBadge.completed');
case 'AVAILABLE':
default: return this.translate.instant('questStatusBadge.available');
}
}
get cssClass(): string {
return `status-badge status-${(this.status ?? 'AVAILABLE').toLowerCase()}`;
}
}

View File

@@ -2,8 +2,8 @@
<div class="sidebar-header"> <div class="sidebar-header">
<div class="logo"> <div class="logo">
<span class="logo-icon"></span> <img class="logo-img" src="assets/logo.png" alt="DM Loremind" />
<span class="logo-text">LoreMind</span> <span class="logo-text">DM Loremind</span>
</div> </div>
<p class="logo-subtitle">{{ 'sidebar.subtitle' | translate }}</p> <p class="logo-subtitle">{{ 'sidebar.subtitle' | translate }}</p>
</div> </div>

View File

@@ -18,6 +18,7 @@
} }
.logo-icon { font-size: 1.2rem; color: #6c63ff; } .logo-icon { font-size: 1.2rem; color: #6c63ff; }
.logo-img { width: 1.75rem; height: 1.75rem; border-radius: 6px; object-fit: contain; flex: 0 0 auto; }
.logo-text { font-size: 1.25rem; font-weight: 700; color: white; } .logo-text { font-size: 1.25rem; font-weight: 700; color: white; }
.logo-subtitle { .logo-subtitle {

BIN
web/src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

BIN
web/src/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -2,7 +2,7 @@
<html lang="fr"> <html lang="fr">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>LoreMind</title> <title>DM Loremind</title>
<base href="/"> <base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="icon" type="image/x-icon" href="favicon.ico">