chore: update Angular build cache and TypeScript definitions

This commit is contained in:
Bruno Charest 2025-09-28 07:13:09 -04:00
parent 53bef252d9
commit f68440656e
13 changed files with 397 additions and 89 deletions

File diff suppressed because one or more lines are too long

View File

@ -20,6 +20,7 @@
"angular-calendar": "^0.32.0",
"date-fns": "^4.1.0",
"express": "^5.1.0",
"chokidar": "^4.0.3",
"highlight.js": "^11.10.0",
"mermaid": "^11.12.0",
"rxjs": "^7.8.2",

View File

@ -4,6 +4,7 @@ import express from 'express';
import path from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs';
import chokidar from 'chokidar';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@ -15,6 +16,43 @@ const rootDir = path.resolve(__dirname, '..');
const distDir = path.join(rootDir, 'dist');
const vaultDir = path.join(rootDir, 'vault');
const vaultEventClients = new Set();
const registerVaultEventClient = (res) => {
const heartbeat = setInterval(() => {
try {
res.write(':keepalive\n\n');
} catch {
// Write failures will be handled by the close handler.
}
}, 20000);
const client = { res, heartbeat };
vaultEventClients.add(client);
return client;
};
const unregisterVaultEventClient = (client) => {
clearInterval(client.heartbeat);
vaultEventClients.delete(client);
};
const broadcastVaultEvent = (payload) => {
if (!vaultEventClients.size) {
return;
}
const data = `data: ${JSON.stringify(payload)}\n\n`;
for (const client of [...vaultEventClients]) {
try {
client.res.write(data);
} catch (error) {
console.error('Failed to notify vault event client:', error);
unregisterVaultEventClient(client);
}
}
};
const isMarkdownFile = (entry) => entry.isFile() && entry.name.toLowerCase().endsWith('.md');
const normalizeString = (value) => {
@ -138,6 +176,44 @@ const isDateWithinRange = (target, start, end) => {
return targetTime >= start.getTime() && targetTime <= end.getTime();
};
const vaultWatcher = chokidar.watch(vaultDir, {
persistent: true,
ignoreInitial: true,
awaitWriteFinish: {
stabilityThreshold: 250,
pollInterval: 100,
},
});
const watchedVaultEvents = ['add', 'change', 'unlink', 'addDir', 'unlinkDir'];
watchedVaultEvents.forEach((eventName) => {
vaultWatcher.on(eventName, (changedPath) => {
const relativePath = path.relative(vaultDir, changedPath).replace(/\\/g, '/');
broadcastVaultEvent({
event: eventName,
path: relativePath,
timestamp: Date.now(),
});
});
});
vaultWatcher.on('ready', () => {
broadcastVaultEvent({
event: 'ready',
timestamp: Date.now(),
});
});
vaultWatcher.on('error', (error) => {
console.error('Vault watcher error:', error);
broadcastVaultEvent({
event: 'error',
message: typeof error?.message === 'string' ? error.message : 'Unknown watcher error',
timestamp: Date.now(),
});
});
// Vérifier si le répertoire dist existe
if (!fs.existsSync(distDir)) {
console.warn(`Warning: build directory not found at ${distDir}. Did you run \`npm run build\`?`);
@ -151,6 +227,29 @@ app.get('/api/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
app.get('/api/vault/events', (req, res) => {
res.set({
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
'X-Accel-Buffering': 'no',
});
res.flushHeaders?.();
res.write(
`data: ${JSON.stringify({
event: 'connected',
timestamp: Date.now(),
})}\n\n`,
);
const client = registerVaultEventClient(res);
req.on('close', () => {
unregisterVaultEventClient(client);
});
});
// API endpoint pour les données de la voûte (contenu réel)
app.get('/api/vault', (req, res) => {
try {

View File

@ -681,54 +681,56 @@
}
:host ::ng-deep .md-heading-1 {
font-size: clamp(2.8rem, 4vw, 3.5rem);
margin-top: 3.5rem;
margin-bottom: 1.4rem;
border-bottom-width: 3px;
font-size: clamp(2.6rem, 3.6vw, 3.1rem);
margin-top: 0.55rem;
margin-bottom: 0.55rem;
color: #0f172a;
}
:host ::ng-deep .md-heading-2 {
font-size: clamp(2.2rem, 3.2vw, 2.8rem);
margin-top: 3rem;
margin-bottom: 1.2rem;
border-bottom-width: 2px;
font-size: clamp(2.1rem, 3vw, 2.5rem);
margin-top: 0.35rem;
margin-bottom: 0.35rem;
color: #0f766e;
}
:host ::ng-deep .md-heading-3 {
font-size: clamp(1.8rem, 2.8vw, 2.2rem);
margin-top: 2.6rem;
margin-bottom: 1rem;
font-size: clamp(1.7rem, 2.6vw, 2rem);
margin-top: 0.3rem;
margin-bottom: 0.3rem;
color: #0f766e;
}
:host ::ng-deep .md-heading-4 {
font-size: clamp(1.5rem, 2.2vw, 1.8rem);
margin-top: 2.4rem;
margin-bottom: 0.9rem;
font-size: clamp(1.45rem, 2.1vw, 1.7rem);
margin-top: 0.25rem;
margin-bottom: 0.25rem;
color: #0f766e;
}
:host ::ng-deep .md-heading-5 {
font-size: clamp(1.3rem, 1.8vw, 1.4rem);
margin-top: 2.2rem;
margin-bottom: 0.75rem;
font-weight: 600;
font-size: clamp(1.25rem, 1.7vw, 1.35rem);
margin-top: 0.2rem;
margin-bottom: 0.22rem;
color: #0f766e;
}
:host ::ng-deep .md-heading-6 {
font-size: clamp(1.1rem, 1.6vw, 1.2rem);
margin-top: 2rem;
margin-bottom: 0.6rem;
font-weight: 600;
letter-spacing: 0.01em;
text-transform: uppercase;
font-size: clamp(1.1rem, 1.5vw, 1.2rem);
margin-top: 0.2rem;
margin-bottom: 0.2rem;
color: #0f766e;
}
:host ::ng-deep .note-content-area p {
margin-top: 0.35rem;
margin-bottom: 0.35rem;
}
:host-context(.dark) ::ng-deep .md-heading-1 {
font-weight: 600;
letter-spacing: 0.01em;
text-transform: uppercase;
color: #e2e8f0;
}
@ -740,6 +742,10 @@
color: #5eead4;
}
:host ::ng-deep .prose :where(p, li, blockquote):not(:where(.not-prose *)) {
line-height: 1.55;
}
:host ::ng-deep .metadata-panel {
margin-bottom: 2.2rem;
padding: 0.2rem 0 0.2rem 0.8rem;

View File

@ -49,47 +49,6 @@
</button>
</nav>
<!-- Navigation mobile -->
<nav class="sticky bottom-0 z-30 flex w-full items-center justify-around gap-2 border-t border-obs-l-border bg-obs-l-bg-main/95 px-2 py-2 backdrop-blur-xs dark:border-obs-d-border dark:bg-obs-d-bg-main/95 lg:hidden">
<button
(click)="setView('files'); toggleSidebarTo(true)"
class="flex flex-1 flex-col items-center gap-1 rounded-lg px-3 py-2 text-xs font-semibold text-obs-l-text-muted transition hover:bg-obs-l-bg-secondary dark:text-obs-d-text-muted dark:hover:bg-obs-d-bg-secondary"
[class.text-obs-l-text-main]="activeView() === 'files'"
[class.dark:text-obs-d-text-main]="activeView() === 'files'"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" /></svg>
Fichiers
</button>
<button
(click)="setView('search'); toggleSidebarTo(true)"
class="flex flex-1 flex-col items-center gap-1 rounded-lg px-3 py-2 text-xs font-semibold text-obs-l-text-muted transition hover:bg-obs-l-bg-secondary dark:text-obs-d-text-muted dark:hover:bg-obs-d-bg-secondary"
[class.text-obs-l-text-main]="activeView() === 'search'"
[class.dark:text-obs-d-text-main]="activeView() === 'search'"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
Recherche
</button>
<button
(click)="setView('tags'); toggleSidebarTo(true)"
class="flex flex-1 flex-col items-center gap-1 rounded-lg px-3 py-2 text-xs font-semibold text-obs-l-text-muted transition hover:bg-obs-l-bg-secondary dark:text-obs-d-text-muted dark:hover:bg-obs-d-bg-secondary"
[class.text-obs-l-text-main]="activeView() === 'tags'"
[class.dark:text-obs-d-text-main]="activeView() === 'tags'"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-5 5a2 2 0 01-2.828 0l-7-7A2 2 0 013 8V3z" /></svg>
Tags
</button>
<button
(click)="setView('calendar'); toggleSidebarTo(true)"
class="flex flex-1 flex-col items-center gap-1 rounded-lg px-3 py-2 text-xs font-semibold text-obs-l-text-muted transition hover:bg-obs-l-bg-secondary dark:text-obs-d-text-muted dark:hover:bg-obs-d-bg-secondary"
[class.text-obs-l-text-main]="activeView() === 'calendar'"
[class.dark:text-obs-d-text-main]="activeView() === 'calendar'"
[attr.aria-pressed]="activeView() === 'calendar'"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2h-1.5a1.5 1.5 0 01-3 0h-5a1.5 1.5 0 01-3 0H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
Agenda
</button>
</nav>
@if (isDesktop() || isSidebarOpen()) {
<aside
class="fixed inset-y-0 left-0 z-40 flex h-screen w-[min(320px,85vw)] -translate-x-full transform flex-col border-r border-obs-l-border bg-obs-l-bg-secondary shadow-xl transition-transform duration-200 ease-in-out dark:border-obs-d-border dark:bg-obs-d-bg-secondary lg:static lg:h-auto lg:w-auto lg:translate-x-0 lg:shadow-none"

View File

@ -390,6 +390,10 @@ export class AppComponent implements OnDestroy {
this.vaultService.ensureFolderOpen(note.originalPath);
this.selectedNoteId.set(note.id);
if (!this.isDesktopView() && this.activeView() === 'search') {
this.isSidebarOpen.set(false);
}
}
handleTagClick(tagName: string): void {

View File

@ -20,7 +20,7 @@ import { adapterFactory } from 'angular-calendar/date-adapters/date-fns';
import { MarkdownCalendarService } from '../../services/markdown-calendar.service';
import { FileMetadata } from '../../types';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Observable, Subject } from 'rxjs';
import { Observable, Subject, Subscription } from 'rxjs';
interface DayBucket {
created: FileMetadata[];
@ -77,6 +77,7 @@ export class MarkdownCalendarComponent {
private dragAnchor: Date | null = null;
private searchRequestId = 0;
private searchSubscription: Subscription | null = null;
constructor() {
this.loadMetadata();
@ -193,12 +194,13 @@ export class MarkdownCalendarComponent {
private runSearch(source$: Observable<FileMetadata[]>): void {
const token = ++this.searchRequestId;
this.searchSubscription?.unsubscribe();
this.isSearching.set(true);
this.searchError.set(null);
this.searchStateChange.emit('loading');
this.searchErrorChange.emit(null);
source$.subscribe({
this.searchSubscription = source$.subscribe({
next: (files: FileMetadata[]) => {
if (token !== this.searchRequestId) {
return;

View File

@ -24,7 +24,7 @@ interface MetadataEntry {
imports: [CommonModule],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="p-8 prose prose-lg dark:prose-invert max-w-none">
<div class="p-8 prose prose-lg dark:prose-invert max-w-none prose-p:leading-[1] prose-li:leading-[1] prose-blockquote:leading-[1]">
<div class="!mb-6 pb-2 border-b border-obs-l-border dark:border-obs-d-border">
<h1 class="!text-4xl !font-bold !mb-3">{{ note().title }}</h1>
@if (note().tags.length > 0) {

View File

@ -1,6 +1,6 @@
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Observable, of, shareReplay, finalize, map } from 'rxjs';
import { FileMetadata } from '../types';
@Injectable({
@ -9,23 +9,48 @@ import { FileMetadata } from '../types';
export class MarkdownCalendarService {
private readonly http = inject(HttpClient);
getFilesMetadata(): Observable<FileMetadata[]> {
return this.http.get<FileMetadata[]>('/api/files/metadata');
private metadataCache: FileMetadata[] | null = null;
private metadataRequest$: Observable<FileMetadata[]> | null = null;
private readonly parsedDateCache = new Map<string, number | null>();
getFilesMetadata(forceRefresh = false): Observable<FileMetadata[]> {
if (!forceRefresh && this.metadataCache) {
return of(this.metadataCache);
}
if (!forceRefresh && this.metadataRequest$) {
return this.metadataRequest$;
}
const request$ = this.http
.get<FileMetadata[]>('/api/files/metadata')
.pipe(
map((metadata) => {
this.metadataCache = metadata;
this.metadataRequest$ = null;
this.parsedDateCache.clear();
return metadata;
}),
finalize(() => {
this.metadataRequest$ = null;
}),
shareReplay({ bufferSize: 1, refCount: true })
);
this.metadataRequest$ = request$;
return request$;
}
searchFilesByDate(date: Date): Observable<FileMetadata[]> {
return this.http.get<FileMetadata[]>('/api/files/by-date', {
params: { date: date.toISOString() },
});
const start = this.startOfDay(date);
const end = this.endOfDay(date);
return this.getFilesMetadata().pipe(map((metadata) => this.filterByRange(metadata, start, end)));
}
searchFilesByDateRange(startDate: Date, endDate: Date): Observable<FileMetadata[]> {
return this.http.get<FileMetadata[]>('/api/files/by-date-range', {
params: {
start: startDate.toISOString(),
end: endDate.toISOString(),
},
});
const rangeStart = this.startOfDay(startDate);
const rangeEnd = this.endOfDay(endDate);
return this.getFilesMetadata().pipe(map((metadata) => this.filterByRange(metadata, rangeStart, rangeEnd)));
}
groupMetadataByDay(metadata: FileMetadata[]): Map<string, { created: FileMetadata[]; updated: FileMetadata[] }> {
@ -66,4 +91,48 @@ export class MarkdownCalendarService {
private appendUnique(list: FileMetadata[], file: FileMetadata): FileMetadata[] {
return list.some((item) => item.id === file.id) ? list : list.concat(file);
}
private filterByRange(metadata: FileMetadata[], start: Date, end: Date): FileMetadata[] {
const startTime = start.getTime();
const endTime = end.getTime();
return metadata.filter((file) => this.matchesRange(file, startTime, endTime));
}
private matchesRange(file: FileMetadata, startTime: number, endTime: number): boolean {
const created = this.getTimeValue(file.createdAt);
if (created !== null && created >= startTime && created <= endTime) {
return true;
}
const updated = this.getTimeValue(file.updatedAt);
return updated !== null && updated >= startTime && updated <= endTime;
}
private getTimeValue(value: string | undefined | null): number | null {
if (!value) {
return null;
}
if (this.parsedDateCache.has(value)) {
return this.parsedDateCache.get(value) ?? null;
}
const time = Date.parse(value);
const normalized = Number.isNaN(time) ? null : time;
this.parsedDateCache.set(value, normalized);
return normalized;
}
private startOfDay(date: Date): Date {
const start = new Date(date);
start.setHours(0, 0, 0, 0);
return start;
}
private endOfDay(date: Date): Date {
const end = new Date(date);
end.setHours(23, 59, 59, 999);
return end;
}
}

View File

@ -0,0 +1,102 @@
import { Injectable, NgZone, OnDestroy } from '@angular/core';
import { Observable, Subject } from 'rxjs';
export interface VaultEventPayload {
event: string;
path?: string;
timestamp: number;
message?: string;
}
@Injectable({
providedIn: 'root',
})
export class VaultEventsService implements OnDestroy {
private readonly eventsSubject = new Subject<VaultEventPayload>();
private eventSource: EventSource | null = null;
private reconnectTimer: number | null = null;
private readonly reconnectDelayMs = 5000;
constructor(private readonly zone: NgZone) {}
events$(): Observable<VaultEventPayload> {
this.ensureConnection();
return this.eventsSubject.asObservable();
}
private ensureConnection(): void {
if (this.eventSource || typeof window === 'undefined' || typeof EventSource === 'undefined') {
return;
}
this.zone.runOutsideAngular(() => {
const source = new EventSource('/api/vault/events');
this.eventSource = source;
source.onmessage = (event: MessageEvent<string>) => {
this.handleMessage(event.data);
};
source.onerror = () => {
// Let the browser attempt to reconnect automatically, but make sure we clean up references.
this.scheduleReconnect();
};
});
}
private handleMessage(rawData: string): void {
if (!rawData) {
return;
}
let payload: VaultEventPayload | null = null;
try {
payload = JSON.parse(rawData) as VaultEventPayload;
} catch (error) {
console.error('Failed to parse vault event payload:', error);
return;
}
if (!payload || typeof payload.event !== 'string') {
return;
}
this.zone.run(() => {
this.eventsSubject.next(payload!);
});
}
private scheduleReconnect(): void {
if (typeof window === 'undefined') {
return;
}
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
if (this.reconnectTimer !== null) {
return;
}
this.reconnectTimer = window.setTimeout(() => {
this.reconnectTimer = null;
this.ensureConnection();
}, this.reconnectDelayMs);
}
ngOnDestroy(): void {
if (this.reconnectTimer !== null && typeof window !== 'undefined') {
window.clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
this.eventsSubject.complete();
}
}

View File

@ -1,6 +1,8 @@
import { Injectable, signal, computed } from '@angular/core';
import { Injectable, signal, computed, OnDestroy } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Note, VaultNode, GraphData, TagInfo, VaultFolder } from '../types';
import { VaultEventsService, VaultEventPayload } from './vault-events.service';
import { Subscription } from 'rxjs';
interface VaultApiNote {
id: string;
@ -22,7 +24,7 @@ interface VaultApiResponse {
@Injectable({
providedIn: 'root'
})
export class VaultService {
export class VaultService implements OnDestroy {
private notesMap = signal<Map<string, Note>>(new Map());
private openFolderPaths = signal(new Set<string>());
private initialVaultName = this.resolveVaultName();
@ -123,8 +125,22 @@ export class VaultService {
.sort((a, b) => b.count - a.count);
});
constructor(private http: HttpClient) {
private vaultEventsSubscription: Subscription | null = null;
private refreshTimeoutId: ReturnType<typeof setTimeout> | null = null;
constructor(private http: HttpClient, private vaultEvents: VaultEventsService) {
this.refreshNotes();
this.observeVaultEvents();
}
ngOnDestroy(): void {
this.vaultEventsSubscription?.unsubscribe();
this.vaultEventsSubscription = null;
if (this.refreshTimeoutId !== null) {
clearTimeout(this.refreshTimeoutId);
this.refreshTimeoutId = null;
}
}
getNoteById(id: string): Note | undefined {
@ -167,6 +183,52 @@ export class VaultService {
this.refreshNotes();
}
private observeVaultEvents(): void {
this.vaultEventsSubscription = this.vaultEvents.events$().subscribe({
next: (event) => this.handleVaultEvent(event),
error: (error) => {
console.error('Vault events stream error:', error);
}
});
}
private handleVaultEvent(event: VaultEventPayload): void {
if (!event || typeof event.event !== 'string') {
return;
}
switch (event.event) {
case 'add':
case 'change':
case 'unlink':
case 'addDir':
case 'unlinkDir':
this.scheduleRefresh();
break;
case 'ready':
case 'connected':
// Initial ready/connected events can trigger a refresh to ensure state is up-to-date.
this.scheduleRefresh();
break;
case 'error':
console.error('Vault watcher reported error:', event.message ?? 'Unknown watcher error');
break;
default:
break;
}
}
private scheduleRefresh(): void {
if (this.refreshTimeoutId !== null) {
return;
}
this.refreshTimeoutId = setTimeout(() => {
this.refreshTimeoutId = null;
this.refreshNotes();
}, 300);
}
private refreshNotes() {
this.http.get<VaultApiResponse>('/api/vault').subscribe({
next: ({ notes }) => {

View File

@ -4,5 +4,9 @@ NomDeVoute: IT
Description: Page d'accueil de la voute IT
tags: [home, accueil]
---
## TEST
bonjour
# Page d'accueil
### allo
bonjour
alloooo

View File

@ -68,6 +68,10 @@ Citation en ligne : « > Ceci est une citation »
- [ ] Tâche à faire
- [X] Tâche terminée
## Images
![[Voute_IT.png]]
## Liens et images
[Lien vers le site officiel d&#39;Obsidian](https://obsidian.md)
@ -209,9 +213,5 @@ Le Markdown peut inclure des notes de bas de page[^1].
</details>
## Sections horizontales
---
Fin de la page de test.
[^1]: Ceci est un exemple de note de bas de page.