Start LibreLedger: an encrypted money ledger that stores only ciphertext
LibreLedger is a single-person money ledger. The browser encrypts
everything (PBKDF2-HMAC-SHA256 with 250k iterations, then AES-256-GCM),
and a small standard-library Python server stores only the resulting
{v, salt, iv, ct} blob. Ledger files from the earlier Money Ledger version
open unchanged.
- On plain http to a LAN or VPN address, where browsers hide Web Crypto,
the app switches to vendored @noble/hashes and @noble/ciphers 2.2.0 and
says so on the lock screen. tests/crypto-interop.test.mjs shows both
paths read each other's files.
- The server hands out only the app's own files, sends a self-only CSP,
nosniff, frame denial and no-referrer, checks the shape of each blob,
refuses cross-site writes and writes atomically. It keeps the last 10
backups plus one per day for 30 days.
- The container runs that server from a digest-pinned python alpine
image, as an unprivileged user, with its data in /data and a health
check on /api/health.
Assisted-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+112
@@ -0,0 +1,112 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>LibreLedger</title>
|
||||
<link rel="icon" href="favicon.svg" type="image/svg+xml">
|
||||
<script src="theme.js"></script>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Lock / unlock screen -->
|
||||
<div id="lock" class="overlay">
|
||||
<form id="lock-form" class="lock-box">
|
||||
<div class="brand brand-lg">
|
||||
<span class="brand-mark">£</span>
|
||||
<span class="brand-name">LibreLedger</span>
|
||||
</div>
|
||||
<p id="lock-msg">Enter your passphrase to unlock</p>
|
||||
<input type="password" id="pass1" placeholder="Passphrase" autocomplete="current-password">
|
||||
<input type="password" id="pass2" placeholder="Confirm passphrase" autocomplete="new-password" hidden>
|
||||
<button type="submit" id="lock-btn">Unlock</button>
|
||||
<p id="lock-err" class="err"></p>
|
||||
<p id="lock-warn" class="lock-warn" hidden></p>
|
||||
<div id="lock-fs" class="lock-fs"></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<header>
|
||||
<div class="brand">
|
||||
<span class="brand-mark">£</span>
|
||||
<span class="brand-name">LibreLedger</span>
|
||||
</div>
|
||||
<div class="tools">
|
||||
<label class="cur">Currency
|
||||
<input id="currency" maxlength="3" value="£">
|
||||
</label>
|
||||
<button id="btn-theme" type="button" class="icon-btn" title="Toggle dark / light theme" aria-label="Toggle theme"></button>
|
||||
<div class="menu" id="tools-menu">
|
||||
<button type="button" class="menu-btn" id="btn-more" aria-haspopup="true" aria-expanded="false" title="More options">⋯</button>
|
||||
<div class="menu-pop" role="menu" hidden>
|
||||
<button id="btn-linkfile" type="button" hidden>🔗 Link file</button>
|
||||
<button id="btn-csv" type="button">Export CSV</button>
|
||||
<button id="btn-pdf" type="button">Export PDF</button>
|
||||
<button id="btn-backup" type="button">Backup</button>
|
||||
<button id="btn-restore" type="button">Restore</button>
|
||||
<button id="btn-passphrase" type="button">Change passphrase</button>
|
||||
</div>
|
||||
</div>
|
||||
<input type="file" id="file-restore" accept="application/json,.mlg" hidden>
|
||||
<button id="btn-lock" type="button" class="primary">Lock</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="app" class="hidden">
|
||||
<div id="budget-bar" class="budget-bar"></div>
|
||||
<div id="section-nav" class="section-nav"></div>
|
||||
|
||||
<section class="panel" data-section="totals">
|
||||
<div class="panel-head">
|
||||
<h2>📊 Totals</h2>
|
||||
<p class="panel-sub">Your money at a glance — annualised over the <em>next 12 months</em> from your recurring schedule. Click any figure to chart it month by month.</p>
|
||||
</div>
|
||||
<div id="totals-body"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel" data-section="savings">
|
||||
<div class="panel-head">
|
||||
<h2>🐷 Savings</h2>
|
||||
<p class="panel-sub">Every 🐷 Savings-tagged row across your <em>whole ledger</em>, added up month by month. (The 📊 Totals card above counts only the next 12 months, so a longer ledger totals more here.)</p>
|
||||
</div>
|
||||
<div id="savings-body"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel" data-section="affordability">
|
||||
<div class="panel-head">
|
||||
<h2>🏡 Housing affordability</h2>
|
||||
<p class="panel-sub">Your 🏠 Housing costs measured against your 💰 Income — based on the 30% rule of thumb.</p>
|
||||
</div>
|
||||
<div id="afford-body"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel" data-section="balances">
|
||||
<div class="panel-head">
|
||||
<h2>💼 Balances</h2>
|
||||
<p class="panel-sub">Where your money lives right now — this total kicks off the ledger 👇</p>
|
||||
</div>
|
||||
<div id="accounts-body"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel" data-section="recurring">
|
||||
<div class="panel-head">
|
||||
<h2>🔁 Recurring</h2>
|
||||
<p class="panel-sub">Bills & income that repeat — auto-added to new months, or drop into any month on demand.</p>
|
||||
</div>
|
||||
<div id="recurring-body"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel" data-section="ledger">
|
||||
<div class="panel-head">
|
||||
<h2>🌊 Monthly ledger</h2>
|
||||
<p class="panel-sub">Every penny in, every penny out — one smooth balance flowing month to month.</p>
|
||||
</div>
|
||||
<div id="months-body"></div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="app.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user