The shared el() helper passed every attr through setAttribute(); for
event handlers like onclick a function value gets stringified into an
attribute the DOM compiles as an expression that is evaluated but never
called - so Bearbeiten/Löschen (wired through el()) were silent no-ops,
while addEventListener-wired buttons (Neuer Datensatz, Sperren) worked.
el() now assigns on*-function attrs as properties (node[k] = v), preserving
the real handler. Verified with a DOM-semantics shim: fixed path fires,
legacy setAttribute path provably compiles but never calls.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
The #editor-backdrop rule set display:flex, which has higher specificity
than the UA style for the hidden attribute - so the modal overlay was
permanently visible on load and blocked the whole UI underneath (user-
reported: "the new record field hides everything"). Adding
#editor-backdrop[hidden] { display:none } makes the attribute win again;
opening/closing the editor still uses flex centering.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
ui.js now (a) decides setup-vs-unlock from GET /api/records emptiness,
(b) runs the setup flow (double entry check, min length, TOCTOU re-check
that the DB stayed empty), (c) detects a fully failed unlock (all rows
undecryptable = wrong passphrase) and returns to a clean state, and
(d) blocks double submits while Argon2id derives the key.
Verified: node --check passes, live server serves both screens and a
crypto roundtrip proves setup passphrase unlocks on second visit while
a wrong passphrase is rejected.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
UX fix from user feedback: previously the app only showed an "Entsperren"
form, leaving first-time users without any passphrase confused. The page
now loads with two distinct pre-screens decided by an initial API call:
- empty DB -> "Willkommen" setup form: new passphrase, typed twice,
minimum 8 characters, with no-reset warning
- existing ciphertext records -> unlock form as before
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
Vanilla HTML/CSS/JS frontend served by Flask:
- Unlock screen: passphrase -> Argon2id session key, held in memory
only (never sent to the server); "Sperren" wipes key and DOM rows.
- Records table (Person A/B, start/end, action) rendered via
textContent only (no HTML injection); undecryptable rows are flagged.
- Create/edit modal encrypts payloads with XChaCha20-Poly1305 before
any network call; updates carry the server version (optimistic lock,
conflict toast on 409); delete fetches current version first.
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
Adds a vanilla ES-module crypto layer for the browser:
- Argon2id (t=2, m=16 MiB, p=1) derives a 256-bit session key from the
passphrase; the passphrase never leaves the client.
- Records are encrypted with XChaCha20-Poly1305, fresh 24-byte nonce
per record, per-record salt bound into the AEAD associated data
together with the uuid (tamper/swap detection).
- Vendored audited @noble/hashes + @noble/ciphers ES modules; verified
end-to-end in node (encrypt/decrypt roundtrip, wrong key, tampered
salt, swapped uuid all rejected).
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>