← back to Robet Site
overnight loop item (g): admin UX — show/hide value toggle + live inline validation (disable Save until valid, name-format hint)
80f29c7a9e7f33f93ada8c29423596add2b2ba57 · 2026-06-29 20:09:16 -0700 · Steve
Files touched
M public/admin.htmlM public/admin.js
Diff
commit 80f29c7a9e7f33f93ada8c29423596add2b2ba57
Author: Steve <steve@designerwallcoverings.com>
Date: Mon Jun 29 20:09:16 2026 -0700
overnight loop item (g): admin UX — show/hide value toggle + live inline validation (disable Save until valid, name-format hint)
---
public/admin.html | 12 +++++++-----
public/admin.js | 34 +++++++++++++++++++++++++++++++---
2 files changed, 38 insertions(+), 8 deletions(-)
diff --git a/public/admin.html b/public/admin.html
index 833a8b8..4d92c61 100644
--- a/public/admin.html
+++ b/public/admin.html
@@ -21,12 +21,14 @@
</p>
<!-- add / update -->
- <div class="keyrow" style="grid-template-columns:1fr 1fr auto">
- <input id="kName" placeholder="KEY NAME (e.g. OPENAI_API_KEY)" />
- <input id="kVal" placeholder="value" type="password" />
- <button class="btn" id="saveBtn">Save</button>
+ <div class="keyrow" style="grid-template-columns:1fr 1fr auto auto">
+ <input id="kName" placeholder="KEY NAME (e.g. OPENAI_API_KEY)" autocomplete="off" spellcheck="false" />
+ <input id="kVal" placeholder="value" type="password" autocomplete="off" spellcheck="false" />
+ <button type="button" class="btn" id="toggleVal" aria-pressed="false" aria-label="Show value" title="Show / hide value">show</button>
+ <button class="btn" id="saveBtn" disabled>Save</button>
</div>
- <div class="formmsg" id="msg" style="margin:12px 0 28px"></div>
+ <div class="formmsg" id="hint" style="margin:8px 0 0; opacity:.8">Name: letters, digits, underscores only. Value required.</div>
+ <div class="formmsg" id="msg" style="margin:8px 0 28px"></div>
<!-- stored keys -->
<div class="keytable" id="table"></div>
diff --git a/public/admin.js b/public/admin.js
index 7bd705b..0d25136 100644
--- a/public/admin.js
+++ b/public/admin.js
@@ -21,16 +21,44 @@ async function load() {
}));
}
+// ── show/hide value toggle ──
+const toggle = $('#toggleVal'), kVal = $('#kVal');
+toggle.addEventListener('click', () => {
+ const show = kVal.type === 'password';
+ kVal.type = show ? 'text' : 'password';
+ toggle.textContent = show ? 'hide' : 'show';
+ toggle.setAttribute('aria-pressed', String(show));
+ toggle.setAttribute('aria-label', show ? 'Hide value' : 'Show value');
+ kVal.focus();
+});
+
+// ── inline validation (live) ──
+const NAME_RE = /^[A-Za-z0-9_]+$/;
+const hint = $('#hint');
+function validate() {
+ const name = $('#kName').value.trim(), value = kVal.value;
+ let ok = true, m = 'Name: letters, digits, underscores only. Value required.';
+ if (name && !NAME_RE.test(name)) { ok = false; m = '✗ Name allows only letters, digits, underscores (no spaces).'; }
+ else if (!name || !value) { ok = false; }
+ $('#saveBtn').disabled = !ok;
+ hint.textContent = m;
+ hint.style.color = (name && !NAME_RE.test(name)) ? 'var(--accent2)' : '';
+ return ok;
+}
+$('#kName').addEventListener('input', validate);
+kVal.addEventListener('input', validate);
+
$('#saveBtn').addEventListener('click', async () => {
- const name = $('#kName').value.trim(), value = $('#kVal').value;
+ const name = $('#kName').value.trim(), value = kVal.value;
const msg = $('#msg');
- if (!name || !value) { msg.textContent = '✗ name and value required'; return; }
+ if (!validate()) { msg.textContent = '✗ Fix the highlighted field'; return; }
const j = await api('/api/keys', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, value }),
});
msg.textContent = j.ok ? `✓ Saved ${j.name} (${j.masked})` : '✗ ' + (j.error || 'Failed');
- if (j.ok) { $('#kName').value = ''; $('#kVal').value = ''; load(); }
+ if (j.ok) { $('#kName').value = ''; kVal.value = ''; validate(); load(); }
});
+validate();
load();
← 7e8855d loop ledger: item (d) done + deployed
·
back to Robet Site
·
loop ledger: item (g) done + deployed cf3d24b →