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

+27 -10
View File
@@ -361,16 +361,33 @@ fn live_status(ui: &mut Ui, b: &Books, since_update: f64, live: bool) {
.color(p.muted),
);
});
ui.label(
RichText::new(format!("{} Journal chain verified", icon::SEAL_CHECK))
.font(theme::regular(11.5))
.color(p.muted),
)
.on_hover_text(format!(
"Every journal event's hash chains to the one before it.\nHead: {}\nData: {}",
b.head,
b.data_dir.display()
));
let check = |ui: &mut Ui, ok: bool, text: String, tip: String| {
let (glyph, color) = if ok { (icon::SEAL_CHECK, p.muted) } else { (icon::WARNING_CIRCLE, p.negative) };
ui.label(RichText::new(format!("{glyph} {text}")).font(theme::regular(11.5)).color(color))
.on_hover_text(tip);
};
check(
ui,
true,
"Journal chain verified".into(),
format!("Every journal event's hash chains to the one before it.\nHead: {}\nData: {}", b.head, b.data_dir.display()),
);
use ledger_store::DiskCheck;
let (ok, text, tip) = match &b.integrity.disk {
DiskCheck::Matches => (true, "Database matches journal".to_string(), "The server's SQLite database matches a fresh projection of the journal, every table, row for row.".to_string()),
DiskCheck::Behind { applied, journal } => (true, "Database catching up".to_string(), format!("The database has applied {applied} of {journal} journal events; the server applies the rest right after writing them.")),
DiskCheck::Missing => (true, "Database will rebuild".to_string(), "No database file yet; the server builds it from the journal when it starts.".to_string()),
DiskCheck::Differs(why) => (false, "Database differs".to_string(), format!("{why}. The server rebuilds it from the journal (books.verify, or on its next start).")),
DiskCheck::Unreadable(why) => (false, "Database unreadable".to_string(), why.clone()),
};
check(ui, ok, text, tip);
let bad = b.integrity.file_failures.len();
check(
ui,
bad == 0,
if bad == 0 { format!("{} files verified", b.integrity.files_ok) } else { format!("{bad} files damaged") },
"Every stored receipt and document re-hashed against its recorded SHA-256.".into(),
);
});
}