Compare commits
4 Commits
v0.16.2
...
v0.17.1-be
| Author | SHA1 | Date | |
|---|---|---|---|
| 024d37dea6 | |||
| 997aadf5b5 | |||
| 560c07d5c3 | |||
| 5aa08a3a27 |
@@ -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
|
||||||
|
|||||||
80
.github/workflows/desktop-release.yml
vendored
80
.github/workflows/desktop-release.yml
vendored
@@ -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/*.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/*.AppImage
|
||||||
|
retention-days: 90
|
||||||
|
|||||||
@@ -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.17.1-beta",
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
41
core/pom.xml
41
core/pom.xml
@@ -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.17.1-beta</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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
BIN
core/src/main/resources/tray-icon.png
Normal file
BIN
core/src/main/resources/tray-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
@@ -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() {
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
BIN
installers/desktop/app-icon.ico
Normal file
BIN
installers/desktop/app-icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 89 KiB |
BIN
installers/desktop/app-icon.png
Normal file
BIN
installers/desktop/app-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
255
installers/desktop/build-linux.sh
Normal file
255
installers/desktop/build-linux.sh
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
#!/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/"
|
||||||
|
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"
|
||||||
|
|
||||||
|
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).
|
||||||
|
TOOL="$OUT_DIR/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'
|
||||||
@@ -57,6 +57,7 @@ $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)
|
||||||
|
|
||||||
# --- Version (numerique pour MSI) ------------------------------------------
|
# --- Version (numerique pour MSI) ------------------------------------------
|
||||||
if (-not $Version) {
|
if (-not $Version) {
|
||||||
@@ -213,11 +214,21 @@ $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 `
|
||||||
--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 `
|
||||||
@@ -226,7 +237,7 @@ jpackage `
|
|||||||
--java-options "-Dbrain.sidecar.command=$brainCmd" `
|
--java-options "-Dbrain.sidecar.command=$brainCmd" `
|
||||||
--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) {
|
||||||
|
|||||||
@@ -52,7 +52,8 @@
|
|||||||
"development": {
|
"development": {
|
||||||
"optimization": false,
|
"optimization": false,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"namedChunks": true
|
"namedChunks": true,
|
||||||
|
"outputHashing": "none"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultConfiguration": "production"
|
"defaultConfiguration": "production"
|
||||||
|
|||||||
@@ -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('');
|
||||||
|
|||||||
@@ -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('');
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
4
web/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "loremind-web",
|
"name": "loremind-web",
|
||||||
"version": "0.16.2",
|
"version": "0.17.1-beta",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "loremind-web",
|
"name": "loremind-web",
|
||||||
"version": "0.16.2",
|
"version": "0.17.1-beta",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^21.2.16",
|
"@angular/animations": "^21.2.16",
|
||||||
"@angular/common": "^21.2.16",
|
"@angular/common": "^21.2.16",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "loremind-web",
|
"name": "loremind-web",
|
||||||
"version": "0.16.2",
|
"version": "0.17.1-beta",
|
||||||
"description": "LoreMind Frontend - Angular",
|
"description": "LoreMind Frontend - Angular",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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()}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
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
BIN
web/src/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.5 KiB |
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user