Bugfix-Runde Panel/Archiv: Datenverlust, Suche, Löschen, Markdown
- ProjectConfig reicht unbekannte Keys durch (serde flatten). Bisher verlor jeder read-modify-write die Felder neuerer Versionen; so ging archiveHome bei der Pool-Zuweisung durch einen älteren Build verloren. - Archiv-Suche: Nutzereingabe wird in einen FTS5-Ausdruck übersetzt, der nicht scheitern kann. ai-control, C++ oder eine halbe Phrase warfen bisher einen Syntaxfehler — bei der Live-Suche in jedem Tippzwischenstand. - commands_delete: Befehlstext als Identität, Indizes werden geprüft. Bisher Panik oder Löschen der falschen Kachel, wenn die Panel-Indizes der Datei hinterherhingen. - Archivieren überschreibt keine Datei mehr: Der Stempel hat Minutenauflösung, bei Kollision wird -2, -3, ... angehängt. - archive_panel prüft erst auf leeres Panel, dann wird das Archiv-Home gesetzt; bisher blieb ein umkonfiguriertes Home trotz Meldung "nicht archiviert". - Kaputte .claude.json eines Pools wird nicht mehr durch eine frische ersetzt; nur eine fehlende Datei rechtfertigt das. - Archiv-Formular leert beim Schließen — sonst galten Ordner und Schlagwörter des vorigen Dokuments unbemerkt fürs nächste. - Markdown fürs Panel entschärft (src/markdown.ts): rohes HTML wird als Text ausgegeben, Link- und Bildziele auf harmlose Schemata beschränkt. Das Panel teilt sich den Webview mit dem Terminal, dessen term_write in die PTY schreibt; ohne CSP war das ein Weg von Fremdinhalt zu Befehlsausführung. Regressionstests je Fix: 73 Rust-Tests, 35 Frontend-Tests.
This commit is contained in:
@@ -145,18 +145,23 @@ pub(crate) fn archive_panel_content(
|
||||
dir_override: Option<&str>,
|
||||
meta: &ArchiveMeta,
|
||||
) -> Result<PathBuf, String> {
|
||||
if let Some(d) = dir_override {
|
||||
set_project_archive_home(project, d)?;
|
||||
}
|
||||
let home = require_archive_home(project)?;
|
||||
let dir = match &meta.folder {
|
||||
Some(f) => home.join(check_folder(f)?),
|
||||
None => home,
|
||||
};
|
||||
// Erst prüfen, dann konfigurieren: `set_project_archive_home` legt Ordner an
|
||||
// und trägt eine Berechtigung in die settings.json des Projekts ein. Käme das
|
||||
// vor der Leer-Prüfung, hinterließe ein Archivieren mit leerem Panel die
|
||||
// Meldung „nicht archiviert“ — und ein dauerhaft umgestelltes Archiv-Home.
|
||||
let text = fs::read_to_string(panel_file(project)).unwrap_or_default();
|
||||
if text.trim().is_empty() {
|
||||
return Err("Panel ist leer — nichts zu archivieren".into());
|
||||
}
|
||||
let folder = meta.folder.as_deref().map(check_folder).transpose()?;
|
||||
if let Some(d) = dir_override {
|
||||
set_project_archive_home(project, d)?;
|
||||
}
|
||||
let home = require_archive_home(project)?;
|
||||
let dir = match folder {
|
||||
Some(f) => home.join(f),
|
||||
None => home,
|
||||
};
|
||||
fs::create_dir_all(&dir).map_err(|e| format!("{}: {e}", dir.display()))?;
|
||||
|
||||
let secs = SystemTime::now()
|
||||
@@ -165,12 +170,29 @@ pub(crate) fn archive_panel_content(
|
||||
.as_secs();
|
||||
let (stamp, iso) = utc_stamp(secs);
|
||||
let title = first_line(&text);
|
||||
let path = dir.join(format!("{stamp}-{}.md", slugify(&title)));
|
||||
let path = free_path(&dir, &stamp, &slugify(&title));
|
||||
let doc = format!("{}{}\n", frontmatter(&title, project, &iso, meta), text.trim_end());
|
||||
fs::write(&path, doc).map_err(|e| format!("{}: {e}", path.display()))?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Freier Dateiname für das Archiv-Dokument.
|
||||
///
|
||||
/// Der Zeitstempel hat Minutenauflösung; zweimal Archivieren innerhalb einer
|
||||
/// Minute mit derselben Titelzeile ergäbe denselben Namen. Da mit `fs::write`
|
||||
/// geschrieben wird, wäre das stiller Datenverlust — ausgerechnet im Archiv,
|
||||
/// das die dauerhafte Ablage ist. Darum bei Kollision `-2`, `-3`, … anhängen.
|
||||
fn free_path(dir: &std::path::Path, stamp: &str, slug: &str) -> PathBuf {
|
||||
let first = dir.join(format!("{stamp}-{slug}.md"));
|
||||
if !first.exists() {
|
||||
return first;
|
||||
}
|
||||
(2..)
|
||||
.map(|n| dir.join(format!("{stamp}-{slug}-{n}.md")))
|
||||
.find(|p| !p.exists())
|
||||
.expect("unendlicher Zahlenraum")
|
||||
}
|
||||
|
||||
/// Unterordner-Pfad: relativ, nur normale Komponenten (kein `..`, kein Root).
|
||||
fn check_folder(folder: &str) -> Result<&std::path::Path, String> {
|
||||
let p = std::path::Path::new(folder);
|
||||
@@ -347,6 +369,25 @@ mod tests {
|
||||
assert_eq!(iso, "2026-07-11T11:14:15Z");
|
||||
}
|
||||
|
||||
/// Zweimal Archivieren in derselben Minute mit gleichem Titel darf die erste
|
||||
/// Datei nicht überschreiben — der Stempel hat nur Minutenauflösung.
|
||||
#[test]
|
||||
fn gleicher_stempel_und_titel_kollidiert_nicht() {
|
||||
let dir = crate::domain::testutil::tmp_paths().home.join("archiv");
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
let erste = free_path(&dir, "2026-07-19_2118", "notiz");
|
||||
assert_eq!(erste.file_name().unwrap(), "2026-07-19_2118-notiz.md");
|
||||
fs::write(&erste, "alt").unwrap();
|
||||
|
||||
let zweite = free_path(&dir, "2026-07-19_2118", "notiz");
|
||||
assert_eq!(zweite.file_name().unwrap(), "2026-07-19_2118-notiz-2.md");
|
||||
fs::write(&zweite, "neu").unwrap();
|
||||
|
||||
assert_eq!(fs::read_to_string(&erste).unwrap(), "alt");
|
||||
let dritte = free_path(&dir, "2026-07-19_2118", "notiz");
|
||||
assert_eq!(dritte.file_name().unwrap(), "2026-07-19_2118-notiz-3.md");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn civil_from_days_referenz() {
|
||||
assert_eq!(civil_from_days(0), (1970, 1, 1));
|
||||
|
||||
Reference in New Issue
Block a user