const api = (path, opts = {}) => fetch(path, { headers: { "Content-Type": "application/json" }, ...opts, }).then(r => { if (r.status === 204 || !r.body) return { ok: true }; return r.json().then(d => { if (!r.ok) throw new Error(d.error || "Request failed"); return d; }); }); const pinTimers = {}; function fmtCountdown(ms) { if (ms <= 0) return "00:00:00:00"; const d = Math.floor(ms / 86400000); const totalSec = Math.floor((ms % 86400000) / 1000); const h = Math.floor(totalSec / 3600); const m = Math.floor((totalSec % 3600) / 60); const s = totalSec % 60; const dd = String(d).padStart(2, "0"); const hh = String(h).padStart(2, "0"); const mm = String(m).padStart(2, "0"); const ss = String(s).padStart(2, "0"); return `${dd}:${hh}:${mm}:${ss}`; } async function loadPins() { const el = document.getElementById("pins-list"); try { const pins = await api("/api/pins"); if (!pins.length) { el.innerHTML = '

No PINs stored yet.

'; return; } el.innerHTML = ` ${pins.map(p => ` `).join("")}
ID Label Status Countdown Codes
#${p.id} ${esc(p.label) || "(none)"} ${p.locked ? `LOCKED` : `UNLOCKED`} ${fmtCountdown(new Date(p.lock_until) - Date.now())} ${p.remaining_codes}/${4}
`; pins.forEach(p => { if (pinTimers[p.id]) clearInterval(pinTimers[p.id]); pinTimers[p.id] = setInterval(() => { const cell = document.getElementById(`cd-${p.id}`); if (!cell) { clearInterval(pinTimers[p.id]); return; } const remaining = new Date(p.lock_until) - Date.now(); cell.textContent = fmtCountdown(remaining); }, 1000); }); } catch (e) { el.innerHTML = `

${esc(e.message)}

`; } } document.getElementById("add-form").addEventListener("submit", async (e) => { e.preventDefault(); const label = document.getElementById("label").value; const lockDays = parseInt(document.getElementById("lock-days").value) || 30; try { const result = await api("/api/pins", { method: "POST", body: JSON.stringify({ label, lock_days: lockDays }), }); const content = document.getElementById("new-pin-content"); const pinId = "pin-" + result.id; content.innerHTML = `

Your PIN

••••
Save your recovery codes now. They will not be shown again.
${result.recovery_codes.map((c, i) => `
[${i + 1}] ${c}
`).join("")}

Access locked until: ${new Date(result.lock_until).toLocaleDateString()}

`; schedulePrint(result); document.getElementById("new-pin-result").classList.remove("hidden"); document.getElementById("label").value = ""; loadPins(); } catch (err) { alert(err.message); } }); async function accessPin(id) { const title = document.getElementById("modal-title"); const body = document.getElementById("modal-body"); title.textContent = `Access PIN #${id}`; const pins = await api("/api/pins"); const pin = pins.find(p => p.id === id); if (pin && !pin.locked) { body.innerHTML = `

This PIN is unlocked and ready to view.

`; document.getElementById("modal-overlay").classList.remove("hidden"); document.getElementById("reveal-btn").addEventListener("click", async () => { try { const result = await api(`/api/pins/${id}/access`, { method: "POST", body: JSON.stringify({}), }); body.innerHTML = `

Your PIN

${result.pin}
`; loadPins(); } catch (err) { body.innerHTML = `

${esc(err.message)}

`; } }); } else { body.innerHTML = `

Enter a recovery code to bypass the lock:

`; document.getElementById("modal-overlay").classList.remove("hidden"); const bypassInput = document.getElementById("bypass-input"); bypassInput.addEventListener("paste", (e) => { e.preventDefault(); }); bypassInput.addEventListener("drop", (e) => { e.preventDefault(); }); document.getElementById("access-form").addEventListener("submit", async (e) => { e.preventDefault(); const code = bypassInput.value.trim(); const errEl = document.getElementById("access-error"); errEl.textContent = ""; try { const result = await api(`/api/pins/${id}/access`, { method: "POST", body: JSON.stringify({ bypass_code: code }), }); body.innerHTML = `

Your PIN

${result.pin}
`; loadPins(); } catch (err) { errEl.textContent = err.message; } }); } } async function deletePin(id, label) { const lbl = label || `#${id}`; if (!confirm(`Delete PIN for "${lbl}"?`)) return; try { await api(`/api/pins/${id}`, { method: "DELETE" }); loadPins(); } catch (e) { alert(e.message); } } function closeModal() { document.getElementById("modal-overlay").classList.add("hidden"); } document.getElementById("modal-overlay").addEventListener("click", (e) => { if (e.target.id === "modal-overlay") closeModal(); }); function copyText(elId) { const text = document.getElementById(elId).textContent; navigator.clipboard.writeText(text); } function togglePin(elId, pin, btnId) { const el = document.getElementById(elId); const btn = document.getElementById(btnId); if (el.classList.contains("masked")) { el.textContent = pin; el.classList.remove("masked"); btn.innerHTML = "👁 Hide PIN"; } else { el.textContent = "\u2022\u2022\u2022\u2022"; el.classList.add("masked"); btn.innerHTML = "👁 Reveal PIN"; } } function copyTextDirect(text) { navigator.clipboard.writeText(text); } let _printData = null; function schedulePrint(result) { _printData = result; } function doPrint() { if (!_printData) return; const r = _printData; const w = window.open("", "_blank", "width=600,height=600"); const dateStr = new Date(r.lock_until).toLocaleDateString("en-US", {year:"numeric",month:"long",day:"numeric"}); const labelStr = r.label || "(no label)"; const codeRows = r.recovery_codes.map((c, i) => `
${i+1}
${c}
` ).join("\n"); w.document.write(` PinVault - PIN #${r.id} `); w.document.close(); setTimeout(() => { w.print(); }, 400); } function esc(s) { const d = document.createElement("div"); d.textContent = s; return d.innerHTML; } loadPins();