Changement dans la config pour éviter les url en dur + mise en place d'un mode démo

This commit is contained in:
2026-04-23 17:15:08 +02:00
parent e3c8232e38
commit 83ac67471e
21 changed files with 155 additions and 38 deletions

View File

@@ -60,7 +60,8 @@
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"buildTarget": "web:build"
"buildTarget": "web:build",
"proxyConfig": "proxy.conf.json"
}
}
}

8
web/proxy.conf.json Normal file
View File

@@ -0,0 +1,8 @@
{
"/api": {
"target": "http://localhost:8080",
"secure": false,
"changeOrigin": true,
"logLevel": "warn"
}
}

View File

@@ -1,4 +1,5 @@
import { Routes } from '@angular/router';
import { hiddenInDemoGuard } from './guards/demo-mode.guard';
export const routes: Routes = [
{ path: 'lore', loadComponent: () => import('./lore/lore.component').then(m => m.LoreComponent) },
@@ -30,6 +31,8 @@ export const routes: Routes = [
{ path: 'game-systems', loadComponent: () => import('./game-systems/game-systems.component').then(m => m.GameSystemsComponent) },
{ path: 'game-systems/create', loadComponent: () => import('./game-systems/game-system-edit/game-system-edit.component').then(m => m.GameSystemEditComponent) },
{ path: 'game-systems/:id/edit', loadComponent: () => import('./game-systems/game-system-edit/game-system-edit.component').then(m => m.GameSystemEditComponent) },
{ path: 'settings', loadComponent: () => import('./settings/settings.component').then(m => m.SettingsComponent) },
// Routes masquees en mode demo : ajouter canActivate: [hiddenInDemoGuard]
// (a prevoir aussi sur la future route d'export VTT).
{ path: 'settings', canActivate: [hiddenInDemoGuard], loadComponent: () => import('./settings/settings.component').then(m => m.SettingsComponent) },
{ path: '', redirectTo: '/lore', pathMatch: 'full' }
];

View File

@@ -0,0 +1,17 @@
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { ConfigService } from '../services/config.service';
/**
* Bloque l'acces aux routes sensibles quand demoMode est actif et redirige
* vers la home. Defense UX ; le verrou serveur reste la source de verite.
*/
export const hiddenInDemoGuard: CanActivateFn = () => {
const config = inject(ConfigService);
const router = inject(Router);
if (config.demoMode) {
router.navigate(['/']);
return false;
}
return true;
};

View File

@@ -45,8 +45,8 @@ export type NarrativeEntityType = 'arc' | 'chapter' | 'scene' | 'character';
@Injectable({ providedIn: 'root' })
export class AiChatService {
private readonly loreEndpoint = 'http://localhost:8080/api/ai/chat/stream';
private readonly campaignEndpoint = 'http://localhost:8080/api/ai/chat/stream-campaign';
private readonly loreEndpoint = '/api/ai/chat/stream';
private readonly campaignEndpoint = '/api/ai/chat/stream-campaign';
/**
* Streame la réponse de l'IA pour un historique de messages donné (chat ancré Lore).

View File

@@ -30,7 +30,7 @@ export interface ChapterDeletionImpact {
providedIn: 'root'
})
export class CampaignService {
private apiUrl = 'http://localhost:8080/api/campaigns';
private apiUrl = '/api/campaigns';
constructor(private http: HttpClient) {}
@@ -60,73 +60,73 @@ export class CampaignService {
// ========== ARC ==========
getArcs(campaignId: string): Observable<Arc[]> {
return this.http.get<Arc[]>(`http://localhost:8080/api/arcs/campaign/${campaignId}`);
return this.http.get<Arc[]>(`/api/arcs/campaign/${campaignId}`);
}
getArcById(id: string): Observable<Arc> {
return this.http.get<Arc>(`http://localhost:8080/api/arcs/${id}`);
return this.http.get<Arc>(`/api/arcs/${id}`);
}
createArc(payload: ArcCreate): Observable<Arc> {
return this.http.post<Arc>('http://localhost:8080/api/arcs', payload);
return this.http.post<Arc>('/api/arcs', payload);
}
updateArc(id: string, payload: ArcCreate): Observable<Arc> {
return this.http.put<Arc>(`http://localhost:8080/api/arcs/${id}`, payload);
return this.http.put<Arc>(`/api/arcs/${id}`, payload);
}
deleteArc(id: string): Observable<void> {
return this.http.delete<void>(`http://localhost:8080/api/arcs/${id}`);
return this.http.delete<void>(`/api/arcs/${id}`);
}
getArcDeletionImpact(id: string): Observable<ArcDeletionImpact> {
return this.http.get<ArcDeletionImpact>(`http://localhost:8080/api/arcs/${id}/deletion-impact`);
return this.http.get<ArcDeletionImpact>(`/api/arcs/${id}/deletion-impact`);
}
// ========== CHAPTER ==========
getChapters(arcId: string): Observable<Chapter[]> {
return this.http.get<Chapter[]>(`http://localhost:8080/api/chapters/arc/${arcId}`);
return this.http.get<Chapter[]>(`/api/chapters/arc/${arcId}`);
}
getChapterById(id: string): Observable<Chapter> {
return this.http.get<Chapter>(`http://localhost:8080/api/chapters/${id}`);
return this.http.get<Chapter>(`/api/chapters/${id}`);
}
createChapter(payload: ChapterCreate): Observable<Chapter> {
return this.http.post<Chapter>('http://localhost:8080/api/chapters', payload);
return this.http.post<Chapter>('/api/chapters', payload);
}
updateChapter(id: string, payload: ChapterCreate): Observable<Chapter> {
return this.http.put<Chapter>(`http://localhost:8080/api/chapters/${id}`, payload);
return this.http.put<Chapter>(`/api/chapters/${id}`, payload);
}
deleteChapter(id: string): Observable<void> {
return this.http.delete<void>(`http://localhost:8080/api/chapters/${id}`);
return this.http.delete<void>(`/api/chapters/${id}`);
}
getChapterDeletionImpact(id: string): Observable<ChapterDeletionImpact> {
return this.http.get<ChapterDeletionImpact>(`http://localhost:8080/api/chapters/${id}/deletion-impact`);
return this.http.get<ChapterDeletionImpact>(`/api/chapters/${id}/deletion-impact`);
}
// ========== SCENE ==========
getScenes(chapterId: string): Observable<Scene[]> {
return this.http.get<Scene[]>(`http://localhost:8080/api/scenes/chapter/${chapterId}`);
return this.http.get<Scene[]>(`/api/scenes/chapter/${chapterId}`);
}
getSceneById(id: string): Observable<Scene> {
return this.http.get<Scene>(`http://localhost:8080/api/scenes/${id}`);
return this.http.get<Scene>(`/api/scenes/${id}`);
}
createScene(payload: SceneCreate): Observable<Scene> {
return this.http.post<Scene>('http://localhost:8080/api/scenes', payload);
return this.http.post<Scene>('/api/scenes', payload);
}
updateScene(id: string, payload: SceneCreate): Observable<Scene> {
return this.http.put<Scene>(`http://localhost:8080/api/scenes/${id}`, payload);
return this.http.put<Scene>(`/api/scenes/${id}`, payload);
}
deleteScene(id: string): Observable<void> {
return this.http.delete<void>(`http://localhost:8080/api/scenes/${id}`);
return this.http.delete<void>(`/api/scenes/${id}`);
}
search(q: string): Observable<Campaign[]> {

View File

@@ -8,7 +8,7 @@ import { Character, CharacterCreate } from './character.model';
*/
@Injectable({ providedIn: 'root' })
export class CharacterService {
private apiUrl = 'http://localhost:8080/api/characters';
private apiUrl = '/api/characters';
constructor(private http: HttpClient) {}

View File

@@ -0,0 +1,31 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
/**
* Configuration publique chargee une seule fois au demarrage via APP_INITIALIZER.
* Le flag demoMode bascule l'UI en mode vitrine (Settings/Export masques).
*/
export interface PublicConfig {
demoMode: boolean;
}
@Injectable({ providedIn: 'root' })
export class ConfigService {
private config: PublicConfig = { demoMode: false };
constructor(private http: HttpClient) {}
async load(): Promise<void> {
try {
this.config = await firstValueFrom(this.http.get<PublicConfig>('/api/config'));
} catch {
// Si l'endpoint n'est pas joignable au boot, on reste sur le default
// (demoMode=false) pour ne pas bloquer l'app en dev.
}
}
get demoMode(): boolean {
return this.config.demoMode;
}
}

View File

@@ -17,7 +17,7 @@ import {
*/
@Injectable({ providedIn: 'root' })
export class ConversationService {
private readonly apiUrl = 'http://localhost:8080/api/conversations';
private readonly apiUrl = '/api/conversations';
constructor(private http: HttpClient) {}

View File

@@ -8,7 +8,7 @@ import { GameSystem, GameSystemCreate } from './game-system.model';
*/
@Injectable({ providedIn: 'root' })
export class GameSystemService {
private apiUrl = 'http://localhost:8080/api/game-systems';
private apiUrl = '/api/game-systems';
constructor(private http: HttpClient) {}

View File

@@ -9,8 +9,8 @@ import { Image } from './image.model';
*/
@Injectable({ providedIn: 'root' })
export class ImageService {
/** Base absolue du backend — utile pour construire des URLs complètes (<img src>). */
readonly apiBase = 'http://localhost:8080';
/** Base du backend (vide = même origine que le front, résolue par le navigateur). */
readonly apiBase = '';
private apiUrl = `${this.apiBase}/api/images`;
constructor(private http: HttpClient) {}

View File

@@ -28,8 +28,8 @@ export interface LoreDeletionImpact {
providedIn: 'root'
})
export class LoreService {
private apiUrl = 'http://localhost:8080/api/lores';
private nodesUrl = 'http://localhost:8080/api/lore-nodes';
private apiUrl = '/api/lores';
private nodesUrl = '/api/lore-nodes';
constructor(private http: HttpClient) {}

View File

@@ -10,7 +10,7 @@ import { Page, PageCreate } from './page.model';
*/
@Injectable({ providedIn: 'root' })
export class PageService {
private apiUrl = 'http://localhost:8080/api/pages';
private apiUrl = '/api/pages';
constructor(private http: HttpClient) {}

View File

@@ -36,7 +36,7 @@ export interface OllamaModelInfo {
@Injectable({ providedIn: 'root' })
export class SettingsService {
private readonly apiUrl = 'http://localhost:8080/api/settings';
private readonly apiUrl = '/api/settings';
// HTTP Basic : le browser gere le prompt natif de credentials au premier 401.
// withCredentials=true pour que les creds soient renvoyees sur les appels

View File

@@ -9,7 +9,7 @@ import { Template, TemplateCreate } from './template.model';
*/
@Injectable({ providedIn: 'root' })
export class TemplateService {
private apiUrl = 'http://localhost:8080/api/templates';
private apiUrl = '/api/templates';
constructor(private http: HttpClient) {}

View File

@@ -53,11 +53,11 @@
<lucide-icon [img]="Dices" [size]="16"></lucide-icon>
<span>Systèmes de JDR</span>
</button>
<button class="tool-btn">
<button class="tool-btn" *ngIf="!config.demoMode">
<lucide-icon [img]="Download" [size]="16"></lucide-icon>
<span>Export VTT</span>
</button>
<button class="tool-btn" [class.active]="currentRoute.startsWith('/settings')" (click)="navigateTo('/settings')">
<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>
</button>

View File

@@ -4,6 +4,7 @@ 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';
// 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';
@@ -30,7 +31,8 @@ export class SidebarComponent {
constructor(
private router: Router,
private layoutService: LayoutService,
private globalSearch: GlobalSearchService
private globalSearch: GlobalSearchService,
public config: ConfigService
) {
this.router.events.subscribe(() => {
this.currentRoute = this.router.url;

View File

@@ -3,6 +3,8 @@ import { AppComponent } from './app/app.component';
import { PreloadAllModules, provideRouter, withPreloading } from '@angular/router';
import { routes } from './app/app.routes';
import { provideHttpClient } from '@angular/common/http';
import { APP_INITIALIZER } from '@angular/core';
import { ConfigService } from './app/services/config.service';
// withPreloading(PreloadAllModules) : une fois l'app initiale rendue, Angular
// telecharge en arriere-plan tous les chunks lazy-loades. Consequence : la
@@ -13,5 +15,11 @@ bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes, withPreloading(PreloadAllModules)),
provideHttpClient(),
{
provide: APP_INITIALIZER,
useFactory: (config: ConfigService) => () => config.load(),
deps: [ConfigService],
multi: true,
},
],
}).catch((err: Error) => console.error(err));