File-Synchronisation¶
Diese Seite dokumentiert den Mechanismus zur Synchronisation zwischen der App und Backlog.md.
Übersicht¶
CC-Sprint verwendet einen bidirektionalen Synchronisations-Mechanismus:
- App → File: Benutzer-Änderungen werden in die SQLite-DB geschrieben und anschließend nach
Backlog.mdgerendert - File → App: Externe Änderungen an
Backlog.md(z.B. durch Claude Code) werden erkannt und in die DB re-importiert
Kernproblem: Wie unterscheiden wir zwischen "Wir haben die Datei selbst geschrieben" und "Jemand anderes hat die Datei geändert"?
Lösung: Hash-basierte Change Detection mit SHA-256.
File Watcher¶
Technologie¶
Crate: notify-debouncer-full 0.3 (basiert auf notify 6.1)
Konfiguration:
let debouncer = new_debouncer(
Duration::from_millis(500), // 500ms debounce
None, // No custom file ID cache
event_handler
)?;
debouncer
.watcher()
.watch(&file_path, RecursiveMode::NonRecursive)?;
Implementierung: src-tauri/src/services/file_watcher.rs
Funktionsweise¶
Debouncing: Verhindert, dass schnelle Mehrfach-Schreiboperationen (z.B. Editor Auto-Save) zu vielen Events führen.
Beispiel-Szenario:
t=0ms: File write (by editor)
t=50ms: File write (by editor auto-save)
t=100ms: File write (by editor final)
t=600ms: → EINE Event emittiert (nach 500ms seit letzter Änderung)
Event-Handling¶
fn handle_events(
rx: Receiver<DebouncedEvent>,
app_handle: AppHandle,
project_id: String,
file_path: PathBuf
) {
loop {
match rx.recv() {
Ok(event) => {
let is_our_file = event.paths.iter().any(|p| p == &file_path);
if is_our_file {
let change_type = match event.kind {
EventKind::Modify(_) => "modified",
EventKind::Create(_) => "created",
EventKind::Remove(_) => "removed",
_ => "other",
};
// Emit to frontend
app_handle.emit("backlog-file-changed", FileChangeEvent {
project_id,
file_path: file_path.to_string_lossy().to_string(),
change_type: change_type.to_string(),
has_unsaved_changes: false, // TODO: Hash comparison
})?;
}
}
Err(e) => {
tracing::error!("File watcher channel error: {}", e);
break;
}
}
}
}
Event-Typen:
- Modify: Datei wurde geändert (häufigster Fall)
- Create: Datei wurde neu erstellt
- Remove: Datei wurde gelöscht
Hash-basierte Change Detection¶
Problem¶
Wenn die App selbst Backlog.md schreibt, löst das einen File-System-Event aus. Wir müssen unterscheiden:
- Self-Write: App hat geschrieben → Ignorieren (kein Re-Import nötig)
- External-Write: Jemand anderes hat geschrieben → Re-Import nötig
Lösung: SHA-256 Hash¶
Konzept: 1. Vor dem Schreiben: Berechne Hash des Inhalts, der geschrieben wird 2. Speichere Hash in DB 3. Bei File-Event: Lese Datei, berechne Hash, vergleiche mit gespeichertem Hash 4. Hashes gleich → Self-Write (ignorieren) 5. Hashes unterschiedlich → External-Write (re-importieren)
Implementierung¶
Schreiben (src-tauri/src/commands/items.rs:395-400):
pub async fn save_backlog(
project_id: String,
app_state: State<'_, AppState>,
) -> Result<(), String> {
// ... render backlog to content ...
// ⚠️ PROBLEM: Hash wird NACH write berechnet und gespeichert
std::fs::write(&backlog_path, &content)
.map_err(|e| format!("Failed to write backlog: {}", e))?;
let hash = calculate_hash(&content);
store_hash(&mut conn, &project_id, &hash)?;
Ok(())
}
Problem: Window zwischen write() und store_hash() → Hash könnte veraltet sein.
Lesen (FileWatcher):
fn check_if_external_change(
file_path: &Path,
db_conn: &Connection,
project_id: &str
) -> Result<bool, String> {
let content = std::fs::read_to_string(file_path)?;
let current_hash = calculate_hash(&content);
let stored_hash: String = db_conn
.query_row(
"SELECT last_hash FROM projects WHERE id = ?",
params![project_id],
|row| row.get(0)
)?;
Ok(current_hash != stored_hash)
}
Hash-Berechnung¶
use sha2::{Sha256, Digest};
fn calculate_hash(content: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(content.as_bytes());
let result = hasher.finalize();
format!("{:x}", result)
}
Performance: SHA-256 für 3900 Zeilen (~160KB) dauert ~5ms → akzeptabel.
Auto-Save¶
Konfiguration¶
Hook: app/src/hooks/useAutoSave.ts
export function useAutoSave(enabled: boolean = true) {
const [isSaving, setIsSaving] = useState(false);
const saveTimerRef = useRef<NodeJS.Timeout | null>(null);
const triggerSave = useCallback(() => {
if (!enabled) return;
// Cancel previous timer
if (saveTimerRef.current) {
clearTimeout(saveTimerRef.current);
}
// Start new 2s timer
saveTimerRef.current = setTimeout(async () => {
setIsSaving(true);
try {
await invoke('save_backlog', { projectId });
console.log('✓ Auto-save successful');
} catch (error) {
console.error('Auto-save failed:', error);
} finally {
setIsSaving(false);
}
}, 2000); // 2 second debounce
}, [enabled, projectId]);
return { triggerSave, isSaving };
}
Trigger: - Bei jeder Änderung an einem Item (Status, Titel, Beschreibung, etc.) - Bei Dependency-Änderungen - Bei Akzeptanzkriterien-Toggling
Timing:
User tippt "F" -> Timer startet (2s)
User tippt "e" -> Timer reset (2s)
User tippt "a" -> Timer reset (2s)
User tippt "t" -> Timer reset (2s)
User tippt "u" -> Timer reset (2s)
User tippt "r" -> Timer reset (2s)
User stoppt -> Timer läuft ab nach 2s -> SAVE
DB-First Approach¶
Wichtig: Alle Änderungen gehen sofort in die DB, nur das Schreiben nach Backlog.md wird debounced.
Vorteil: Keine Daten verloren, wenn App crashed vor Auto-Save.
Nachteil: DB und Backlog.md können kurzfristig out-of-sync sein.
Konflikt-Erkennung¶
Szenario: Konkurrente Änderungen¶
Konflikt-Auflösung UI¶
ConflictBanner (app/src/components/ConflictBanner.tsx):
interface ConflictBannerProps {
onReload: () => void; // Discard app changes, load from file
onKeep: () => void; // Keep app changes, overwrite file
onMerge: () => void; // Open DiffView for manual merge
externalChanges: string[]; // List of changed items (IDs)
localChanges: string[]; // List of modified items in app
}
Beispiel:
⚠️ Backlog.md wurde extern geändert
Externe Änderungen: F-0032, T-0015
Ihre Änderungen: F-0018 (Status), T-0020 (Beschreibung)
[ Neu laden ] [ Meine Änderungen behalten ] [ Zusammenführen ]
Optionen¶
| Option | Aktion | Datenverlust? |
|---|---|---|
| Neu laden | Re-import von Backlog.md |
Ja, App-Änderungen gehen verloren |
| Meine Änderungen behalten | Schreibe App-Daten nach Backlog.md (mit Backup) |
Ja, externe Änderungen gehen verloren |
| Zusammenführen | Öffne DiffView für manuellen Merge | Nein, aber manueller Aufwand |
Backup-Strategie¶
Wann werden Backups erstellt?¶
- Vor Overwrite: Wenn User "Meine Änderungen behalten" wählt
- Bei External Edit: Vor Re-Import (optional, konfigurierbar)
- Manuell: Via UI ("Backup erstellen" Button)
Backup-Format¶
Speicherort: .backlog-admin/backups/
Dateinamen: backlog-YYYY-MM-DD_HH-MM-SS.md
Beispiel:
.backlog-admin/
├── backlog.db
└── backups/
├── backlog-2026-02-01_10-30-45.md
├── backlog-2026-02-01_14-22-10.md
└── backlog-2026-02-02_09-15-33.md
Retention Policy¶
Standard: Letzte 10 Backups behalten, ältere automatisch löschen.
Konfigurierbar: User kann in Settings einstellen: - Anzahl Backups (5, 10, 20, 50, Unbegrenzt) - Maximale Größe (100MB, 500MB, 1GB)
Performance-Überlegungen¶
Timing-Analyse¶
| Operation | Dauer | Häufigkeit | Optimierung |
|---|---|---|---|
| File Watch Event | ~1ms | Bei jeder Datei-Änderung | Debounced (500ms) |
| Hash Berechnung | ~5ms | Bei jedem Event | Effizient, kein Problem |
| Parse Backlog | ~100ms | Bei External Change | Nur bei Hash-Mismatch |
| Render Backlog | ~50ms | Bei Auto-Save | Debounced (2s) |
| DB Write | ~10ms | Bei jeder Änderung | Indexed, schnell |
Gesamtlatenz (External Change → UI Update):
Optimierungen¶
Früher Abbruch:
// Wenn Hash gleich, breche früh ab (kein Re-Import nötig)
if current_hash == stored_hash {
return Ok(()); // Nur 505ms statt 635ms
}
Incremental Parsing (Future): - Nur geänderte Items parsen, nicht komplettes File - Erfordert Line-Range Tracking - Potenzielle Beschleunigung: ~10x bei einzelner Item-Änderung
Bekannte Probleme¶
Siehe: - Race Conditions - FileWatcher vs AutoSave Timing-Issues - Datenverlust-Risiken - Hash-Timing, rebuild_database
Nächste Schritte¶
- Datenbank-Schema - SQLite-Struktur
- Prozesse - Workflows für Backlog.md Bearbeitung