Initial-Scan-Modal mit Fortschrittsanzeige

Batch-Processing (50 Dateien pro Batch) mit UI-Yield,
Fortschrittsbalken, Datei-/Wort-/Link-/Tag-Zähler,
Abbruch-Möglichkeit, Baseline-Event nach Scan.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Luca Oelfke 2026-02-12 10:50:48 +01:00
parent e0d9f301d6
commit 91cc22c3e5

View file

@ -0,0 +1,153 @@
import { App, Modal, TFile } from 'obsidian';
import { LogfireSettings, categoryForType } from '../types';
import { ContentAnalyzer } from '../core/content-analyzer';
import { DatabaseManager } from '../core/database';
import { EventBus } from '../core/event-bus';
import { SessionManager } from '../core/session-manager';
const BATCH_SIZE = 50;
const BATCH_DELAY_MS = 50;
export class InitialScanModal extends Modal {
private cancelled = false;
private resolve!: (scanned: boolean) => void;
constructor(
app: App,
private settings: LogfireSettings,
private analyzer: ContentAnalyzer,
private db: DatabaseManager,
private eventBus: EventBus,
private sessionManager: SessionManager,
private shouldTrack: (path: string) => boolean,
) {
super(app);
}
open(): Promise<boolean> {
return new Promise((resolve) => {
this.resolve = resolve;
super.open();
});
}
onOpen(): void {
const { contentEl } = this;
contentEl.empty();
contentEl.addClass('logfire-scan-modal');
const files = this.app.vault.getMarkdownFiles()
.filter(f => this.shouldTrack(f.path));
contentEl.createEl('h2', { text: 'Logfire Vault-Scan' });
contentEl.createEl('p', {
text: `${files.length} Markdown-Dateien gefunden. Der Scan erstellt die Baseline für Content-Tracking.`,
});
const progressContainer = contentEl.createDiv({ cls: 'logfire-scan-progress' });
const progressBar = progressContainer.createEl('progress', {
attr: { max: String(files.length), value: '0' },
});
progressBar.style.width = '100%';
const statusEl = contentEl.createDiv({ cls: 'logfire-scan-status' });
const currentFileEl = statusEl.createDiv();
const totalsEl = statusEl.createDiv();
totalsEl.style.marginTop = '8px';
totalsEl.style.opacity = '0.8';
const buttonContainer = contentEl.createDiv({ cls: 'logfire-scan-buttons' });
buttonContainer.style.display = 'flex';
buttonContainer.style.justifyContent = 'flex-end';
buttonContainer.style.gap = '8px';
buttonContainer.style.marginTop = '16px';
const cancelBtn = buttonContainer.createEl('button', { text: 'Abbrechen' });
cancelBtn.addEventListener('click', () => {
this.cancelled = true;
});
const startBtn = buttonContainer.createEl('button', { text: 'Scan starten', cls: 'mod-cta' });
startBtn.addEventListener('click', async () => {
startBtn.disabled = true;
cancelBtn.textContent = 'Stoppen';
let scanned = 0;
let totalWords = 0;
let totalLinks = 0;
let totalTags = 0;
for (let i = 0; i < files.length; i += BATCH_SIZE) {
if (this.cancelled) break;
const batch = files.slice(i, i + BATCH_SIZE);
for (const file of batch) {
if (this.cancelled) break;
try {
const content = await this.app.vault.cachedRead(file);
const metadata = this.app.metadataCache.getFileCache(file);
const snapshot = this.analyzer.buildSnapshot(file.path, content, metadata ?? null);
this.db.upsertBaseline(
snapshot,
file.stat.ctime,
file.stat.mtime,
file.stat.size,
);
totalWords += snapshot.wordCount;
totalLinks += snapshot.links.length;
totalTags += snapshot.tags.length;
scanned++;
progressBar.value = scanned;
currentFileEl.textContent = file.path;
totalsEl.textContent = `${scanned}/${files.length} Dateien | ${totalWords.toLocaleString()} Wörter | ${totalLinks} Links | ${totalTags} Tags`;
} catch (err) {
console.error(`[Logfire] Scan-Fehler bei ${file.path}:`, err);
}
}
await sleep(BATCH_DELAY_MS);
}
this.eventBus.emit({
id: crypto.randomUUID(),
timestamp: Date.now(),
type: this.cancelled ? 'system:rescan' : 'system:baseline-scan',
category: categoryForType('system:baseline-scan'),
source: this.app.vault.getName(),
payload: {
wordCount: totalWords,
linkCount: totalLinks,
tagCount: totalTags,
filesScanned: scanned,
filesTotal: files.length,
},
session: this.sessionManager.currentSessionId,
});
currentFileEl.textContent = this.cancelled
? `Scan gestoppt. ${scanned} von ${files.length} Dateien gescannt.`
: `Scan abgeschlossen! ${scanned} Dateien gescannt.`;
cancelBtn.style.display = 'none';
const closeBtn = buttonContainer.createEl('button', { text: 'Schließen', cls: 'mod-cta' });
closeBtn.addEventListener('click', () => {
this.close();
this.resolve(true);
});
});
}
onClose(): void {
this.cancelled = true;
this.contentEl.empty();
this.resolve(false);
}
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}