Mise en place de l'import / export des données pour pouvoir sauvegarder les lores / campagnes
This commit is contained in:
@@ -66,6 +66,13 @@ export interface OllamaModelInfo {
|
||||
context_length: number;
|
||||
}
|
||||
|
||||
/** Resultat d'un import de donnees (compteurs par type + images). */
|
||||
export interface ImportResult {
|
||||
created: Record<string, number>;
|
||||
imagesUploaded: number;
|
||||
imagesReused: number;
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SettingsService {
|
||||
private readonly apiUrl = '/api/settings';
|
||||
@@ -85,6 +92,21 @@ export class SettingsService {
|
||||
return this.http.put<AppSettings>(this.apiUrl, patch, this.authOptions);
|
||||
}
|
||||
|
||||
// --- Export / Import des donnees (sauvegarde & transfert d'instance) -------
|
||||
|
||||
/** Telecharge l'export complet du contenu (zip : data.json + images). */
|
||||
exportData(): Observable<Blob> {
|
||||
return this.http.get('/api/admin/data/export',
|
||||
{ withCredentials: true, responseType: 'blob' });
|
||||
}
|
||||
|
||||
/** Importe un zip d'export en mode FUSION (ajoute, ne remplace pas). */
|
||||
importData(file: File): Observable<ImportResult> {
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
return this.http.post<ImportResult>('/api/admin/data/import', form, this.authOptions);
|
||||
}
|
||||
|
||||
listOllamaModels(): Observable<{ models: string[] }> {
|
||||
return this.http.get<{ models: string[] }>(`${this.apiUrl}/models/ollama`, this.authOptions);
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
<label for="onemin-key">{{ 'settings.apiKey.label' | translate }}</label>
|
||||
<input
|
||||
id="onemin-key"
|
||||
type="password"
|
||||
type="password" autocomplete="new-password" data-1p-ignore data-lpignore="true" data-bwignore
|
||||
[(ngModel)]="oneminApiKeyInput"
|
||||
[placeholder]="(settings.onemin_api_key_set ? 'settings.apiKey.placeholderSet' : 'settings.apiKey.placeholderOnemin') | translate">
|
||||
@if (settings.onemin_api_key_set) {
|
||||
@@ -149,7 +149,7 @@
|
||||
<label for="openrouter-key">{{ 'settings.apiKey.label' | translate }}</label>
|
||||
<input
|
||||
id="openrouter-key"
|
||||
type="password"
|
||||
type="password" autocomplete="new-password" data-1p-ignore data-lpignore="true" data-bwignore
|
||||
[(ngModel)]="openrouterApiKeyInput"
|
||||
[placeholder]="(settings.openrouter_api_key_set ? 'settings.apiKey.placeholderSet' : 'settings.apiKey.placeholderOpenrouter') | translate">
|
||||
@if (settings.openrouter_api_key_set) {
|
||||
@@ -183,7 +183,7 @@
|
||||
<label for="mistral-key">{{ 'settings.apiKey.label' | translate }}</label>
|
||||
<input
|
||||
id="mistral-key"
|
||||
type="password"
|
||||
type="password" autocomplete="new-password" data-1p-ignore data-lpignore="true" data-bwignore
|
||||
[(ngModel)]="mistralApiKeyInput"
|
||||
[placeholder]="(settings.mistral_api_key_set ? 'settings.apiKey.placeholderSet' : 'settings.apiKey.placeholderMistral') | translate">
|
||||
@if (settings.mistral_api_key_set) {
|
||||
@@ -213,7 +213,7 @@
|
||||
<label for="gemini-key">{{ 'settings.apiKey.label' | translate }}</label>
|
||||
<input
|
||||
id="gemini-key"
|
||||
type="password"
|
||||
type="password" autocomplete="new-password" data-1p-ignore data-lpignore="true" data-bwignore
|
||||
[(ngModel)]="geminiApiKeyInput"
|
||||
[placeholder]="(settings.gemini_api_key_set ? 'settings.apiKey.placeholderSet' : 'settings.apiKey.placeholderGemini') | translate">
|
||||
@if (settings.gemini_api_key_set) {
|
||||
@@ -285,7 +285,7 @@
|
||||
<span class="key-state">{{ 'settings.apiKey.configured' | translate }}</span>
|
||||
}
|
||||
</label>
|
||||
<input id="emb-mistral-key" type="password" [(ngModel)]="mistralApiKeyInput"
|
||||
<input id="emb-mistral-key" type="password" autocomplete="new-password" data-1p-ignore data-lpignore="true" data-bwignore [(ngModel)]="mistralApiKeyInput"
|
||||
[placeholder]="(settings.mistral_api_key_set ? 'settings.apiKey.placeholderSet' : 'settings.apiKey.placeholderMistral') | translate">
|
||||
@if (settings.mistral_api_key_set) {
|
||||
<label class="checkbox">
|
||||
@@ -372,6 +372,33 @@
|
||||
<!-- Mises a jour conteneurs + licence Patreon + canal beta (sous-composant) -->
|
||||
<app-settings-updates-section></app-settings-updates-section>
|
||||
|
||||
<!-- Sauvegarde / Restauration des donnees (export & import) -->
|
||||
<section class="card">
|
||||
<h2>{{ 'settings.data.title' | translate }}</h2>
|
||||
<p class="hint" [innerHTML]="'settings.data.hint' | translate"></p>
|
||||
<div class="actions">
|
||||
<button type="button" class="btn-secondary" (click)="exportData()" [disabled]="exporting || importing">
|
||||
<span>{{ (exporting ? 'settings.data.exporting' : 'settings.data.exportBtn') | translate }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn-secondary" (click)="importInput.click()" [disabled]="exporting || importing">
|
||||
<span>{{ (importing ? 'settings.data.importing' : 'settings.data.importBtn') | translate }}</span>
|
||||
</button>
|
||||
<input #importInput type="file" accept=".zip,application/zip" hidden (change)="onImportFileSelected($event)">
|
||||
</div>
|
||||
@if (dataError) {
|
||||
<div class="alert alert-error">
|
||||
<lucide-icon [img]="AlertCircle" [size]="16"></lucide-icon>
|
||||
<span>{{ dataError }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (dataMessage) {
|
||||
<div class="alert alert-success">
|
||||
<lucide-icon [img]="Check" [size]="16"></lucide-icon>
|
||||
<span>{{ dataMessage }}</span>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
@if (settings) {
|
||||
<div class="actions">
|
||||
<button class="btn-primary" (click)="save()" [disabled]="saving">
|
||||
|
||||
@@ -4,7 +4,7 @@ import { FormsModule } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { LucideAngularModule, ArrowLeft, RefreshCw, Save, Check, AlertCircle, Plus } from 'lucide-angular';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { SettingsService, AppSettings, AppSettingsUpdate, OneMinModelGroup, OpenRouterModel, MistralModel, GeminiModel } from '../services/settings.service';
|
||||
import { SettingsService, AppSettings, AppSettingsUpdate, OneMinModelGroup, OpenRouterModel, MistralModel, GeminiModel, ImportResult } from '../services/settings.service';
|
||||
import { LayoutService } from '../services/layout.service';
|
||||
import { UpdatesSectionComponent } from './updates-section/updates-section.component';
|
||||
import { OllamaModelManagerComponent } from './ollama-model-manager/ollama-model-manager.component';
|
||||
@@ -332,6 +332,60 @@ export class SettingsComponent implements OnInit {
|
||||
this.router.navigate(['/lore']);
|
||||
}
|
||||
|
||||
// --- Sauvegarde / Restauration des donnees --------------------------------
|
||||
|
||||
exporting = false;
|
||||
importing = false;
|
||||
dataMessage = '';
|
||||
dataError = '';
|
||||
|
||||
/** Telecharge l'export complet (zip) via un lien temporaire. */
|
||||
exportData(): void {
|
||||
this.exporting = true;
|
||||
this.dataMessage = '';
|
||||
this.dataError = '';
|
||||
this.settingsService.exportData().subscribe({
|
||||
next: (blob) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'loremind-export.zip';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
this.exporting = false;
|
||||
},
|
||||
error: (err) => {
|
||||
this.dataError = this.extractError(err, this.translate.instant('settings.data.exportError'));
|
||||
this.exporting = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Déclenché par le sélecteur de fichier : importe le zip choisi (fusion). */
|
||||
onImportFileSelected(event: Event): void {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
input.value = ''; // permet de re-sélectionner le même fichier
|
||||
if (!file) return;
|
||||
|
||||
if (!confirm(this.translate.instant('settings.data.importConfirm'))) return;
|
||||
|
||||
this.importing = true;
|
||||
this.dataMessage = '';
|
||||
this.dataError = '';
|
||||
this.settingsService.importData(file).subscribe({
|
||||
next: (result: ImportResult) => {
|
||||
const total = Object.values(result.created).reduce((a, b) => a + b, 0);
|
||||
this.dataMessage = this.translate.instant('settings.data.importSuccess', { count: total });
|
||||
this.importing = false;
|
||||
},
|
||||
error: (err) => {
|
||||
this.dataError = this.extractError(err, this.translate.instant('settings.data.importError'));
|
||||
this.importing = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private extractError(err: any, fallback: string): string {
|
||||
if (err?.error?.detail) return String(err.error.detail);
|
||||
if (err?.message) return err.message;
|
||||
|
||||
Reference in New Issue
Block a user