Ajout d'un script pour installation automatique du produit
Ajout d'une partie mise à jour automatique : plus besoin de docker pull en ligne de commande ; on peut passer par l'interface Refactoring partie Java pour respecter d'avantage le DDD : plus de jackson dans la partie domain Passage version 0.6.6
This commit is contained in:
4
web/package-lock.json
generated
4
web/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "loremind-web",
|
||||
"version": "0.6.5",
|
||||
"version": "0.6.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "loremind-web",
|
||||
"version": "0.6.5",
|
||||
"version": "0.6.6",
|
||||
"dependencies": {
|
||||
"@angular/animations": "^17.0.0",
|
||||
"@angular/common": "^17.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "loremind-web",
|
||||
"version": "0.6.5",
|
||||
"version": "0.6.6",
|
||||
"description": "LoreMind Frontend - Angular",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
|
||||
@@ -8,11 +8,12 @@ import { firstValueFrom } from 'rxjs';
|
||||
*/
|
||||
export interface PublicConfig {
|
||||
demoMode: boolean;
|
||||
updateCheckEnabled: boolean;
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ConfigService {
|
||||
private config: PublicConfig = { demoMode: false };
|
||||
private config: PublicConfig = { demoMode: false, updateCheckEnabled: false };
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
@@ -28,4 +29,8 @@ export class ConfigService {
|
||||
get demoMode(): boolean {
|
||||
return this.config.demoMode;
|
||||
}
|
||||
|
||||
get updateCheckEnabled(): boolean {
|
||||
return this.config.updateCheckEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
62
web/src/app/services/updates.service.ts
Normal file
62
web/src/app/services/updates.service.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { BehaviorSubject, Observable, catchError, of, tap } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Reflet de UpdateCheckService.UpdateStatus cote backend.
|
||||
*/
|
||||
export interface ImageStatus {
|
||||
image: string;
|
||||
localDigest: string | null;
|
||||
remoteDigest: string | null;
|
||||
updateAvailable: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateStatus {
|
||||
enabled: boolean;
|
||||
updateAvailable: boolean;
|
||||
images: ImageStatus[];
|
||||
checkedAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service de detection / declenchement des mises a jour des conteneurs
|
||||
* LoreMind. Endpoints proteges par HTTP Basic (admin) — withCredentials
|
||||
* comme pour SettingsService.
|
||||
*
|
||||
* `updateAvailable$` est un signal global consomme par la sidebar pour
|
||||
* afficher un badge. Il est rafraichi via {@link checkNow}.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class UpdatesService {
|
||||
private readonly apiUrl = '/api/admin/updates';
|
||||
private readonly authOptions = { withCredentials: true };
|
||||
|
||||
private readonly _updateAvailable$ = new BehaviorSubject<boolean>(false);
|
||||
readonly updateAvailable$ = this._updateAvailable$.asObservable();
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
/**
|
||||
* Interroge le backend. Met a jour `updateAvailable$` au passage.
|
||||
* Renvoie `null` en cas d'erreur (pas authentifie, feature off, etc.)
|
||||
* pour ne pas faire crasher l'UI au boot.
|
||||
*/
|
||||
checkNow(): Observable<UpdateStatus | null> {
|
||||
return this.http.get<UpdateStatus>(`${this.apiUrl}/check`, this.authOptions).pipe(
|
||||
tap(s => this._updateAvailable$.next(!!s?.updateAvailable)),
|
||||
catchError(() => {
|
||||
this._updateAvailable$.next(false);
|
||||
return of(null);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
apply(): Observable<{ status: string; message: string } | null> {
|
||||
return this.http.post<{ status: string; message: string }>(
|
||||
`${this.apiUrl}/apply`, null, this.authOptions
|
||||
).pipe(
|
||||
catchError(() => of(null))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -148,4 +148,54 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Bloc Mises a jour -->
|
||||
<section class="card" *ngIf="config.updateCheckEnabled">
|
||||
<h2>Mises a jour</h2>
|
||||
<p class="hint">Verifie aupres du registry Docker si une nouvelle version
|
||||
des conteneurs (core, brain, web) est disponible. Postgres et MinIO sont
|
||||
exclus — ils sont mis a jour manuellement.</p>
|
||||
|
||||
<div class="form-row">
|
||||
<button type="button" class="btn-secondary" (click)="checkUpdates()" [disabled]="updateChecking">
|
||||
<lucide-icon [img]="RefreshCw" [size]="14"></lucide-icon>
|
||||
<span>{{ updateChecking ? 'Verification...' : 'Verifier maintenant' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div *ngIf="updateStatus && !updateStatus.enabled" class="hint">
|
||||
Feature non configuree (WATCHTOWER_TOKEN absent).
|
||||
</div>
|
||||
|
||||
<div *ngIf="updateStatus?.enabled">
|
||||
<div *ngIf="updateStatus?.updateAvailable" class="alert alert-success">
|
||||
<lucide-icon [img]="Download" [size]="16"></lucide-icon>
|
||||
<span>Une mise a jour est disponible.</span>
|
||||
</div>
|
||||
<div *ngIf="!updateStatus?.updateAvailable" class="hint">
|
||||
Tout est a jour (verifie le {{ updateStatus?.checkedAt | date:'short' }}).
|
||||
</div>
|
||||
|
||||
<ul class="update-images" *ngIf="updateStatus?.images?.length">
|
||||
<li *ngFor="let img of updateStatus?.images">
|
||||
<strong>{{ img.image }}</strong>
|
||||
<span *ngIf="img.updateAvailable" class="badge-update">MAJ dispo</span>
|
||||
<span *ngIf="!img.updateAvailable && img.remoteDigest" class="badge-ok">a jour</span>
|
||||
<span *ngIf="!img.remoteDigest" class="badge-warn">indisponible</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="form-row" *ngIf="updateStatus?.updateAvailable">
|
||||
<button type="button" class="btn-primary" (click)="applyUpdate()" [disabled]="updateApplying">
|
||||
<lucide-icon [img]="Download" [size]="16"></lucide-icon>
|
||||
<span>{{ updateApplying ? 'Mise a jour en cours...' : 'Mettre a jour maintenant' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div *ngIf="updateMessage" class="alert alert-success">
|
||||
<lucide-icon [img]="Check" [size]="16"></lucide-icon>
|
||||
<span>{{ updateMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -153,3 +153,46 @@
|
||||
width: 100%;
|
||||
accent-color: #6c63ff;
|
||||
}
|
||||
|
||||
.update-images {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0.75rem 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.update-images li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
padding: 0.4rem 0.6rem;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.badge-update {
|
||||
margin-left: auto;
|
||||
background: #6c63ff;
|
||||
color: white;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.badge-ok {
|
||||
margin-left: auto;
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
color: #81c784;
|
||||
font-size: 0.7rem;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.badge-warn {
|
||||
margin-left: auto;
|
||||
background: rgba(255, 152, 0, 0.2);
|
||||
color: #ffb74d;
|
||||
font-size: 0.7rem;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ import { Component, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { LucideAngularModule, ArrowLeft, RefreshCw, Save, Check, AlertCircle } from 'lucide-angular';
|
||||
import { LucideAngularModule, ArrowLeft, RefreshCw, Save, Check, AlertCircle, Download } from 'lucide-angular';
|
||||
import { SettingsService, AppSettings, AppSettingsUpdate, OneMinModelGroup } from '../services/settings.service';
|
||||
import { UpdatesService, UpdateStatus } from '../services/updates.service';
|
||||
import { ConfigService } from '../services/config.service';
|
||||
|
||||
/**
|
||||
* Ecran de parametrage du LLM utilise par le Brain.
|
||||
@@ -30,6 +32,13 @@ export class SettingsComponent implements OnInit {
|
||||
readonly Save = Save;
|
||||
readonly Check = Check;
|
||||
readonly AlertCircle = AlertCircle;
|
||||
readonly Download = Download;
|
||||
|
||||
// Mises a jour conteneurs
|
||||
updateStatus: UpdateStatus | null = null;
|
||||
updateChecking = false;
|
||||
updateApplying = false;
|
||||
updateMessage = '';
|
||||
|
||||
settings: AppSettings | null = null;
|
||||
ollamaModels: string[] = [];
|
||||
@@ -61,11 +70,51 @@ export class SettingsComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
private settingsService: SettingsService,
|
||||
private router: Router
|
||||
private router: Router,
|
||||
private updatesService: UpdatesService,
|
||||
public config: ConfigService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadSettings();
|
||||
if (this.config.updateCheckEnabled) {
|
||||
this.checkUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
checkUpdates(): void {
|
||||
this.updateChecking = true;
|
||||
this.updateMessage = '';
|
||||
this.updatesService.checkNow().subscribe({
|
||||
next: (s) => {
|
||||
this.updateStatus = s;
|
||||
this.updateChecking = false;
|
||||
},
|
||||
error: () => {
|
||||
this.updateChecking = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
applyUpdate(): void {
|
||||
if (!confirm('Telecharger et redemarrer les conteneurs maintenant ? L\'app sera indisponible quelques secondes.')) {
|
||||
return;
|
||||
}
|
||||
this.updateApplying = true;
|
||||
this.updateMessage = '';
|
||||
this.updatesService.apply().subscribe({
|
||||
next: (r) => {
|
||||
this.updateApplying = false;
|
||||
// Le redemarrage de core peut couper la connexion avant la reponse —
|
||||
// dans ce cas r vaut null (gere par catchError dans le service).
|
||||
this.updateMessage = r?.message
|
||||
?? 'Mise a jour declenchee. Rechargez la page dans 30s.';
|
||||
},
|
||||
error: () => {
|
||||
this.updateApplying = false;
|
||||
this.updateMessage = 'Mise a jour declenchee. Rechargez la page dans 30s.';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadSettings(): void {
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
<button class="tool-btn" *ngIf="!config.demoMode" [class.active]="currentRoute.startsWith('/settings')" (click)="navigateTo('/settings')">
|
||||
<lucide-icon [img]="Settings" [size]="16"></lucide-icon>
|
||||
<span>Paramètres</span>
|
||||
<span class="update-badge" *ngIf="updateAvailable$ | async" title="Mise a jour disponible">MAJ</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -178,6 +178,23 @@
|
||||
border: 1px solid #3a3f55;
|
||||
}
|
||||
|
||||
.update-badge {
|
||||
margin-left: auto;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
background: #6c63ff;
|
||||
color: white;
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
animation: update-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes update-pulse {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(108, 99, 255, 0.5); }
|
||||
50% { box-shadow: 0 0 0 4px rgba(108, 99, 255, 0); }
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #1e1e3a;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { AsyncPipe, NgIf, NgFor } from '@angular/common';
|
||||
import { Router } from '@angular/router';
|
||||
import { LucideAngularModule, Search, Download, Settings, ArrowLeft, Dices } from 'lucide-angular';
|
||||
import { LayoutService } from '../services/layout.service';
|
||||
import { GlobalSearchService } from '../services/global-search.service';
|
||||
import { ConfigService } from '../services/config.service';
|
||||
import { UpdatesService } from '../services/updates.service';
|
||||
// Single source of truth pour la version affichée dans le footer :
|
||||
// on lit directement package.json à la compilation (resolveJsonModule).
|
||||
import packageJson from '../../../package.json';
|
||||
@@ -16,7 +17,7 @@ import packageJson from '../../../package.json';
|
||||
templateUrl: './sidebar.component.html',
|
||||
styleUrls: ['./sidebar.component.scss']
|
||||
})
|
||||
export class SidebarComponent {
|
||||
export class SidebarComponent implements OnInit {
|
||||
currentRoute = '';
|
||||
|
||||
readonly Search = Search;
|
||||
@@ -27,18 +28,30 @@ export class SidebarComponent {
|
||||
|
||||
readonly layoutConfig$ = this.layoutService.secondarySidebar$;
|
||||
readonly appVersion = packageJson.version;
|
||||
readonly updateAvailable$ = this.updates.updateAvailable$;
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private layoutService: LayoutService,
|
||||
private globalSearch: GlobalSearchService,
|
||||
public config: ConfigService
|
||||
public config: ConfigService,
|
||||
private updates: UpdatesService
|
||||
) {
|
||||
this.router.events.subscribe(() => {
|
||||
this.currentRoute = this.router.url;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Premier check au boot uniquement si la feature est activee + mode non-demo.
|
||||
// L'erreur 401 (admin non auth) est silencieusement ignoree par le service —
|
||||
// le badge ne s'affichera que si l'utilisateur est passe par /settings et a
|
||||
// saisi ses credentials HTTP Basic. Comportement attendu mono-utilisateur.
|
||||
if (this.config.updateCheckEnabled && !this.config.demoMode) {
|
||||
this.updates.checkNow().subscribe();
|
||||
}
|
||||
}
|
||||
|
||||
navigateTo(route: string): void {
|
||||
this.router.navigate([route]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user