Replace the HTML preview with a worker-rendered PDF preview

Rendering moves into a module Web Worker behind a latest-wins RenderClient
with a watchdog and a handshake-based main-thread fallback. The preview
shows the real PDF through pdf.js at 2x with zoom, page indicator, audit
issues popover and a focus modal. A4/Letter and margin controls are frozen
into each invoice at issue and can be saved as the default (migration M3).
react-pdf and pdf.js are lazy-loaded; the main chunk shrinks from 1.8 MB
to 0.5 MB. The HTML twin and its styles are removed.
This commit is contained in:
2026-10-04 05:47:19 +05:30
parent a014a907e4
commit cedf4839bd
24 changed files with 1579 additions and 505 deletions
+23 -2
View File
@@ -10,7 +10,7 @@ const MAX_BACKUPS: usize = 10;
const BACKUP_PREFIX: &str = "voiced-pre-v";
/// Highest schema version, i.e. the number of entries in `migrations()`.
const LATEST_VERSION: i64 = 2;
const LATEST_VERSION: i64 = 3;
#[derive(Debug, thiserror::Error)]
pub enum DbError {
@@ -169,8 +169,13 @@ CREATE TABLE IF NOT EXISTS invoice_drafts (
);
"#;
/// Version 3: the user's default page setup (RenderPrefsV1 JSON; empty means built-in defaults).
const M3: &str = r#"
ALTER TABLE app_settings ADD COLUMN render_prefs TEXT NOT NULL DEFAULT '';
"#;
fn migrations() -> Migrations<'static> {
Migrations::new(vec![M::up(SCHEMA), M::up(M2)])
Migrations::new(vec![M::up(SCHEMA), M::up(M2), M::up(M3)])
}
/// Open (creating if needed) the database at `path` and bring it to the latest schema.
@@ -536,6 +541,22 @@ CREATE INDEX IF NOT EXISTS idx_invoices_created ON invoices(created_at DESC);
assert!(backups_in(&backups).is_empty());
}
#[test]
fn render_prefs_column_defaults_to_empty_and_round_trips() {
let conn = open_in_memory().unwrap();
let initial: String = conn
.query_row("SELECT render_prefs FROM app_settings WHERE id = 1", [], |r| r.get(0))
.unwrap();
assert_eq!(initial, "");
let json = r#"{"version":1,"templateId":"classic","pageSize":"LETTER","margins":"narrow"}"#;
conn.execute("UPDATE app_settings SET render_prefs = ?1 WHERE id = 1", [json]).unwrap();
let stored: String = conn
.query_row("SELECT render_prefs FROM app_settings WHERE id = 1", [], |r| r.get(0))
.unwrap();
assert_eq!(stored, json);
}
fn build_v1_database(path: &Path) {
let conn = Connection::open(path).unwrap();
conn.execute_batch(V1_SCHEMA).unwrap();