Add backup/restore, daily auto-backup and history CSV/JSON export (Phase E3)
- create_backup writes a zip (manifest with sha256 per file, a VACUUM INTO database snapshot, assets, archive, fonts) atomically. restore_backup validates the manifest, rejects tampered files, path traversal, symlinks and newer schemas, checks the staged DB with integrity_check, then stages a pending restore. The swap runs on the next start before the database opens, moves the current data to backups/pre-restore-<ts>/ and rolls back on any failure. - Daily automatic backup (migration M9 auto_backup, default on) keeps the newest 14 and never blocks startup. - Settings Data tab: back up now, restore with confirmation and restart, auto-backup toggle, last backup time. - History exports the filtered rows as CSV (UTF-8 BOM, CRLF, formula-injection guard) or JSON (voiced.history.v1) with CA-friendly tax columns from a new list_invoice_ledger command. - Rust round-trip, rejection, swap-rollback and retention tests; vitest for the export and backup helpers. Adds the zip 8.6.0 and typed-path 0.12.3 crates (pure Rust); package builds must vendor them. PDF output and fingerprint goldens are unchanged. The new UI has not been run in a webview yet.
This commit is contained in:
@@ -19,7 +19,9 @@ import {
|
||||
Tag,
|
||||
Tile,
|
||||
} from "@carbon/react";
|
||||
import { save } from "@tauri-apps/plugin-dialog";
|
||||
import { api } from "../lib/api";
|
||||
import { exportHistory, type HistoryFormat } from "../lib/historyExport";
|
||||
import { formatDate } from "../lib/format";
|
||||
import { formatPaise } from "../lib/money";
|
||||
import { statusBadge } from "../lib/payments";
|
||||
@@ -73,6 +75,7 @@ export default function InvoiceHistory({ settings, active, onOpen }: Props) {
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(PAGE_SIZES[0]);
|
||||
const exp = useInvoiceExport({ settings, bank: null });
|
||||
const [exportingHistory, setExportingHistory] = useState(false);
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
@@ -123,6 +126,34 @@ export default function InvoiceHistory({ settings, active, onOpen }: Props) {
|
||||
}
|
||||
};
|
||||
|
||||
/** Exactly the rows the table is filtered to (every page, in the current sort order). */
|
||||
const onExportHistory = async (format: HistoryFormat) => {
|
||||
setExportingHistory(true);
|
||||
try {
|
||||
const done = await exportHistory(
|
||||
{
|
||||
listLedger: api.listInvoiceLedger,
|
||||
getLastExportDir: api.getLastExportDir,
|
||||
save: ({ defaultPath, format: fmt }) =>
|
||||
save({
|
||||
defaultPath,
|
||||
filters: [fmt === "csv" ? { name: "CSV (Excel)", extensions: ["csv"] } : { name: "JSON", extensions: ["json"] }],
|
||||
}),
|
||||
writeExportFile: api.writeExportFile,
|
||||
now: () => new Date(),
|
||||
},
|
||||
sorted,
|
||||
filters,
|
||||
format,
|
||||
);
|
||||
if (done) toast.success(`Exported ${done.count} invoices`, done.path);
|
||||
} catch (e) {
|
||||
toast.error("Could not export the invoices", String(e));
|
||||
} finally {
|
||||
setExportingHistory(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="voiced-page">
|
||||
<h1 className="voiced-page__title">Invoices</h1>
|
||||
@@ -216,6 +247,18 @@ export default function InvoiceHistory({ settings, active, onOpen }: Props) {
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="voiced-history__export">
|
||||
<Button kind="tertiary" size="md" disabled={sorted.length === 0 || exportingHistory} onClick={() => void onExportHistory("csv")}>
|
||||
Export CSV
|
||||
</Button>
|
||||
<Button kind="ghost" size="md" disabled={sorted.length === 0 || exportingHistory} onClick={() => void onExportHistory("json")}>
|
||||
Export JSON
|
||||
</Button>
|
||||
<span className="voiced-muted">
|
||||
{filtersActive ? `The ${sorted.length} filtered invoices` : `All ${sorted.length} invoices`}, for your accountant
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{sorted.length === 0 ? (
|
||||
<div className="voiced-empty">No invoices match these filters.</div>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user