perf: optimize Docker build with layer caching and BuildKit support

This commit is contained in:
Bruno Charest 2025-10-22 23:13:09 -04:00
parent 58bd57543b
commit c030f91ebe
11 changed files with 172 additions and 36 deletions

14
.dockerignore Normal file
View File

@ -0,0 +1,14 @@
node_modules
dist
.git
.angular
.vscode
*.log
tmp
db/*.db
.env
.DS_Store
coverage
*.swp
*.swo
*~

View File

@ -4,17 +4,21 @@
FROM node:20-bullseye AS builder
WORKDIR /app
# Install dependencies
# Copy ONLY package files first (creates cacheable layer)
COPY package*.json ./
RUN npm ci
# Copy sources
# Install dependencies with cache mount for faster rebuilds
RUN --mount=type=cache,target=/root/.npm \
npm ci
# Copy source files AFTER npm install (only invalidates cache if code changes)
COPY . .
# Build Angular app (outputs to ./dist)
RUN npx ng build --configuration=production
# Build Angular app with cache mount for Angular's build cache
RUN --mount=type=cache,target=/app/.angular/cache \
npx ng build --configuration=production
# Prune dev dependencies to keep only production deps for runtime copy
# Prune dev dependencies to keep only production deps
RUN npm prune --omit=dev
# -------------------- Runtime stage --------------------

View File

@ -2,7 +2,7 @@
.SYNOPSIS
Construit l'image Docker avec toutes les dépendances requises via PowerShell et WSL (Debian).
.DESCRIPTION
Ce script prépare les dépendances et construit l'image Docker sous WSL (Debian).
Ce script prépare les dépendances et construit l'image Docker sous WSL (Debian) avec BuildKit activé pour des builds plus rapides.
.PARAMETRES
-full : Si présent, effectue une construction complète de l'image Docker (équivalent à --no-cache).
.EXEMPLE
@ -43,30 +43,79 @@ try {
exit 1
}
# Compose build command
# Check if .dockerignore exists, warn if not
$dockerignorePath = Join-Path $projectRoot ".dockerignore"
if (-not (Test-Path $dockerignorePath)) {
Write-Warning "Aucun fichier .dockerignore trouvé. Créez-en un pour optimiser le build."
Write-Host "Exemple de contenu :" -ForegroundColor Yellow
Write-Host @"
node_modules
dist
.git
.angular
.vscode
*.log
tmp
db/*.db
.env
"@ -ForegroundColor DarkGray
}
# Compose build command with BuildKit enabled
$noCache = if ($full) { '--no-cache' } else { '' }
$innerCmd = "cd '$wslProjectRoot' && docker build $noCache -t obsiviewer-angular:latest -f docker/Dockerfile ."
# Escape spaces in path for bash
$escapedPath = $wslProjectRoot -replace ' ','\\ '
# Build command parts
$cdCmd = "cd `"$wslProjectRoot`""
$dockerCmd = "docker build $noCache --progress=plain -t obsiviewer-angular:latest -f docker/Dockerfile ."
# Complete command with BuildKit
$innerCmd = "export DOCKER_BUILDKIT=1 && $cdCmd && $dockerCmd"
# Run build inside WSL Debian to use Linux Docker daemon
Write-Host "Construction de l'image Docker obsiviewer-angular:latest..." -ForegroundColor Cyan
Write-Host "BuildKit activé pour un build optimisé avec cache" -ForegroundColor Cyan
$buildResult = wsl -d Debian bash -lc $innerCmd 2>&1
# Display build output
$buildResult | ForEach-Object { Write-Host $_ }
if ($LASTEXITCODE -ne 0) {
Write-Error "Erreur lors de la construction de l'image Docker : $buildResult"
Write-Error "Erreur lors de la construction de l'image Docker (code: $LASTEXITCODE)"
exit 1
}
# Verify the image was built successfully
$verifyCmd = "docker image inspect obsiviewer-angular:latest"
Write-Host "Vérification de l'image construite..." -ForegroundColor Cyan
$verifyResult = wsl -d Debian bash -lc $verifyCmd 2>&1
$verifyCmd = "docker image inspect obsiviewer-angular:latest --format='{{.Size}}'"
Write-Host "`nVérification de l'image construite..." -ForegroundColor Cyan
$imageSize = wsl -d Debian bash -lc $verifyCmd 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Error "L'image Docker n'a pas été construite correctement : $verifyResult"
Write-Error "L'image Docker n'a pas été construite correctement : $imageSize"
exit 1
}
Write-Host "Image Docker obsiviewer-angular:latest construite avec succès via WSL Debian." -ForegroundColor Green
# Convert size to MB
$sizeMB = [math]::Round([long]$imageSize / 1MB, 2)
Write-Host "`n================================================" -ForegroundColor Green
Write-Host "✓ Image Docker construite avec succès" -ForegroundColor Green
Write-Host " Nom: obsiviewer-angular:latest" -ForegroundColor Green
Write-Host " Taille: $sizeMB MB" -ForegroundColor Green
Write-Host "================================================" -ForegroundColor Green
# Display tips for faster rebuilds
if (-not $full) {
Write-Host "`nConseils pour les prochains builds :" -ForegroundColor Cyan
Write-Host " • Les dépendances npm sont mises en cache" -ForegroundColor Gray
Write-Host " • Le cache Angular est préservé entre les builds" -ForegroundColor Gray
Write-Host " • Seuls les fichiers modifiés déclencheront un rebuild" -ForegroundColor Gray
}
}
catch {
Write-Error "Erreur lors de la construction de l'image Docker : $_"
exit 1
}
}

View File

@ -15,8 +15,8 @@ export interface ThemePrefs {
}
const DEFAULT_PREFS: ThemePrefs = {
mode: 'system',
theme: 'light',
mode: 'dark',
theme: 'nord',
language: 'fr'
};

View File

@ -21,6 +21,14 @@ import { TagFilterStore } from '../../core/stores/tag-filter.store';
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button>
</div>
<div *ngIf="quickLinkFilter() && getQuickLinkDisplay(quickLinkFilter()) as ql" class="flex items-center gap-2 text-xs">
<span class="inline-flex items-center gap-1 rounded-full bg-surface1 dark:bg-card text-main dark:text-main px-2 py-1">
{{ ql.icon }} {{ ql.name }}
</span>
<button type="button" (click)="clearQuickLinkFilter.emit()" class="rounded-full hover:bg-slate-500/10 dark:hover:bg-surface2/10 w-6 h-6 inline-flex items-center justify-center" title="Effacer le filtre">
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button>
</div>
<input type="text"
[value]="query()"
(input)="onQuery($any($event.target).value)"
@ -64,6 +72,7 @@ export class NotesListComponent {
@Output() openNote = new EventEmitter<string>();
@Output() queryChange = new EventEmitter<string>();
@Output() clearQuickLinkFilter = new EventEmitter<void>();
private store = inject(TagFilterStore);
private q = signal('');
@ -88,6 +97,14 @@ export class NotesListComponent {
const quickLink = this.quickLinkFilter();
let list = this.notes();
// Exclude trash notes by default unless specifically viewing trash
if (folder !== '.trash') {
list = list.filter(n => {
const filePath = (n.filePath || n.originalPath || '').toLowerCase().replace(/\\/g, '/');
return !filePath.startsWith('.trash/') && !filePath.includes('/.trash/');
});
}
if (folder) {
if (folder === '.trash') {
// All files anywhere under .trash (including subfolders)
@ -130,6 +147,19 @@ export class NotesListComponent {
return [...list].sort((a, b) => (score(b) - score(a)));
});
getQuickLinkDisplay(quickLink: string): { icon: string; name: string } | null {
const displays: Record<string, { icon: string; name: string }> = {
'favoris': { icon: '❤️', name: 'Favoris' },
'publish': { icon: '🌐', name: 'Publish' },
'draft': { icon: '📝', name: 'Draft' },
'template': { icon: '📑', name: 'Template' },
'task': { icon: '🗒️', name: 'Task' },
'private': { icon: '🔒', name: 'Private' },
'archive': { icon: '🗃️', name: 'Archive' }
};
return displays[quickLink] || null;
}
onQuery(v: string) {
this.q.set(v);
this.queryChange.emit(v);

View File

@ -68,7 +68,7 @@ import { TrashExplorerComponent } from '../../layout/sidebar/trash/trash-explore
<span class="text-xs text-muted transition-transform duration-200" [class.rotate-90]="!open.folders">{{ open.folders ? '▾' : '▸' }}</span>
</button>
<div *ngIf="open.folders" class="px-1 py-1">
<app-file-explorer [nodes]="nodes" [selectedNoteId]="selectedNoteId" [foldersOnly]="true" (folderSelected)="onFolder($event)" (fileSelected)="onSelect($event)"></app-file-explorer>
<app-file-explorer [nodes]="nodes" [selectedNoteId]="selectedNoteId" [foldersOnly]="true" [quickLinkFilter]="quickLinkFilter" (folderSelected)="onFolder($event)" (fileSelected)="onSelect($event)"></app-file-explorer>
</div>
</section>
@ -152,6 +152,7 @@ export class AppSidebarDrawerComponent {
@Input() selectedNoteId: string | null = null;
@Input() vaultName = '';
@Input() tags: TagInfo[] = [];
@Input() quickLinkFilter: 'favoris' | 'publish' | 'draft' | 'template' | 'task' | 'private' | 'archive' | null = null;
@Output() noteSelected = new EventEmitter<string>();
@Output() folderSelected = new EventEmitter<string>();
@Output() tagSelected = new EventEmitter<string>();

View File

@ -66,6 +66,7 @@ import { VaultService } from '../../../services/vault.service';
[nodes]="effectiveFileTree"
[selectedNoteId]="selectedNoteId"
[foldersOnly]="true"
[quickLinkFilter]="quickLinkFilter"
(folderSelected)="folderSelected.emit($event)"
(fileSelected)="fileSelected.emit($event)">
</app-file-explorer>
@ -145,6 +146,7 @@ export class NimbusSidebarComponent {
@Input() effectiveFileTree: VaultNode[] = [];
@Input() selectedNoteId: string | null = null;
@Input() tags: TagInfo[] = [];
@Input() quickLinkFilter: 'favoris' | 'publish' | 'draft' | 'template' | 'task' | 'private' | 'archive' | null = null;
@Output() toggleSidebarRequest = new EventEmitter<void>();
@Output() folderSelected = new EventEmitter<string>();
@ -156,7 +158,7 @@ export class NimbusSidebarComponent {
@Output() aboutSelected = new EventEmitter<void>();
env = environment;
open = { quick: true, folders: true, tags: false, trash: false, tests: true };
open = { quick: true, folders: false, tags: false, trash: false, tests: false };
private vault = inject(VaultService);
onQuickLink(id: string) { this.quickLinkSelected.emit(id); }

View File

@ -57,6 +57,7 @@ import { AboutPanelComponent } from '../../features/about/about-panel.component'
[effectiveFileTree]="effectiveFileTree"
[selectedNoteId]="selectedNoteId"
[tags]="tags"
[quickLinkFilter]="quickLinkFilter"
(toggleSidebarRequest)="toggleSidebarRequest.emit()"
(folderSelected)="onFolderSelected($event)"
(fileSelected)="noteSelected.emit($event)"
@ -100,7 +101,7 @@ import { AboutPanelComponent } from '../../features/about/about-panel.component'
<ng-container [ngSwitch]="f">
<app-quick-links *ngSwitchCase="'quick'" (quickLinkSelected)="onQuickLink($event)"></app-quick-links>
<div *ngSwitchCase="'folders'" class="p-2">
<app-file-explorer [nodes]="effectiveFileTree" [selectedNoteId]="selectedNoteId" [foldersOnly]="true" (folderSelected)="onFolderSelected($event)" (fileSelected)="noteSelected.emit($event)"></app-file-explorer>
<app-file-explorer [nodes]="effectiveFileTree" [selectedNoteId]="selectedNoteId" [foldersOnly]="true" [quickLinkFilter]="quickLinkFilter" (folderSelected)="onFolderSelected($event)" (fileSelected)="noteSelected.emit($event)"></app-file-explorer>
</div>
<div *ngSwitchCase="'tags'" class="p-2">
<ul class="space-y-0.5 text-sm">
@ -153,6 +154,7 @@ import { AboutPanelComponent } from '../../features/about/about-panel.component'
[query]="listQuery"
(openNote)="onOpenNote($event)"
(queryChange)="onQueryChange($event)"
(clearQuickLinkFilter)="onClearQuickLinkFilter()"
/>
</div>
</section>
@ -224,10 +226,10 @@ import { AboutPanelComponent } from '../../features/about/about-panel.component'
</div>
<div class="flex-1 overflow-hidden">
<div [hidden]="mobileNav.activeTab() !== 'sidebar'" class="h-full overflow-y-auto p-2" appScrollableOverlay>
<app-file-explorer [nodes]="effectiveFileTree" [selectedNoteId]="selectedNoteId" [foldersOnly]="true" (folderSelected)="onFolderSelected($event)" (fileSelected)="onOpenNote($event)"></app-file-explorer>
<app-file-explorer [nodes]="effectiveFileTree" [selectedNoteId]="selectedNoteId" [foldersOnly]="true" [quickLinkFilter]="quickLinkFilter" (folderSelected)="onFolderSelected($event)" (fileSelected)="onOpenNote($event)"></app-file-explorer>
</div>
<div [hidden]="mobileNav.activeTab() !== 'list'" class="h-full overflow-y-auto" appScrollableOverlay>
<app-notes-list [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [quickLinkFilter]="quickLinkFilter" [query]="listQuery" (queryChange)="onQueryChange($event)" (openNote)="onOpenNote($event)"></app-notes-list>
<app-notes-list [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [quickLinkFilter]="quickLinkFilter" [query]="listQuery" (queryChange)="onQueryChange($event)" (openNote)="onOpenNote($event)" (clearQuickLinkFilter)="onClearQuickLinkFilter()"></app-notes-list>
</div>
<div [hidden]="mobileNav.activeTab() !== 'page'" class="note-content-area h-full overflow-y-auto px-3 py-4" appScrollableOverlay>
<app-markdown-playground *ngIf="activeView === 'markdown-playground'"></app-markdown-playground>
@ -244,6 +246,7 @@ import { AboutPanelComponent } from '../../features/about/about-panel.component'
[selectedNoteId]="selectedNoteId"
[vaultName]="vaultName"
[tags]="tags"
[quickLinkFilter]="quickLinkFilter"
(noteSelected)="onNoteSelectedMobile($event)"
(folderSelected)="onFolderSelectedFromDrawer($event)"
(tagSelected)="onTagSelected($event)"
@ -255,7 +258,7 @@ import { AboutPanelComponent } from '../../features/about/about-panel.component'
@if (mobileNav.activeTab() === 'list') {
<div class="h-full flex flex-col overflow-hidden animate-fadeIn">
<app-notes-list class="flex-1" [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [quickLinkFilter]="quickLinkFilter" [query]="listQuery" (queryChange)="onQueryChange($event)" (openNote)="onNoteSelectedMobile($event)"></app-notes-list>
<app-notes-list class="flex-1" [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [quickLinkFilter]="quickLinkFilter" [query]="listQuery" (queryChange)="onQueryChange($event)" (openNote)="onNoteSelectedMobile($event)" (clearQuickLinkFilter)="onClearQuickLinkFilter()"></app-notes-list>
</div>
}
@ -353,6 +356,14 @@ export class AppShellNimbusLayoutComponent {
const quickLink = this.quickLinkFilter;
let list = this.vault.allNotes();
// Exclude trash notes by default unless specifically viewing trash
if (folder !== '.trash') {
list = list.filter(n => {
const filePath = (n.filePath || n.originalPath || '').toLowerCase().replace(/\\/g, '/');
return !filePath.startsWith('.trash/') && !filePath.includes('/.trash/');
});
}
if (folder) {
if (folder === '.trash') {
// All files anywhere under .trash (including subfolders)
@ -603,7 +614,11 @@ export class AppShellNimbusLayoutComponent {
this.helpPageRequested.emit();
}
onAboutSelected(): void {
this.showAboutPanel = true;
onClearQuickLinkFilter(): void {
this.folderFilter = null;
this.tagFilter = null;
this.quickLinkFilter = null;
this.listQuery = '';
this.autoSelectFirstNote();
}
}

View File

@ -66,12 +66,33 @@ export class FileExplorerComponent {
selectedNoteId = input<string | null>(null);
foldersOnly = input<boolean>(false);
useTrashCounts = input<boolean>(false);
quickLinkFilter = input<'favoris' | 'publish' | 'draft' | 'template' | 'task' | 'private' | 'archive' | null>(null);
fileSelected = output<string>();
folderSelected = output<string>();
private vaultService = inject(VaultService);
folderCount(path: string): number {
const quickLink = this.quickLinkFilter();
if (quickLink) {
// Compute filtered count
let filteredNotes = this.vaultService.allNotes().filter(note => {
const frontmatter = note.frontmatter || {};
return frontmatter[quickLink] === true;
});
// Apply folder filter
if (path) {
const folder = path.toLowerCase().replace(/^\/+|\/+$/g, '');
filteredNotes = filteredNotes.filter(note => {
const originalPath = (note.originalPath || '').toLowerCase().replace(/^\/+|\/+$/g, '');
return originalPath === folder || originalPath.startsWith(folder + '/');
});
}
return filteredNotes.length;
}
if (this.useTrashCounts()) {
const counts = this.vaultService.trashFolderCounts();
const raw = (path || '').replace(/\\/g, '/');

View File

@ -11,11 +11,11 @@ aliases: []
status: en-cours
publish: true
favoris: true
template: false
template: true
task: true
archive: false
draft: false
private: false
archive: true
draft: true
private: true
---
# Archived Note

View File

@ -4,18 +4,18 @@ auteur: Bruno Charest
creation_date: 2025-10-19T11:13:12-04:00
modification_date: 2025-10-19T12:09:46-04:00
catégorie: ""
tags:
- bruno
- configuration
aliases: []
status: en-cours
publish: true
favoris: true
template: false
template: true
task: true
archive: false
draft: false
private: false
tags:
- bruno
- configuration
draft: true
private: true
---
# Archived Note