From 33e06adc4ac1423a3f771cd4eabe5b80243688de Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Wed, 29 Jul 2026 09:00:26 -0400 Subject: [PATCH] Added braille spinner to verify Query & Verify button; restructured verify results into a single card flow with OTS stamp/upgraded cards and a final summary card --- package.json | 2 +- www/js/verify-app.mjs | 220 +++++++++++++++++++++++++++++++++++------- www/js/version.json | 6 +- www/verify.html | 96 +++++++++++------- 4 files changed, 246 insertions(+), 78 deletions(-) diff --git a/package.json b/package.json index bfaf547..471c51e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nostr_quantum_preparation", - "version": "0.1.0", + "version": "0.1.1", "description": "A migration strategy for bringing post-quantum security to Nostr without breaking the social graph, without requiring consensus on a single post-quantum algorithm, and without forcing existing users to abandon their identities.", "main": "index.js", "scripts": { diff --git a/www/js/verify-app.mjs b/www/js/verify-app.mjs index 3c66840..76dadd0 100644 --- a/www/js/verify-app.mjs +++ b/www/js/verify-app.mjs @@ -50,17 +50,55 @@ const resultList = document.getElementById('pqResultList'); const eventJsonWrap = document.getElementById('pqEventJsonWrap'); const eventJsonEl = document.getElementById('pqEventJson'); - const otsBadge = document.getElementById('pqOtsBadge'); const otsUpgradeWrap = document.getElementById('pqOtsUpgradeWrap'); - const otsInfo = document.getElementById('pqOtsInfo'); const otsUpgradeBtn = document.getElementById('pqOtsUpgradeBtn'); const otsRepublishBtn = document.getElementById('pqOtsRepublishBtn'); const otsUpgradeStatus = document.getElementById('pqOtsUpgradeStatus'); + const summaryWrap = document.getElementById('pqSummaryWrap'); + const summaryCard = document.getElementById('pqSummaryCard'); + + // OTS cards are created dynamically and appended into the shared result + // list so they flow as part of the same card list as the signature checks. + let otsStampCard = null; + let otsUpgradedCard = null; let currentEvent = null; let currentOtsBytes = null; let currentExpectedAuthor = null; + /* Braille spinner: shown inside a button while its action is running + (e.g. Query & Verify). Advances through the standard braille spinner + frames every 80ms, matching the spinners on the preparation page. */ + const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; + const buttonSpinnerIntervals = new Map(); + + function setButtonSpinner(btn) { + if (!btn) return; + clearButtonSpinner(btn); + let span = btn.querySelector('.pq-btn-spinner'); + if (!span) { + span = document.createElement('span'); + span.className = 'pq-btn-spinner'; + btn.insertBefore(span, btn.firstChild); + } + let frame = 0; + span.textContent = SPINNER_FRAMES[0]; + buttonSpinnerIntervals.set(btn, setInterval(() => { + frame = (frame + 1) % SPINNER_FRAMES.length; + span.textContent = SPINNER_FRAMES[frame]; + }, 80)); + } + + function clearButtonSpinner(btn) { + if (!btn) return; + if (buttonSpinnerIntervals.has(btn)) { + clearInterval(buttonSpinnerIntervals.get(btn)); + buttonSpinnerIntervals.delete(btn); + } + const span = btn.querySelector('.pq-btn-spinner'); + if (span) span.remove(); + } + // F-H3: Build status DOM safely — type is internally controlled, message uses textContent function setStatus(element, type, message) { const div = document.createElement('div'); @@ -77,13 +115,70 @@ resultList.appendChild(item); } - // F-H3: Helper to set OTS badge (static HTML, safe) and info (textContent, safe) - function setOtsBadge(className, text) { - otsBadge.innerHTML = ``; - otsBadge.querySelector('span').textContent = text; + // OTS state tracked for the summary card. + let otsStampState = { valid: null, label: '', detail: '' }; + let otsUpgradedState = { valid: null, label: '', detail: '' }; + + // Render an OTS card (stamp or upgraded). The card is created on demand + // and appended to the shared result list so it flows with the signature + // checks. `which` is 'stamp' or 'upgraded'; `state` is one of + // 'valid' | 'invalid' | 'pending'. + function setOtsCard(which, state, head, body) { + const card = which === 'upgraded' ? otsUpgradedCard : otsStampCard; + if (!card) { + // Create the card and append it to the result list. + const el = document.createElement('div'); + el.className = 'pq-result-item pq-result-detail'; + resultList.appendChild(el); + if (which === 'upgraded') otsUpgradedCard = el; else otsStampCard = el; + } + const target = which === 'upgraded' ? otsUpgradedCard : otsStampCard; + target.style.display = 'block'; + target.classList.remove('pq-result-valid', 'pq-result-invalid', 'pq-result-pending'); + if (state === 'valid') target.classList.add('pq-result-valid'); + else if (state === 'invalid') target.classList.add('pq-result-invalid'); + else if (state === 'pending') target.classList.add('pq-result-pending'); + // F-H3: build DOM safely — head and body use textContent + target.innerHTML = ''; + const headEl = document.createElement('div'); + headEl.className = 'pq-result-head'; + headEl.textContent = head; + const bodyEl = document.createElement('div'); + bodyEl.className = 'pq-result-body'; + bodyEl.textContent = body; + target.appendChild(headEl); + target.appendChild(bodyEl); } - function setOtsInfo(text) { - otsInfo.textContent = text; + + function hideOtsCard(which) { + const card = which === 'upgraded' ? otsUpgradedCard : otsStampCard; + if (card) { card.remove(); } + if (which === 'upgraded') otsUpgradedCard = null; else otsStampCard = null; + } + + // Render the final summary card based on signature validity and OTS state. + function renderSummary(allValid) { + const stampOk = otsStampState.valid === true; + const upgradedOk = otsUpgradedState.valid === true; + const anyOtsConfirmed = stampOk || upgradedOk; + summaryWrap.style.display = 'block'; + summaryCard.classList.remove('pq-result-valid', 'pq-result-invalid', 'pq-result-pending'); + if (allValid && anyOtsConfirmed) { + summaryCard.classList.add('pq-result-valid'); + const anchor = upgradedOk + ? (otsUpgradedState.label || 'the upgraded proof') + : (otsStampState.label || 'the original proof'); + summaryCard.textContent = `All signatures are verified and valid, and the event is stamped on the Bitcoin blockchain (via ${anchor}).`; + } else if (allValid && otsStampState.valid === 'pending') { + summaryCard.classList.add('pq-result-pending'); + summaryCard.textContent = 'All signatures are verified and valid. The OpenTimestamps proof is pending — not yet stamped on the Bitcoin blockchain. You can upgrade it below.'; + } else if (allValid) { + summaryCard.classList.add('pq-result-invalid'); + summaryCard.textContent = 'All signatures are verified and valid, but no Bitcoin timestamp attestation was found.'; + } else { + summaryCard.classList.add('pq-result-invalid'); + summaryCard.textContent = 'Some checks failed verification. See the results above for details.'; + } } /* ================================================================ @@ -174,9 +269,13 @@ resultsWrap.style.display = 'none'; eventJsonWrap.style.display = 'none'; otsUpgradeWrap.style.display = 'none'; - otsBadge.innerHTML = ''; + hideOtsCard('stamp'); + hideOtsCard('upgraded'); + summaryWrap.style.display = 'none'; + otsStampState = { valid: null, label: '', detail: '' }; + otsUpgradedState = { valid: null, label: '', detail: '' }; - // Display full proof carrier JSON + // Display full proof carrier JSON at the top of the page eventJsonEl.textContent = JSON.stringify(event, null, 2); eventJsonWrap.style.display = 'block'; @@ -237,9 +336,10 @@ } // 3. Inspect OTS proof tag and verify it matches the sha256 tag. - // First do a quick structural check for immediate UI feedback, then - // run full cryptographic verification (async — fetches Bitcoin block - // headers and validates the Merkle path). + // The OTS result is rendered as a dedicated card (pqOtsStampCard) + // alongside the signature results. If the proof is pending, an + // upgrade card (pqOtsUpgradedCard) is reserved for a later upgraded + // proof. A final summary card is rendered at the end. const otsTag = event.tags.find(t => t[0] === 'ots'); const sha256Tag = event.tags.find(t => t[0] === 'sha256'); if (otsTag && otsTag[1]) { @@ -254,17 +354,27 @@ : `WARNING: sha256 tag (${sha256Tag[1].substring(0, 16)}...) does not match computed hash (${fullHash.substring(0, 16)}...)`) : 'No sha256 tag found'; - // Quick structural display first + otsUpgradeWrap.style.display = 'block'; + if (!validFile) { - setOtsBadge('pq-ots-badge-none', 'OTS: Invalid format'); - setOtsInfo(`OTS tag present but does not appear to be a valid detached .ots file (${currentOtsBytes.length} bytes). ${hashMatchNote}.`); - otsUpgradeWrap.style.display = 'none'; - } else if (hasBitcoinAttestation) { - setOtsBadge('pq-ots-badge-confirmed', 'OTS: Verifying...'); - setOtsInfo(`OpenTimestamps proof contains a Bitcoin attestation. Performing full cryptographic verification (fetching block header)... Proof size: ${currentOtsBytes.length} bytes. ${hashMatchNote}.`); - otsUpgradeWrap.style.display = 'block'; + // Invalid format — no upgrade possible + otsStampState = { valid: false, label: '', detail: '' }; + setOtsCard('stamp', 'invalid', 'OTS Stamp: Invalid format', + `OTS tag present but does not appear to be a valid detached .ots file (${currentOtsBytes.length} bytes). ${hashMatchNote}.`); + hideOtsCard('upgraded'); otsUpgradeBtn.style.display = 'none'; otsRepublishBtn.style.display = 'none'; + renderSummary(allValid); + } else if (hasBitcoinAttestation) { + // Contains a Bitcoin attestation — run full async verification. + // Show a "verifying" stamp card first, then update it. + otsStampState = { valid: null, label: '', detail: '' }; + setOtsCard('stamp', 'pending', 'OTS Stamp: Verifying...', + `OpenTimestamps proof contains a Bitcoin attestation. Performing full cryptographic verification (fetching block header)... Proof size: ${currentOtsBytes.length} bytes. ${hashMatchNote}.`); + hideOtsCard('upgraded'); + otsUpgradeBtn.style.display = 'none'; + otsRepublishBtn.style.display = 'none'; + renderSummary(allValid); // Run full async verification: parse the proof, bind the target // digest to the sha256 tag, walk the Merkle path, and check the @@ -278,36 +388,56 @@ : result.trustMode === 'single-explorer-checked' ? 'single-explorer-checked (trusted API)' : result.trustMode || 'unknown'; - setOtsBadge('pq-ots-badge-confirmed', 'OTS: Verified (Bitcoin)'); - setOtsInfo(`OpenTimestamps proof verified. Bitcoin block ${att.height} (mined ${date} UTC). Merkle root matches. Trust mode: ${trustLabel} — block header is trusted from explorer API(s), not independently verified against PoW. Proof commits to the event hash. Proof size: ${currentOtsBytes.length} bytes. ${hashMatchNote}.`); + otsStampState = { + valid: true, + label: `Bitcoin block ${att.height}`, + detail: `OpenTimestamps proof verified. Bitcoin block ${att.height} (mined ${date} UTC). Merkle root matches. Trust mode: ${trustLabel} — block header is trusted from explorer API(s), not independently verified against PoW. Proof commits to the event hash. Proof size: ${currentOtsBytes.length} bytes. ${hashMatchNote}.` + }; + setOtsCard('stamp', 'valid', 'OTS Stamp: Verified (Bitcoin)', otsStampState.detail); otsUpgradeBtn.style.display = 'none'; + renderSummary(allValid); } else { - setOtsBadge('pq-ots-badge-none', 'OTS: Verification failed'); const errDetail = result.errors.length > 0 ? ` Errors: ${result.errors.join('; ')}` : ''; - setOtsInfo(`OpenTimestamps proof could NOT be cryptographically verified. ${hashMatchNote}.${errDetail}`); + otsStampState = { valid: false, label: '', detail: '' }; + setOtsCard('stamp', 'invalid', 'OTS Stamp: Verification failed', + `OpenTimestamps proof could NOT be cryptographically verified. ${hashMatchNote}.${errDetail}`); otsUpgradeBtn.style.display = 'inline-block'; + renderSummary(allValid); } }).catch(err => { - setOtsBadge('pq-ots-badge-none', 'OTS: Verification error'); - setOtsInfo(`OpenTimestamps verification error: ${err.message}. ${hashMatchNote}.`); + otsStampState = { valid: false, label: '', detail: '' }; + setOtsCard('stamp', 'invalid', 'OTS Stamp: Verification error', + `OpenTimestamps verification error: ${err.message}. ${hashMatchNote}.`); otsUpgradeBtn.style.display = 'inline-block'; + renderSummary(allValid); }); } else { - setOtsBadge('pq-ots-badge-pending', 'OTS: Pending'); - setOtsInfo(`OpenTimestamps proof is pending (no Bitcoin attestation yet). Proof size: ${currentOtsBytes.length} bytes. ${hashMatchNote}. You can try upgrading it below.`); - otsUpgradeWrap.style.display = 'block'; + // Pending — no Bitcoin attestation yet. Reserve the upgraded card + // for when the user upgrades the proof below. + otsStampState = { valid: 'pending', label: '', detail: '' }; + setOtsCard('stamp', 'pending', 'OTS Stamp: Pending', + `OpenTimestamps proof is pending (no Bitcoin attestation yet). Proof size: ${currentOtsBytes.length} bytes. ${hashMatchNote}. You can try upgrading it below.`); + hideOtsCard('upgraded'); otsUpgradeBtn.style.display = 'inline-block'; otsRepublishBtn.style.display = 'none'; + renderSummary(allValid); } } catch (e) { - setOtsBadge('pq-ots-badge-none', 'OTS: Error'); - setOtsInfo(`Failed to parse OTS proof: ${e.message}`); - otsUpgradeWrap.style.display = 'none'; + otsStampState = { valid: false, label: '', detail: '' }; + setOtsCard('stamp', 'invalid', 'OTS Stamp: Error', + `Failed to parse OTS proof: ${e.message}`); + hideOtsCard('upgraded'); + otsUpgradeBtn.style.display = 'none'; + otsRepublishBtn.style.display = 'none'; + renderSummary(allValid); } } else { - setOtsBadge('pq-ots-badge-none', 'OTS: None'); - setOtsInfo('No OpenTimestamps proof tag found in this event.'); + otsStampState = { valid: false, label: '', detail: '' }; + setOtsCard('stamp', 'invalid', 'OTS Stamp: None', + 'No OpenTimestamps proof tag found in this event.'); + hideOtsCard('upgraded'); otsUpgradeWrap.style.display = 'none'; + renderSummary(allValid); } resultsWrap.style.display = 'block'; @@ -322,6 +452,7 @@ queryBtn.addEventListener('click', async () => { queryBtn.disabled = true; + setButtonSpinner(queryBtn); setStatus(queryStatus, 'info', 'Querying relays...'); const pubkeyInput = document.getElementById('pqPubkeyInput').value.trim(); @@ -331,6 +462,7 @@ if (!pubkeyHex) { setStatus(queryStatus, 'error', 'Invalid pubkey. Enter a 64-char hex pubkey or an npub.'); queryBtn.disabled = false; + clearButtonSpinner(queryBtn); return; } @@ -338,6 +470,7 @@ if (relayUrls.length === 0) { setStatus(queryStatus, 'error', 'Enter at least one relay URL.'); queryBtn.disabled = false; + clearButtonSpinner(queryBtn); return; } @@ -373,6 +506,7 @@ if (allCandidates.length === 0) { setStatus(queryStatus, 'error', `No proof carrier events found for this pubkey on any of the ${relayUrls.length} relay(s)${lastError ? ' (last error: ' + lastError + ')' : ''}.`); queryBtn.disabled = false; + clearButtonSpinner(queryBtn); return; } @@ -397,6 +531,7 @@ if (!kind1Event) { setStatus(queryStatus, 'error', 'Could not parse kind 1 announcement from any candidate.'); queryBtn.disabled = false; + clearButtonSpinner(queryBtn); return; } @@ -427,6 +562,7 @@ } queryBtn.disabled = false; + clearButtonSpinner(queryBtn); }); /* ================================================================ @@ -523,10 +659,20 @@ setStatus(otsUpgradeStatus, 'success', `Bitcoin attestation verified! Block ${att ? att.height : '?'} (mined ${date} UTC). Trust mode: ${trustLabel}. Proof upgraded (${currentOtsBytes.length} bytes). You can publish a new kind 9999 event carrying the confirmed proof below; it will reference the original via an upgrade_of tag.`); otsUpgradeBtn.style.display = 'none'; otsRepublishBtn.style.display = 'inline-block'; - setOtsBadge('pq-ots-badge-confirmed', 'OTS: Verified (Bitcoin)'); - setOtsInfo(`OpenTimestamps proof verified. Bitcoin block ${att ? att.height : '?'}. Trust mode: ${trustLabel}. Proof size: ${currentOtsBytes.length} bytes.`); + // Render the upgraded proof as a second OTS card and refresh summary. + otsUpgradedState = { + valid: true, + label: `Bitcoin block ${att ? att.height : '?'}`, + detail: `OpenTimestamps proof verified. Bitcoin block ${att ? att.height : '?'} (mined ${date} UTC). Trust mode: ${trustLabel}. Proof size: ${currentOtsBytes.length} bytes.` + }; + setOtsCard('upgraded', 'valid', 'OTS Upgraded: Verified (Bitcoin)', otsUpgradedState.detail); + renderSummary(true); } else { setStatus(otsUpgradeStatus, 'info', `Proof upgraded but still pending (no Bitcoin attestation yet). ${result.detail ? 'Detail: ' + result.detail : ''} Try again later.`); + otsUpgradedState = { valid: 'pending', label: '', detail: '' }; + setOtsCard('upgraded', 'pending', 'OTS Upgraded: Still pending', + `Proof upgraded but still pending (no Bitcoin attestation yet). ${result.detail ? 'Detail: ' + result.detail : ''} Try again later.`); + renderSummary(true); } } catch (error) { setStatus(otsUpgradeStatus, 'error', `Upgrade failed: ${error.message}`); diff --git a/www/js/version.json b/www/js/version.json index 89fa99f..818d18d 100644 --- a/www/js/version.json +++ b/www/js/version.json @@ -1,5 +1,5 @@ { - "VERSION": "v0.1.0", - "VERSION_NUMBER": "0.1.0", - "BUILD_DATE": "2026-07-28T10:01:19.383Z" + "VERSION": "v0.1.1", + "VERSION_NUMBER": "0.1.1", + "BUILD_DATE": "2026-07-29T13:00:25.910Z" } diff --git a/www/verify.html b/www/verify.html index 0e88290..f6a2fa2 100644 --- a/www/verify.html +++ b/www/verify.html @@ -69,6 +69,22 @@ .pq-button:hover { opacity: 0.7; } .pq-button:disabled { opacity: 0.3; cursor: not-allowed; } + /* Braille spinner shown inside a button while its action is running. + Frames are advanced by JS (setButtonSpinner); this class just styles + the inline spinner glyph. */ + .pq-btn-spinner { + display: inline-block; + color: var(--accent-color); + font-size: 16px; + line-height: 1; + margin-right: 6px; + vertical-align: middle; + } + + /* Keep the spinner fully visible even while the button is dimmed + (disabled) during a running action. */ + .pq-button:disabled .pq-btn-spinner { opacity: 1; } + .pq-button-row { display: flex; gap: 10px; @@ -164,6 +180,31 @@ border-left: 4px solid #cc0000; } + .pq-result-pending { + border-left: 4px solid #f0ad4e; + } + + /* Multi-line OTS / summary cards wrap their detail text. */ + .pq-result-item.pq-result-detail { + display: block; + line-height: 1.6; + white-space: normal; + } + .pq-result-detail .pq-result-head { + font-weight: bold; + margin-bottom: 4px; + } + .pq-result-detail .pq-result-body { + font-size: 13px; + font-weight: normal; + } + + .pq-summary-card { + font-size: 16px; + font-weight: bold; + padding: 15px 18px; + } + .pq-event-preview { background: var(--secondary-color); color: var(--primary-color); @@ -211,30 +252,6 @@ .pq-tab-panel { display: none; } .pq-tab-panel.pq-tab-panel-active { display: block; } - - .pq-ots-badge { - display: inline-block; - padding: 3px 10px; - border-radius: var(--border-radius); - font-size: 12px; - font-weight: bold; - margin-left: 8px; - } - - .pq-ots-badge-pending { - background: #f0ad4e; - color: #fff; - } - - .pq-ots-badge-confirmed { - background: #00aa00; - color: #fff; - } - - .pq-ots-badge-none { - background: var(--muted-color); - color: var(--secondary-color); - } @@ -314,24 +331,24 @@
- - - + - -