From c030f91ebefb494b01d99e62078a8cd58554e060 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Wed, 22 Oct 2025 23:13:09 -0400 Subject: [PATCH] perf: optimize Docker build with layer caching and BuildKit support --- .dockerignore | 14 ++++ docker/Dockerfile | 16 +++-- docker/build-img.ps1 | 69 ++++++++++++++++--- src/app/core/services/theme.service.ts | 4 +- src/app/features/list/notes-list.component.ts | 30 ++++++++ .../sidebar/app-sidebar-drawer.component.ts | 3 +- .../sidebar/nimbus-sidebar.component.ts | 4 +- .../app-shell-nimbus.component.ts | 27 ++++++-- .../file-explorer/file-explorer.component.ts | 21 ++++++ vault/.trash/archive/archived-note.md | 8 +-- vault/.trash/archive/archived-note.md.bak | 12 ++-- 11 files changed, 172 insertions(+), 36 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..81654b2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +node_modules +dist +.git +.angular +.vscode +*.log +tmp +db/*.db +.env +.DS_Store +coverage +*.swp +*.swo +*~ diff --git a/docker/Dockerfile b/docker/Dockerfile index dd21ecc..48cab98 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -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 -------------------- diff --git a/docker/build-img.ps1 b/docker/build-img.ps1 index c768d3f..5d72c15 100644 --- a/docker/build-img.ps1 +++ b/docker/build-img.ps1 @@ -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 -} +} \ No newline at end of file diff --git a/src/app/core/services/theme.service.ts b/src/app/core/services/theme.service.ts index 4927cb6..3d8aad5 100644 --- a/src/app/core/services/theme.service.ts +++ b/src/app/core/services/theme.service.ts @@ -15,8 +15,8 @@ export interface ThemePrefs { } const DEFAULT_PREFS: ThemePrefs = { - mode: 'system', - theme: 'light', + mode: 'dark', + theme: 'nord', language: 'fr' }; diff --git a/src/app/features/list/notes-list.component.ts b/src/app/features/list/notes-list.component.ts index efd0165..90a9108 100644 --- a/src/app/features/list/notes-list.component.ts +++ b/src/app/features/list/notes-list.component.ts @@ -21,6 +21,14 @@ import { TagFilterStore } from '../../core/stores/tag-filter.store'; +
+ + {{ ql.icon }} {{ ql.name }} + + +
(); @Output() queryChange = new EventEmitter(); + @Output() clearQuickLinkFilter = new EventEmitter(); 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 = { + '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); diff --git a/src/app/features/sidebar/app-sidebar-drawer.component.ts b/src/app/features/sidebar/app-sidebar-drawer.component.ts index fc06463..be85412 100644 --- a/src/app/features/sidebar/app-sidebar-drawer.component.ts +++ b/src/app/features/sidebar/app-sidebar-drawer.component.ts @@ -68,7 +68,7 @@ import { TrashExplorerComponent } from '../../layout/sidebar/trash/trash-explore {{ open.folders ? '▾' : '▸' }}
- +
@@ -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(); @Output() folderSelected = new EventEmitter(); @Output() tagSelected = new EventEmitter(); diff --git a/src/app/features/sidebar/nimbus-sidebar.component.ts b/src/app/features/sidebar/nimbus-sidebar.component.ts index 2f63102..20ad8a2 100644 --- a/src/app/features/sidebar/nimbus-sidebar.component.ts +++ b/src/app/features/sidebar/nimbus-sidebar.component.ts @@ -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)"> @@ -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(); @Output() folderSelected = new EventEmitter(); @@ -156,7 +158,7 @@ export class NimbusSidebarComponent { @Output() aboutSelected = new EventEmitter(); 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); } diff --git a/src/app/layout/app-shell-nimbus/app-shell-nimbus.component.ts b/src/app/layout/app-shell-nimbus/app-shell-nimbus.component.ts index 8ce5b01..299b465 100644 --- a/src/app/layout/app-shell-nimbus/app-shell-nimbus.component.ts +++ b/src/app/layout/app-shell-nimbus/app-shell-nimbus.component.ts @@ -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'
- +
    @@ -153,6 +154,7 @@ import { AboutPanelComponent } from '../../features/about/about-panel.component' [query]="listQuery" (openNote)="onOpenNote($event)" (queryChange)="onQueryChange($event)" + (clearQuickLinkFilter)="onClearQuickLinkFilter()" />
@@ -224,10 +226,10 @@ import { AboutPanelComponent } from '../../features/about/about-panel.component'
- +
- +
@@ -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') {
- +
} @@ -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(); } } diff --git a/src/components/file-explorer/file-explorer.component.ts b/src/components/file-explorer/file-explorer.component.ts index d591ebc..65bc008 100644 --- a/src/components/file-explorer/file-explorer.component.ts +++ b/src/components/file-explorer/file-explorer.component.ts @@ -66,12 +66,33 @@ export class FileExplorerComponent { selectedNoteId = input(null); foldersOnly = input(false); useTrashCounts = input(false); + quickLinkFilter = input<'favoris' | 'publish' | 'draft' | 'template' | 'task' | 'private' | 'archive' | null>(null); fileSelected = output(); folderSelected = output(); 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, '/'); diff --git a/vault/.trash/archive/archived-note.md b/vault/.trash/archive/archived-note.md index 53e1370..e317d3a 100644 --- a/vault/.trash/archive/archived-note.md +++ b/vault/.trash/archive/archived-note.md @@ -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 diff --git a/vault/.trash/archive/archived-note.md.bak b/vault/.trash/archive/archived-note.md.bak index a99adc2..327ee08 100644 --- a/vault/.trash/archive/archived-note.md.bak +++ b/vault/.trash/archive/archived-note.md.bak @@ -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