Commit Graph
22 Commits
Author SHA256 Message Date
jhkaktus c82de902a8 feat: free-text timestamps instead of datetime pickers
Users wanted to type things like "unbekannt" or comments for the date
fields. Start/End are now plain text inputs (max 120 chars) and are
stored/displayed verbatim - no Date parsing, no formatting. Existing
records that contain ISO strings keep working: they render exactly as
stored and refill the edit form unchanged. Also removes a duplicated
block in setup() that had slipped in earlier (double reload/flash).

Verified: node --check, no datetime-local/Date-parsing remnants,
crypto roundtrip with free-text and legacy ISO timestamps, pytest 3/3.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
2026-10-11 00:58:19 +02:00
jhkaktus 8d488f44ff fix: edit and delete buttons now actually fire their handlers
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>
2026-10-11 00:43:21 +02:00
jhkaktus 168927453a fix: backdrop now actually hidden, no longer covers the page
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>
2026-10-11 00:37:57 +02:00
jhkaktus 2df603b32b style+docs: consistent pre-screen styling and first-start UX docs
Setup screen shares the unlock panel look; README gains an "Erster
Start (UX)" section explaining first visit vs returning visit behaviour.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
2026-10-11 00:30:51 +02:00
jhkaktus 535a9c9db0 feat: client logic for setup vs unlock flow and wrong-passphrase handling
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>
2026-10-11 00:30:51 +02:00
jhkaktus 863cd1fd3a feat: first-time setup screen for creating a new passphrase
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>
2026-10-11 00:30:51 +02:00
jhkaktus 6bb6a38040 docs: fix leftover port 5000 in README start instructions
Aligns the example curl-and-start snippet with the 8085 default that
run.py, the systemd unit and the architecture docs already use.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
2026-10-11 00:12:28 +02:00
jhkaktus 47b65508f4 feat: systemd unit for deployment behind reverse proxy (port 8085)
deploy/crushdb.service runs run.py on 127.0.0.1:8085 with auto-restart
and hardening (ProtectSystem=strict, ReadWritePaths only for the SQLite
dir, no capabilities, restricted address families). Verified with
systemd-analyze verify against locally substituted paths (exit 0); the
only remaining warning on the un-substituted unit is the expected
path-not-found for the deployment location in this dev checkout.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
2026-10-11 00:12:03 +02:00
jhkaktus 505b1839dd chore: default port 8085 (app + service unit), README aligned
User's proxy expects the upstream on port 8085; defaults are aligned
across run.py, the systemd unit and the docs.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
2026-10-11 00:11:19 +02:00
jhkaktusandCodebuff 7c48057360 docs: README mentions the systemd deployment path
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
2026-10-11 00:11:06 +02:00
jhkaktus 6c41779667 feat: systemd unit for deployment behind reverse proxy
Adds deploy/crushdb.service: plain HTTP on 127.0.0.1:5000 (HOST/PORT
env), auto-restart on failure, hardened with ProtectSystem/strict,
NoNewPrivileges, capability drop and restricted address families so
only the SQLite dir and private tmp are writable. Passes
systemd-analyze verify (the one reported warning is the expected
/path-in-this-repo venv path of the source checkout, not the deployed
one).

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
2026-10-11 00:10:58 +02:00
jhkaktus ae1af1f8d2 refactor: drop built-in HTTPS, serve plain HTTP behind reverse proxy
TLS termination belongs to the trusted reverse proxy in front
(nginx/caddy/...), not the app. run.py now binds plain HTTP to
127.0.0.1 (HOST/PORT configurable) and the dirty rollback of the
reverted self-signed-cert logic was cleaned up. README updated.
Verified: all endpoints 200 over HTTP, integrity tests 3/3 passing.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
2026-10-11 00:07:06 +02:00
jhkaktus bd9097ba27 Revert "feat: HTTPS via self-signed dev certificate (auto-generated)"
This reverts commit d6f508f848.
2026-10-11 00:06:41 +02:00
jhkaktusandCodebuff bb604768cc docs: README with architecture, security model and test instructions
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
2026-10-11 00:04:18 +02:00
jhkaktus d6f508f848 feat: HTTPS via self-signed dev certificate (auto-generated)
run.py now ensures a self-signed TLS certificate exists (openssl,
SAN localhost/127.0.0.1, 365 days) and serves on https://127.0.0.1:8443.
Falls back to plain HTTP with a loud warning only when openssl is
unavailable. Verified end-to-end with curl over TLS.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
2026-10-11 00:03:45 +02:00
jhkaktus 9bab17e5dd feat: integrity tests – replay, stale-delete and duplicate detection
Adds pytest suite proving server-side tamper/replay guarantees:
stale-version updates and deletes are refused (409), duplicate uuids
are rejected, and post-delete resurrection scenarios are documented.
Combined with the client-side AEAD binding of uuid+salt any server-side
manipulation surfaces as an undecryptable row in the UI. 3/3 passing.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
2026-10-11 00:03:15 +02:00
jhkaktus a963baa8de feat: browser UI – unlock, table view, create/edit/delete with client-side crypto
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>
2026-10-11 00:02:40 +02:00
jhkaktusandCodebuff 7cbc2eecd2 chore: remove leftover stray vendor files from earlier fetch attempt
🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
2026-10-11 00:01:33 +02:00
jhkaktus 2ff58441f4 feat: client-side crypto – Argon2id KDF + XChaCha20-Poly1305 AEAD
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>
2026-10-11 00:01:30 +02:00
jhkaktus a6a37d675d feat: Flask API for encrypted records (CRUD + replay protection)
Exposes a ciphertext-only REST API: list/get/create/update/delete of
records as opaque base64 blobs. Enforces base64/size validation,
rejects duplicates, and uses patch-version checks server-side so
replayed or stale updates are refused. Includes run.py entry point
that enables TLS when certs/ are present. Verified via test client.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
2026-10-10 23:59:19 +02:00
jhkaktus 440cd78203 feat: encrypted records storage model + SQLite layer (ciphertext-only)
Adds the base project setup (requirements, README, gitignore entries)
and a SQLite storage layer that persists only encrypted records:
uuid, ciphertext, nonce, salt, plus a monotonic version counter used
later for tamper/replay detection. Includes a smoke test run.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
2026-10-10 23:56:12 +02:00
jhkaktus 7f045492b1 Initial commit 2026-10-10 21:50:03 +00:00