4 Commits

Author SHA1 Message Date
aaebeaa547 Mise à jour du readme d'accueil pour l'accès à la documentation
Some checks failed
E2E Tests / e2e (push) Failing after 17s
2026-04-27 08:27:38 +02:00
03ee3855f5 Passage version 0.6.14 + résolution d'un soucis sur l'updater depuis la migration sur git
Some checks failed
E2E Tests / e2e (push) Failing after 24s
Build & Push Images / build (brain) (push) Successful in 59s
Build & Push Images / build (core) (push) Successful in 1m36s
Build & Push Images / build (web) (push) Successful in 1m47s
2026-04-26 19:08:49 +02:00
94a39cf3b4 Mise en place de la pipeline pour github plutot que gitea ; mise en place des images docker sur GHCR plutôt que gitea
Some checks failed
E2E Tests / e2e (push) Failing after 22s
Build & Push Images / build (brain) (push) Successful in 58s
Build & Push Images / build (core) (push) Successful in 1m32s
Build & Push Images / build (web) (push) Successful in 1m40s
Passage version v0.6.13
2026-04-26 10:46:46 +02:00
efe6f6c2b0 Empêche la modale de ce fermer tant que le llm n'est pas télécharger
Some checks failed
E2E Tests / e2e (push) Failing after 19s
Build & Push Images / build (brain) (push) Successful in 48s
Build & Push Images / build (core) (push) Successful in 1m20s
Build & Push Images / build (web) (push) Successful in 1m30s
2026-04-26 09:12:36 +02:00
15 changed files with 121 additions and 85 deletions

View File

@@ -6,8 +6,10 @@ on:
- 'v*' - 'v*'
env: env:
REGISTRY: git.igmlcreation.fr GITEA_REGISTRY: git.igmlcreation.fr
REGISTRY_USER: ietm64 GITEA_REGISTRY_USER: ietm64
GHCR_REGISTRY: ghcr.io
GHCR_NAMESPACE: igmlcreation
jobs: jobs:
build: build:
@@ -26,19 +28,39 @@ jobs:
- name: Login to Gitea Registry - name: Login to Gitea Registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.GITEA_REGISTRY }}
username: ${{ env.REGISTRY_USER }} username: ${{ env.GITEA_REGISTRY_USER }}
password: ${{ secrets.DOCKER_PAT }} password: ${{ secrets.DOCKER_PAT }}
# Login to GHCR (GitHub Container Registry) pour distribuer les images
# publiquement aux utilisateurs finaux. Reputation domaine plus elevee
# que git.igmlcreation.fr (mieux pour les antivirus / SmartScreen).
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.GHCR_REGISTRY }}
username: ${{ env.GHCR_NAMESPACE }}
password: ${{ secrets.GHCR_TOKEN }}
- name: Extract version - name: Extract version
id: meta id: meta
run: echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT run: echo "version=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT
# Push vers les deux registries en un seul build (build-push-action
# accepte une liste de tags ; aucun build supplementaire necessaire).
# Naming :
# - Gitea : conserve l'ancien pattern ietm64/<component> pour ne pas
# casser les installs existantes qui ont REGISTRY=git.igmlcreation.fr
# dans leur .env.
# - GHCR : nouveau pattern igmlcreation/loremind-<component> qui evite
# la collision avec d'autres projets de l'org.
- name: Build & push ${{ matrix.component }} - name: Build & push ${{ matrix.component }}
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
context: ./${{ matrix.component }} context: ./${{ matrix.component }}
push: true push: true
tags: | tags: |
${{ env.REGISTRY }}/${{ env.REGISTRY_USER }}/${{ matrix.component }}:latest ${{ env.GITEA_REGISTRY }}/${{ env.GITEA_REGISTRY_USER }}/${{ matrix.component }}:latest
${{ env.REGISTRY }}/${{ env.REGISTRY_USER }}/${{ matrix.component }}:${{ steps.meta.outputs.version }} ${{ env.GITEA_REGISTRY }}/${{ env.GITEA_REGISTRY_USER }}/${{ matrix.component }}:${{ steps.meta.outputs.version }}
${{ env.GHCR_REGISTRY }}/${{ env.GHCR_NAMESPACE }}/loremind-${{ matrix.component }}:latest
${{ env.GHCR_REGISTRY }}/${{ env.GHCR_NAMESPACE }}/loremind-${{ matrix.component }}:${{ steps.meta.outputs.version }}

1
.gitignore vendored
View File

@@ -91,6 +91,7 @@ Thumbs.db
# Documentation hors-code (conservee hors du repo) # Documentation hors-code (conservee hors du repo)
# ============================================================================ # ============================================================================
docs/ docs/
loremind-docs/
# ============================================================================ # ============================================================================
# Docker Compose override (dev uniquement, non-distribue aux end users) # Docker Compose override (dev uniquement, non-distribue aux end users)

View File

@@ -1,69 +1,22 @@
# LoreMind # LoreMind
Application web d'aide aux Maîtres de Jeu (JDR) pour centraliser la gestion de l'univers (Lore) et le suivi des campagnes, avec un moteur IA intégré pour générer du contenu structuré. ![Tableau de bord](https://raw.githubusercontent.com/IGMLcreation/loremind-docs/main/static/img/screenshots/dashboard.png)
Loremind est une application web angular auto-hébergable afin de venir en aide aux Maîtres de jeu qui souhaitent centraliser leur univers et leurs campagnes.
Cette dernière intègre un moteur IA qui va ingérer le contenu du lore et de la campagne afin de pouvoir répondre à des questions précises sur l'univers ou la campagne, mais également proposer des idées de création dans le contexte de la campagne et du lore.
Pour le moment seul Ollama est supporté pour la partie locale, il y-a également une intégration pour 1min.ai. Plus tard, d'autres moteurs seront supportés.
## Documentation
La documentation complète est accessible sur le site [loremind-docs](https://loremind-docs.igmlcreation.fr/)
Pour l'installation, consultez le guide dans cette dernière .
## Fonctionnalités ## Fonctionnalités
- Gestion centralisée du Lore : Lieux, Factions, PNJ, et tous les éléments de votre univers - Gestion centralisée du Lore : Lieux, Factions, PNJ, et tous les éléments de votre univers
- Suivi de campagnes : Sessions, actions des joueurs, chronologie - Suivi de campagnes : Sessions, actions des joueurs, chronologie
- Moteur IA intégré : Génération automatique de contenu (PNJ, Villes, Quêtes) à partir de templates - Moteur IA intégré : Génération automatique de contenu (PNJ, Villes, Quêtes) à partir de templates
- Export vers FoundryVTT : Transfert structuré des données vers votre VTT préféré (en développement)
## Captures d'écran
### Page d'accueil
![Accueil](docs/maquettes/général/Accueil.png)
### Recherche
![Recherche](docs/maquettes/général/Ecran de recherche.png)
## Stack Technologique
LoreMind utilise une architecture distribuée pour séparer les responsabilités :
- **Frontend** : Angular (Interface utilisateur, affichage du lore, formulaires de templates)
- **Backend Core** : Java (Spring Boot) - Orchestration, persistance, export VTT
- **Backend IA** : Python - Traitement des LLM et génération de contenu
- **Base de données** : PostgreSQL avec JSONB pour les templates flexibles
## Architecture
### Backend Java (Domain-Driven Design & Hexagonal)
Le Backend Core respecte strictement :
- **Domain-Driven Design (DDD)** : Séparation en Bounded Contexts autonomes
- **Architecture Hexagonale (Ports et Adaptateurs)** : Domaine pur sans dépendances techniques
#### Bounded Contexts
- **LoreContext** : Gestion de l'encyclopédie de l'univers
- **CampaignContext** : Suivi des sessions et chronologie
- **GenerationContext** : Gestion des requêtes IA et templates
#### Couches
- **Domaine (Core)** : Entités métier pures et interfaces (Ports)
- **Application** : Orchestration des flux (Use Cases)
- **Infrastructure** : Implémentation technique (Adapters)
## Installation
Pour installer LoreMind chez vous (Docker requis), suivez le guide **[INSTALL.md](INSTALL.md)** — 3 étapes, 5 minutes chrono :
1. Télécharger `docker-compose.yml` + `.env.example` depuis la [dernière release](https://git.igmlcreation.fr/ietm64/LoreMindMJ/releases)
2. Renommer `.env.example``.env` et changer `POSTGRES_PASSWORD`
3. `docker compose up -d` → ouvrir http://localhost:8081
Mise à jour : `docker compose pull && docker compose up -d`.
## Développement (contributeurs)
Pour builder les images localement depuis les sources :
```bash
git clone https://git.igmlcreation.fr/ietm64/LoreMindMJ.git
cd LoreMindMJ
# Créer un docker-compose.override.yml local (voir docs de contrib)
docker compose up -d --build
```
## License ## License

View File

@@ -61,7 +61,16 @@ class OllamaLLMProvider:
async with httpx.AsyncClient(timeout=self._timeout) as client: async with httpx.AsyncClient(timeout=self._timeout) as client:
try: try:
response = await client.post(url, json=payload) response = await client.post(url, json=payload)
response.raise_for_status() if response.status_code >= 400:
body = response.text
try:
err_obj = json.loads(body)
err_msg = err_obj.get("error") or body
except json.JSONDecodeError:
err_msg = body
raise LLMProviderError(
f"Ollama HTTP {response.status_code} : {err_msg.strip()[:500]}"
)
except httpx.HTTPError as exc: except httpx.HTTPError as exc:
raise LLMProviderError( raise LLMProviderError(
f"Erreur lors de l'appel à Ollama : {exc}" f"Erreur lors de l'appel à Ollama : {exc}"
@@ -105,7 +114,20 @@ class OllamaLLMProvider:
async with httpx.AsyncClient(timeout=self._timeout) as client: async with httpx.AsyncClient(timeout=self._timeout) as client:
try: try:
async with client.stream("POST", url, json=payload) as response: async with client.stream("POST", url, json=payload) as response:
response.raise_for_status() if response.status_code >= 400:
# On lit le body d'erreur pour le remonter a l'utilisateur,
# sinon on ne voit que "500 Internal Server Error" sans
# savoir POURQUOI Ollama refuse (modele introuvable, OOM,
# num_ctx trop grand pour la VRAM, etc.).
body = (await response.aread()).decode("utf-8", errors="replace")
try:
err_obj = json.loads(body)
err_msg = err_obj.get("error") or body
except json.JSONDecodeError:
err_msg = body
raise LLMProviderError(
f"Ollama HTTP {response.status_code} : {err_msg.strip()[:500]}"
)
async for line in response.aiter_lines(): async for line in response.aiter_lines():
if not line.strip(): if not line.strip():
continue continue

View File

@@ -14,7 +14,7 @@
<groupId>com.loremind</groupId> <groupId>com.loremind</groupId>
<artifactId>loremind-core</artifactId> <artifactId>loremind-core</artifactId>
<version>0.6.11</version> <version>0.6.14</version>
<name>LoreMind Core</name> <name>LoreMind Core</name>
<description>Backend Core - Architecture Hexagonale</description> <description>Backend Core - Architecture Hexagonale</description>

View File

@@ -198,9 +198,18 @@ public class UpdateCheckService {
for (String key : new String[]{"service", "scope"}) { for (String key : new String[]{"service", "scope"}) {
String v = params.get(key); String v = params.get(key);
if (v != null) { if (v != null) {
// URLEncoder fait du "form encoding" qui transforme `:` et `/`
// en %3A et %2F. La plupart des registries (Docker Hub, Gitea)
// acceptent les deux, mais GHCR est strict et rejette le scope
// encode (403 DENIED). On preserve donc `:` et `/` dans la
// valeur, conformement a ce que GHCR attend
// (et que docker pull lui-meme envoie).
String encoded = URLEncoder.encode(v, StandardCharsets.UTF_8)
.replace("%3A", ":")
.replace("%2F", "/");
url.append(hasQuery ? '&' : '?') url.append(hasQuery ? '&' : '?')
.append(key).append('=') .append(key).append('=')
.append(URLEncoder.encode(v, StandardCharsets.UTF_8)); .append(encoded);
hasQuery = true; hasQuery = true;
} }
} }

View File

@@ -5,6 +5,12 @@ server.port=8080
# de WebFlux (utilisé uniquement pour WebClient côté adapter SSE vers le Brain). # de WebFlux (utilisé uniquement pour WebClient côté adapter SSE vers le Brain).
spring.main.web-application-type=servlet spring.main.web-application-type=servlet
# Pas de timeout sur les requetes async (StreamingResponseBody, SSE).
# Le defaut Tomcat coupe a 30s, ce qui interrompt le streaming d'un pull
# de modele Ollama (peut durer des dizaines de minutes pour un GGUF de 10+ Go).
# -1 = pas de timeout, on s'appuie sur la fermeture cote client ou cote upstream.
spring.mvc.async.request-timeout=-1
# Configuration de la base de donnees PostgreSQL # Configuration de la base de donnees PostgreSQL
# Valeurs surchargeables via variables d'env (cf. docker-compose.yml). # Valeurs surchargeables via variables d'env (cf. docker-compose.yml).
# En dev local, creez un fichier .env a la racine de core/ OU definissez les # En dev local, creez un fichier .env a la racine de core/ OU definissez les

View File

@@ -60,7 +60,12 @@ services:
" "
core: core:
image: ${REGISTRY:-git.igmlcreation.fr}/ietm64/core:${TAG:-latest} # Defaut : GHCR (registry public, reputation domaine elevee).
# Pour les anciennes installs qui pointaient sur Gitea, REGISTRY et
# IMAGE_NAMESPACE peuvent etre overrides dans .env :
# REGISTRY=git.igmlcreation.fr
# IMAGE_NAMESPACE=ietm64/ (le slash final est important : voir image: ci-dessous)
image: ${REGISTRY:-ghcr.io}/${IMAGE_NAMESPACE:-igmlcreation/loremind-}core:${TAG:-latest}
container_name: loremind-core container_name: loremind-core
labels: labels:
- "com.centurylinklabs.watchtower.enable=true" - "com.centurylinklabs.watchtower.enable=true"
@@ -84,8 +89,8 @@ services:
# Detection des mises a jour : interroge le registry et delegue le pull/restart # Detection des mises a jour : interroge le registry et delegue le pull/restart
# a Watchtower. Si WATCHTOWER_TOKEN est vide, la feature est desactivee # a Watchtower. Si WATCHTOWER_TOKEN est vide, la feature est desactivee
# (l'UI masque le badge et le bouton). # (l'UI masque le badge et le bouton).
UPDATE_CHECK_REGISTRY: ${REGISTRY:-git.igmlcreation.fr} UPDATE_CHECK_REGISTRY: ${REGISTRY:-ghcr.io}
UPDATE_CHECK_IMAGES: ietm64/core,ietm64/brain,ietm64/web UPDATE_CHECK_IMAGES: ${IMAGE_NAMESPACE:-igmlcreation/loremind-}core,${IMAGE_NAMESPACE:-igmlcreation/loremind-}brain,${IMAGE_NAMESPACE:-igmlcreation/loremind-}web
UPDATE_CHECK_TAG: ${TAG:-latest} UPDATE_CHECK_TAG: ${TAG:-latest}
WATCHTOWER_URL: http://watchtower:8080 WATCHTOWER_URL: http://watchtower:8080
WATCHTOWER_TOKEN: ${WATCHTOWER_TOKEN:-} WATCHTOWER_TOKEN: ${WATCHTOWER_TOKEN:-}
@@ -115,7 +120,7 @@ services:
restart: unless-stopped restart: unless-stopped
brain: brain:
image: ${REGISTRY:-git.igmlcreation.fr}/ietm64/brain:${TAG:-latest} image: ${REGISTRY:-ghcr.io}/${IMAGE_NAMESPACE:-igmlcreation/loremind-}brain:${TAG:-latest}
container_name: loremind-brain container_name: loremind-brain
labels: labels:
- "com.centurylinklabs.watchtower.enable=true" - "com.centurylinklabs.watchtower.enable=true"
@@ -138,7 +143,7 @@ services:
restart: unless-stopped restart: unless-stopped
web: web:
image: ${REGISTRY:-git.igmlcreation.fr}/ietm64/web:${TAG:-latest} image: ${REGISTRY:-ghcr.io}/${IMAGE_NAMESPACE:-igmlcreation/loremind-}web:${TAG:-latest}
container_name: loremind-web container_name: loremind-web
labels: labels:
- "com.centurylinklabs.watchtower.enable=true" - "com.centurylinklabs.watchtower.enable=true"

View File

@@ -29,7 +29,7 @@ déclaratif et auditable en quelques lignes.
## Linux (Debian / Ubuntu / Fedora / Arch) ## Linux (Debian / Ubuntu / Fedora / Arch)
```bash ```bash
curl -fsSL https://git.igmlcreation.fr/ietm64/loremind/raw/branch/main/installers/install.sh | bash curl -fsSL https://raw.githubusercontent.com/IGMLcreation/LoreMind/main/installers/install.sh | bash
``` ```
Le script : Le script :

View File

@@ -40,16 +40,16 @@
Auteur : ietm64 Auteur : ietm64
Licence : AGPL-3.0 Licence : AGPL-3.0
Projet : LoreMindMJ - assistant pour Maitres de Jeu de JDR Projet : LoreMindMJ - assistant pour Maitres de Jeu de JDR
Version : 0.6.11 Version : 0.6.14
.LINK .LINK
https://git.igmlcreation.fr/ietm64/loremind https://github.com/IGMLcreation/LoreMind
#> #>
[CmdletBinding()] [CmdletBinding()]
param( param(
[string]$InstallDir = "$env:LOCALAPPDATA\LoreMind", [string]$InstallDir = "$env:LOCALAPPDATA\LoreMind",
[string]$ComposeUrl = "https://git.igmlcreation.fr/ietm64/loremind/raw/branch/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
) )
@@ -316,7 +316,8 @@ $composeProfiles = $profilesList -join ','
$envContent = @" $envContent = @"
# Genere par install.ps1 le $(Get-Date -Format 'yyyy-MM-dd HH:mm') # Genere par install.ps1 le $(Get-Date -Format 'yyyy-MM-dd HH:mm')
REGISTRY=git.igmlcreation.fr REGISTRY=ghcr.io
IMAGE_NAMESPACE=igmlcreation/loremind-
TAG=latest TAG=latest
WEB_PORT=$WebPort WEB_PORT=$WebPort

View File

@@ -2,12 +2,12 @@
# ========================================================================== # ==========================================================================
# Installeur LoreMindMJ pour Linux (Debian/Ubuntu/Fedora/Arch) # Installeur LoreMindMJ pour Linux (Debian/Ubuntu/Fedora/Arch)
# Usage : # Usage :
# curl -fsSL https://git.igmlcreation.fr/ietm64/loremind/raw/branch/main/installers/install.sh | bash # curl -fsSL https://raw.githubusercontent.com/IGMLcreation/LoreMind/main/installers/install.sh | bash
# ========================================================================== # ==========================================================================
set -euo pipefail set -euo pipefail
INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/share/loremind}" INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/share/loremind}"
COMPOSE_URL="${COMPOSE_URL:-https://git.igmlcreation.fr/ietm64/loremind/raw/branch/main/docker-compose.yml}" COMPOSE_URL="${COMPOSE_URL:-https://raw.githubusercontent.com/IGMLcreation/LoreMind/main/docker-compose.yml}"
WEB_PORT="${WEB_PORT:-8081}" WEB_PORT="${WEB_PORT:-8081}"
NON_INTERACTIVE="${NON_INTERACTIVE:-0}" NON_INTERACTIVE="${NON_INTERACTIVE:-0}"
@@ -190,7 +190,8 @@ COMPOSE_PROFILES="$(IFS=,; echo "${PROFILES_ARR[*]}")"
cat > .env <<EOF cat > .env <<EOF
# Genere par install.sh le $(date '+%Y-%m-%d %H:%M') # Genere par install.sh le $(date '+%Y-%m-%d %H:%M')
REGISTRY=git.igmlcreation.fr REGISTRY=ghcr.io
IMAGE_NAMESPACE=igmlcreation/loremind-
TAG=latest TAG=latest
WEB_PORT=${WEB_PORT} WEB_PORT=${WEB_PORT}

View File

@@ -24,7 +24,7 @@
faciliter leur identification et suppression ulterieure. faciliter leur identification et suppression ulterieure.
.LINK .LINK
https://git.igmlcreation.fr/ietm64/loremind https://github.com/IGMLcreation/LoreMind
#> #>
[CmdletBinding()] [CmdletBinding()]

4
web/package-lock.json generated
View File

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

View File

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

View File

@@ -59,6 +59,10 @@ export class SettingsComponent implements OnInit {
pullTotal = 0; pullTotal = 0;
/** Souscription au flux de pull pour pouvoir l'annuler. */ /** Souscription au flux de pull pour pouvoir l'annuler. */
private pullSubscription: Subscription | null = null; private pullSubscription: Subscription | null = null;
/** True si on a recu un evenement {status:"success"} d'Ollama. Sans ca,
* une fermeture de stream (timeout proxy, perte reseau) ne doit PAS etre
* interpretee comme une reussite. */
private pullSucceeded = false;
/** Modele en cours de suppression (nom) pour disabler son bouton. */ /** Modele en cours de suppression (nom) pour disabler son bouton. */
deletingModel: string | null = null; deletingModel: string | null = null;
@@ -293,6 +297,9 @@ export class SettingsComponent implements OnInit {
if (event.status) this.pullStatus = event.status; if (event.status) this.pullStatus = event.status;
if (event.completed != null) this.pullCompleted = event.completed; if (event.completed != null) this.pullCompleted = event.completed;
if (event.total != null) this.pullTotal = event.total; if (event.total != null) this.pullTotal = event.total;
// Marqueur explicite : Ollama emet "success" en derniere ligne quand
// le pull est reellement complet (manifest + layers + verify).
if (event.status === 'success') this.pullSucceeded = true;
}, },
error: (err) => { error: (err) => {
this.errorMessage = this.extractError(err, `Echec du telechargement de ${name}.`); this.errorMessage = this.extractError(err, `Echec du telechargement de ${name}.`);
@@ -300,6 +307,14 @@ export class SettingsComponent implements OnInit {
}, },
complete: () => { complete: () => {
this.pullInProgress = false; this.pullInProgress = false;
if (!this.pullSucceeded) {
// Stream ferme sans 'success' final = connexion coupee
// (timeout proxy, perte reseau, ...). Le modele est probablement
// partiellement telecharge ; Ollama gardera les couches deja DL.
this.errorMessage = `Telechargement de ${name} interrompu avant la fin. Relancez pour reprendre.`;
this.refreshModels();
return;
}
this.successMessage = `Modele ${name} telecharge.`; this.successMessage = `Modele ${name} telecharge.`;
this.refreshModels(); this.refreshModels();
// Si l'utilisateur n'avait aucun modele, on selectionne celui-ci. // Si l'utilisateur n'avait aucun modele, on selectionne celui-ci.
@@ -326,6 +341,7 @@ export class SettingsComponent implements OnInit {
this.pullStatus = ''; this.pullStatus = '';
this.pullCompleted = 0; this.pullCompleted = 0;
this.pullTotal = 0; this.pullTotal = 0;
this.pullSucceeded = false;
if (this.pullSubscription) { if (this.pullSubscription) {
this.pullSubscription.unsubscribe(); this.pullSubscription.unsubscribe();
this.pullSubscription = null; this.pullSubscription = null;