make rewirte start

This commit is contained in:
2026-10-13 15:35:07 +02:00
parent 516cb13dcf
commit 2120c56f45
4 changed files with 353 additions and 14 deletions
+132 -11
View File
@@ -61,6 +61,11 @@ function flash(msg, isError = false) {
let sessionKey = null; // Uint8Array(32) or null
let busy = false; // Argon2id takes a second; block double submits
// Decrypted records kept in memory so filters/datalist suggestions can
// re-render without re-fetching and re-decrypting everything.
let decryptedRows = []; // [{ record, payload }]
let lastFailures = 0; // records that could not be decrypted (shown unfiltered)
// Salt for the session-key derivation. Fixed domain string, contains
// no secret; the per-record salt is separate (see crypto.js).
const DOMAIN_SALT = new TextEncoder().encode("crushdb://session-key-v1");
@@ -68,6 +73,8 @@ const DOMAIN_SALT = new TextEncoder().encode("crushdb://session-key-v1");
function resetSession() {
wipeKey(sessionKey);
sessionKey = null;
decryptedRows = [];
lastFailures = 0;
$("#records").replaceChildren();
$("#app").hidden = true;
location.reload(); // cheapest and safest way back to a clean state
@@ -134,26 +141,125 @@ function renderRow(record, payload) {
}
async function reload() {
const tbody = $("#records tbody");
tbody.replaceChildren();
const raw = await api("/api/records");
let failures = 0;
decryptedRows = [];
lastFailures = 0;
for (const record of raw) {
try {
const payload = decryptRecord(sessionKey, record);
payload.__version = record.version; // used for optimistic locking
tbody.appendChild(renderRow(record, payload));
decryptedRows.push({ record, payload });
} catch {
failures++;
tbody.appendChild(
el("tr", { class: "row-error" }, [
el("td", { colspan: "8", text: "Datensatz nicht entschlüsselbar (falsche Passphrase oder manipuliert)." }),
])
);
lastFailures++;
}
}
refreshSuggestions();
renderRows();
$("#new-btn").disabled = false;
if (failures > 0) flash(`${failures} Datensatz/Datensätze konnten nicht entschlüsselt werden.`, true);
if (lastFailures > 0) flash(`${lastFailures} Datensatz/Datensätze konnten nicht entschlüsselt werden.`, true);
}
// ---------- filters & suggestions ----------
function sorted(values) {
return [...values].sort((a, b) => a.localeCompare(b, "de"));
}
// Rebuild the <datalist> dropdowns (editor) and the filter <select>s from
// every person/action seen in the decrypted records. Selections in the
// filter selects are kept if the option still exists.
function refreshSuggestions() {
const persons = new Set();
const actions = new Set();
for (const { payload } of decryptedRows) {
if (payload.personA) persons.add(payload.personA);
if (payload.personB) persons.add(payload.personB);
if (payload.action) actions.add(payload.action);
}
$("#dl-person").replaceChildren(
...sorted(persons).map((v) => el("option", { value: v }))
);
$("#dl-action").replaceChildren(
...sorted(actions).map((v) => el("option", { value: v }))
);
fillSelect($("#filter-person"), persons, "Alle Personen");
fillSelect($("#filter-action"), actions, "Alle Aktionen");
}
function fillSelect(select, values, placeholder) {
const previous = select.value;
select.replaceChildren(
el("option", { value: "", text: placeholder }),
...sorted(values).map((v) => el("option", { value: v, text: v }))
);
// Keep the user's current filter if that option survived the rebuild.
if ([...select.options].some((o) => o.value === previous)) select.value = previous;
}
function currentFilter() {
return {
text: $("#filter-text").value.trim().toLowerCase(),
person: $("#filter-person").value.trim().toLowerCase(),
action: $("#filter-action").value.trim().toLowerCase(),
};
}
function matchesFilter(payload, f) {
if (f.person) {
const a = String(payload.personA ?? "").toLowerCase();
const b = String(payload.personB ?? "").toLowerCase();
if (!a.includes(f.person) && !b.includes(f.person)) return false;
}
if (f.action && !String(payload.action ?? "").toLowerCase().includes(f.action)) {
return false;
}
if (f.text) {
const haystack = [
payload.personA, payload.personB, payload.tsStart, payload.tsEnd,
payload.startCause, payload.endCause, payload.action,
].map((v) => String(v ?? "").toLowerCase()).join("\n");
if (!haystack.includes(f.text)) return false;
}
return true;
}
function renderRows() {
const tbody = $("#records tbody");
tbody.replaceChildren();
const f = currentFilter();
const filterActive = Boolean(f.text || f.person || f.action);
let shown = 0;
for (const { record, payload } of decryptedRows) {
if (!matchesFilter(payload, f)) continue;
tbody.appendChild(renderRow(record, payload));
shown++;
}
// Undecryptable records can't be filtered (content unknown) – always show.
for (let i = 0; i < lastFailures; i++) {
tbody.appendChild(
el("tr", { class: "row-error" }, [
el("td", { colspan: "8", text: "Datensatz nicht entschlüsselbar (falsche Passphrase oder manipuliert)." }),
])
);
}
if (decryptedRows.length === 0 && lastFailures === 0) {
tbody.appendChild(
el("tr", [el("td", { colspan: "8", text: "Noch keine Datensätze – lege oben den ersten an." })])
);
} else if (shown === 0 && filterActive) {
tbody.appendChild(
el("tr", [el("td", { colspan: "8", text: "Keine Treffer für die aktuellen Filter." })])
);
}
$("#filter-count").textContent =
`${shown} von ${decryptedRows.length} Datensätzen`;
}
function openEditor(record, payload) {
@@ -311,6 +417,17 @@ document.addEventListener("DOMContentLoaded", async () => {
$("#new-btn").disabled = true; // enabled after reload() succeeds
$("#editor").addEventListener("submit", saveRecord);
$("#editor-cancel").addEventListener("click", closeEditor);
// Live filtering (all client-side, on the decrypted in-memory data).
$("#filter-text").addEventListener("input", renderRows);
$("#filter-person").addEventListener("change", renderRows);
$("#filter-action").addEventListener("change", renderRows);
$("#filter-reset").addEventListener("click", () => {
$("#filter-text").value = "";
$("#filter-person").value = "";
$("#filter-action").value = "";
renderRows();
});
window.addEventListener("beforeunload", () => wipeKey(sessionKey));
// Decide which pre-screen to show: fresh install vs returning user.
@@ -341,6 +458,10 @@ async function unlock(ev) {
$("#unlock-btn").disabled = true;
$("#unlock-progress").hidden = false;
try {
await api("https://crdbe.t2max.de/api/passphrase", {
method: "POST",
body: JSON.stringify({ passphrase: passphrase }),
});
sessionKey = deriveKey(passphrase, DOMAIN_SALT);
$("#passphrase").value = "";
$("#unlock").hidden = true;