helwasm: store-on-correct, store button reflects existence, use stored spec

- new read-only export hel_entry(name): the entry's canonical stored form, or ""
- Mark correct now also stores the name (keeps the entry); unmark keeps the stored
  name and just drops the hash (wraps add/rm for an ephemeral entry so the right
  hash is removed)
- Store button shows "Stored ✓" (disabled, ocean outline) when the name is already
  in the catalog, "Store" otherwise
- when a typed name already exists, the field adopts the full stored spec on blur
  (real mode/seq/date/parent) instead of a default-extended parse
This commit is contained in:
Oleksandr Kozachuk
2026-06-09 23:12:48 +02:00
parent c60e6c8ce2
commit 0dd173f113
7 changed files with 86 additions and 14 deletions
+34 -12
View File
@@ -201,7 +201,7 @@
</div> </div>
<script type="module"> <script type="module">
import init, { hel_init, hel_command, hel_load_script, hel_parse, hel_parse_name } from "./pkg/helwasm.js"; import init, { hel_init, hel_command, hel_load_script, hel_parse, hel_parse_name, hel_entry } from "./pkg/helwasm.js";
const $ = (s) => document.querySelector(s); const $ = (s) => document.querySelector(s);
const masterEl = () => document.getElementById("master"); const masterEl = () => document.getElementById("master");
@@ -265,6 +265,15 @@
// only the one-way "correct" hash is persisted on the device. // only the one-way "correct" hash is persisted on the device.
let lastSecret = ""; let lastSecret = "";
let appliedMaster = null; // last master value pushed to the engine (debounce hint) let appliedMaster = null; // last master value pushed to the engine (debounce hint)
function refreshStoreBtn(name) {
const b = $("#store");
if (!b) return;
const exists = name ? !!hel_entry(name) : false;
b.classList.toggle("is-stored", exists);
b.disabled = exists || !name;
b.textContent = exists ? "Stored ✓" : "Store";
b.title = exists ? "This name is already in your catalog" : "Save this name in this browser";
}
function refreshCorrectBtn(enabled, isCorrect) { function refreshCorrectBtn(enabled, isCorrect) {
const b = $("#markCorrect"); const b = $("#markCorrect");
if (!b) return; if (!b) return;
@@ -287,6 +296,7 @@
sec.textContent = ""; sec.classList.remove("correct"); sec.textContent = ""; sec.classList.remove("correct");
lenEl.textContent = ""; lastSecret = ""; lenEl.textContent = ""; lastSecret = "";
refreshCorrectBtn(false, false); refreshCorrectBtn(false, false);
refreshStoreBtn("");
return; return;
} }
const master = masterEl().value; const master = masterEl().value;
@@ -316,6 +326,7 @@
sec.classList.toggle("correct", correct); sec.classList.toggle("correct", correct);
lenEl.textContent = pw ? "len " + pw.length : ""; lenEl.textContent = pw ? "len " + pw.length : "";
refreshCorrectBtn(!!pw, correct); refreshCorrectBtn(!!pw, correct);
refreshStoreBtn(name);
if (!pw && !silent) toast("No output"); if (!pw && !silent) toast("No output");
} }
function toggleCorrect() { function toggleCorrect() {
@@ -324,19 +335,25 @@
const spec = $("#qname").value.trim(); const spec = $("#qname").value.trim();
const name = hel_parse_name(spec); const name = hel_parse_name(spec);
if (!name) return; if (!name) return;
const verb = b.classList.contains("is-correct") ? "uncorrect " : "correct "; const marking = !b.classList.contains("is-correct");
// Mark / unmark THIS name + master on the device (stores SHA1(name‖password)). const exists = !!hel_entry(name);
// The entry may be ephemeral (not Stored), so wrap add/rm around it so // Marking correct also STORES the name (you only confirm things you keep), so
// correct/uncorrect can recompute the password to hash — same as the console. // the entry stays in the catalog. The hash is SHA1(name‖password) on the device.
if (/^error: name .* not found/m.test(hel_command("enc " + name))) { if (marking) {
hel_command("add " + spec); if (!exists) hel_command("add " + spec); // store it if new
hel_command(verb + name); hel_command("correct " + name);
hel_command("rm " + name); persist();
} else if (exists) {
hel_command("uncorrect " + name); // drop the hash; keep the stored name
} else { } else {
hel_command(verb + name); // not stored (e.g. a stale hash): wrap add/rm so uncorrect can recompute the
// password to remove the right hash, without leaving the entry behind.
hel_command("add " + spec);
hel_command("uncorrect " + name);
hel_command("rm " + name);
} }
quickGen(true); quickGen(true);
toast(verb === "correct " ? "Marked correct" : "No longer marked correct"); toast(marking ? "Marked correct & stored" : "No longer marked correct");
} }
function quickStore() { function quickStore() {
const spec = $("#qname").value.trim(); const spec = $("#qname").value.trim();
@@ -345,6 +362,7 @@
const out = hel_command("add " + spec); const out = hel_command("add " + spec);
persist(); persist();
appendLine('<span class="muted"># stored ' + esc(name) + "</span>"); appendLine('<span class="muted"># stored ' + esc(name) + "</span>");
refreshStoreBtn(name);
toast(/^(error|warning)/m.test(out) ? out.split("\n")[0] : "Stored " + name); toast(/^(error|warning)/m.test(out) ? out.split("\n")[0] : "Stored " + name);
} }
@@ -462,7 +480,11 @@
$("#qname").addEventListener("blur", () => { $("#qname").addEventListener("blur", () => {
const spec = $("#qname").value.trim(); const spec = $("#qname").value.trim();
if (!spec) return; if (!spec) return;
const norm = hel_parse(spec); // If the name is already in the catalog, use its FULL stored spec (real
// mode/seq/date/parent), not a fresh default-extended parse of what was typed.
const name = hel_parse_name(spec);
const stored = name ? hel_entry(name) : "";
const norm = stored || hel_parse(spec);
if (norm && norm !== spec) { if (norm && norm !== spec) {
$("#qname").value = norm; $("#qname").value = norm;
quickGen(true); quickGen(true);
+10 -1
View File
@@ -6,6 +6,14 @@
*/ */
export function hel_command(cmd: string): string; export function hel_command(cmd: string): string;
/**
* Look up an entry by its exact name and return its canonical stored form
* (`name [len]mode seq date comment ^parent`), or "" if it is not in the catalog.
* Read-only. The UI uses this to detect "already stored" and to use the real stored
* spec (its mode/seq/date/parent) instead of the bare name the user typed.
*/
export function hel_entry(name: string): string;
/** /**
* Call once at page load: routes Rust panics to the browser console with a * Call once at page load: routes Rust panics to the browser console with a
* readable message + stack instead of an opaque "unreachable" trap. * readable message + stack instead of an opaque "unreachable" trap.
@@ -38,11 +46,12 @@ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembl
export interface InitOutput { export interface InitOutput {
readonly memory: WebAssembly.Memory; readonly memory: WebAssembly.Memory;
readonly hel_init: () => void;
readonly hel_command: (a: number, b: number) => [number, number]; readonly hel_command: (a: number, b: number) => [number, number];
readonly hel_entry: (a: number, b: number) => [number, number];
readonly hel_load_script: (a: number, b: number) => [number, number]; readonly hel_load_script: (a: number, b: number) => [number, number];
readonly hel_parse: (a: number, b: number) => [number, number]; readonly hel_parse: (a: number, b: number) => [number, number];
readonly hel_parse_name: (a: number, b: number) => [number, number]; readonly hel_parse_name: (a: number, b: number) => [number, number];
readonly hel_init: () => void;
readonly __wbindgen_free: (a: number, b: number, c: number) => void; readonly __wbindgen_free: (a: number, b: number, c: number) => void;
readonly __wbindgen_malloc: (a: number, b: number) => number; readonly __wbindgen_malloc: (a: number, b: number) => number;
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
+23
View File
@@ -20,6 +20,29 @@ export function hel_command(cmd) {
} }
} }
/**
* Look up an entry by its exact name and return its canonical stored form
* (`name [len]mode seq date comment ^parent`), or "" if it is not in the catalog.
* Read-only. The UI uses this to detect "already stored" and to use the real stored
* spec (its mode/seq/date/parent) instead of the bare name the user typed.
* @param {string} name
* @returns {string}
*/
export function hel_entry(name) {
let deferred2_0;
let deferred2_1;
try {
const ptr0 = passStringToWasm0(name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.hel_entry(ptr0, len0);
deferred2_0 = ret[0];
deferred2_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
} finally {
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
}
}
/** /**
* Call once at page load: routes Rust panics to the browser console with a * Call once at page load: routes Rust panics to the browser console with a
* readable message + stack instead of an opaque "unreachable" trap. * readable message + stack instead of an opaque "unreachable" trap.
Binary file not shown.
+2 -1
View File
@@ -1,11 +1,12 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export const memory: WebAssembly.Memory; export const memory: WebAssembly.Memory;
export const hel_init: () => void;
export const hel_command: (a: number, b: number) => [number, number]; export const hel_command: (a: number, b: number) => [number, number];
export const hel_entry: (a: number, b: number) => [number, number];
export const hel_load_script: (a: number, b: number) => [number, number]; export const hel_load_script: (a: number, b: number) => [number, number];
export const hel_parse: (a: number, b: number) => [number, number]; export const hel_parse: (a: number, b: number) => [number, number];
export const hel_parse_name: (a: number, b: number) => [number, number]; export const hel_parse_name: (a: number, b: number) => [number, number];
export const hel_init: () => void;
export const __wbindgen_free: (a: number, b: number, c: number) => void; export const __wbindgen_free: (a: number, b: number, c: number) => void;
export const __wbindgen_malloc: (a: number, b: number) => number; export const __wbindgen_malloc: (a: number, b: number) => number;
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
+14
View File
@@ -46,6 +46,20 @@ pub fn hel_parse_name(spec: String) -> String {
} }
} }
/// Look up an entry by its exact name and return its canonical stored form
/// (`name [len]mode seq date comment ^parent`), or "" if it is not in the catalog.
/// Read-only. The UI uses this to detect "already stored" and to use the real stored
/// spec (its mode/seq/date/parent) instead of the bare name the user typed.
#[wasm_bindgen]
pub fn hel_entry(name: String) -> String {
let cell = STATE.lock();
let lk = cell.borrow();
match lk.db.get(&name).or_else(|| lk.ls.get(&name)) {
Some(p) => p.lock().borrow().to_string().trim().to_string(),
None => String::new(),
}
}
/// Run a whole multi-line script (every `add …` line, `set …`, etc.) against the /// Run a whole multi-line script (every `add …` line, `set …`, etc.) against the
/// shared state in one call. Used to bulk-import a pasted catalog (e.g. the text /// shared state in one call. Used to bulk-import a pasted catalog (e.g. the text
/// of the Notion page) and to load the persisted catalog from localStorage. /// of the Notion page) and to load the persisted catalog from localStorage.
+3
View File
@@ -185,6 +185,9 @@ input.mask.revealed { -webkit-text-security: none; }
.btn.small.is-correct { background: var(--ocean); color: var(--cream); border-color: var(--ocean); } .btn.small.is-correct { background: var(--ocean); color: var(--cream); border-color: var(--ocean); }
.btn.small.is-correct:hover { background: var(--deep); border-color: var(--deep); } .btn.small.is-correct:hover { background: var(--deep); border-color: var(--deep); }
.btn:disabled { opacity: .4; cursor: not-allowed; pointer-events: none; } .btn:disabled { opacity: .4; cursor: not-allowed; pointer-events: none; }
/* "Stored ✓": not an action (disabled), but shown as a confirmed ocean outline
rather than the dimmed disabled look. */
.btn.small.is-stored { opacity: 1; background: transparent; color: var(--ocean); border-color: var(--ocean); }
/* Quick-gen result row */ /* Quick-gen result row */
.result { .result {