feat: end-to-end integrity checks with self-healing; website; installer and Windows fixes

Store::verify compares the whole SQLite projection, every table row for
row, against a fresh projection of the journal; verify_and_heal rebuilds a
drifted database (keeping the old file) at every server start and via the
new books.verify tool, which also re-hashes every blob. The GUI checks the
server's database read-only and every stored file, and shows the result.
rebuild() also drops stale WAL/SHM files.

site/: the wanderbooks website (just site, just publish). install.sh:
OS detection (Windows points to WSL2), C compiler and git checks,
WANDERBOOKS_REPO. Windows: no directory fsync off Unix; .exe-aware lookup.
Repo justfile: check, build, install, demo, site.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MtYEaR6M6Ybdhm6X98BWNu
This commit is contained in:
swaitsandClaude Opus 5.5 committed 2026-10-12 17:05:05 -07:00
1 parent 714867ed58
commit 9694d2a89a
25 files changed
+808 -41

No files matched your search

+42
View File
@@ -85,6 +85,46 @@ pub struct Books {
pub receipt_threshold: Decimal,
pub attention: Vec<Attention>,
pub kpis: Kpis,
pub integrity: Integrity,
}
/// Whether what's on disk is what the journal says.
#[derive(Clone, Debug)]
pub struct Integrity {
/// The writer's SQLite database against the journal.
pub disk: ledger_store::DiskCheck,
/// Stored files whose bytes match their hash.
pub files_ok: u64,
/// Files that are missing or damaged: (attachment id, why).
pub file_failures: Vec<(u64, String)>,
}
impl Integrity {
/// Check the database and every file not already verified (by hash, so
/// a file is read once however often the books change).
pub fn check(snap: &Snapshot, verified: &mut HashSet<String>) -> Integrity {
let store = ledger_store::BlobStore::open_read_only(&snap.data_dir().join("blobs"));
let mut ok = 0;
let mut failures = Vec::new();
for a in snap.attachments(None).unwrap_or_default() {
if verified.contains(&a.blob_sha) {
ok += 1;
continue;
}
match store.get(&a.blob_sha) {
Ok(_) => {
verified.insert(a.blob_sha.clone());
ok += 1;
}
Err(e) => failures.push((a.id, e.to_string())),
}
}
Integrity {
disk: snap.check_disk(),
files_ok: ok,
file_failures: failures,
}
}
}
/// The headline numbers.
@@ -113,6 +153,7 @@ impl Books {
settings: BooksSettings,
today: NaiveDate,
prev_seq: Option<u64>,
integrity: Integrity,
) -> Result<Books, String> {
let ledger = snap.ledger().clone();
let attachments = snap.attachments(None).map_err(|e| e.to_string())?;
@@ -268,6 +309,7 @@ impl Books {
receipt_threshold,
attention: Vec::new(),
kpis: Kpis::default(),
integrity,
};
books.kpis = books.compute_kpis();
books.attention = crate::attention::find(&books);