perf: optimize Docker build with layer caching and BuildKit support
This commit is contained in:
parent
58bd57543b
commit
c030f91ebe
14
.dockerignore
Normal file
14
.dockerignore
Normal file
@ -0,0 +1,14 @@
|
||||
node_modules
|
||||
dist
|
||||
.git
|
||||
.angular
|
||||
.vscode
|
||||
*.log
|
||||
tmp
|
||||
db/*.db
|
||||
.env
|
||||
.DS_Store
|
||||
coverage
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
@ -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 --------------------
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -15,8 +15,8 @@ export interface ThemePrefs {
|
||||
}
|
||||
|
||||
const DEFAULT_PREFS: ThemePrefs = {
|
||||
mode: 'system',
|
||||
theme: 'light',
|
||||
mode: 'dark',
|
||||
theme: 'nord',
|
||||
language: 'fr'
|
||||
};
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>();
|
||||
|
||||
@ -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); }
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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, '/');
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user