fix(potidaea): welcome overlay hides properly + compact classified errors

Two bugs one commit:

1) Welcome overlay left the swap layout at the bottom of the page
   .welcome-overlay { display: flex; min-height: calc(100vh - 140px) }
   is an author rule (specificity 0,1,0) that outranks the user-agent
   [hidden] { display: none } (same specificity, but user-agent). So
   overlay.hidden = true had no visual effect: the empty container kept
   eating a full viewport and shoved .od-layout below the fold. Reported
   by user with a screenshot showing panels at the very bottom after
   wallet import.
   Fix: explicit .welcome-overlay[hidden] { display: none !important; }
   in the author sheet (specificity 0,2,0 wins). Verified live: overlay
   collapses to display:none / height 0 after import; .od-layout sits
   at y=181 (just under the topbar) as expected.

2) Swap-sheet error was a wall of text with no way to grab it
   setSheetError previously dumped the raw message into a single div
   with textContent. Broadcast / covenant errors can be multi-KB dumps
   (tx hex, cashscript stack, Bitauth trace) and blew up the review
   sheet vertically. Reported by user attempting MBA→BCH redeem.
   Fix:
   - classifySwapError maps common patterns to short code labels:
     E-INSUFFICIENT / E-DUST / E-CHANGE-DUST / E-COVENANT-STATE /
     E-STALE-UTXO / E-FEE-LOW / E-NET / E-COVENANT / E-LEFTOVER-DUST /
     E-REDEEM-SMALL / E-OTHER
   - Compact display: mark + code chip + one-line headline + copy
     button + expand toggle
   - Full text lives in a collapsible block (max-height 240px, scroll)
     that stays hidden until the user clicks the details arrow
   - Copy button writes "[E-CODE] <full text>" to clipboard so a
     support paste starts with the error code

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Local Dev 2026-09-22 08:33:34 +02:00
parent acdfceaf40
commit 440416f522
2 changed files with 118 additions and 2 deletions

View file

@ -731,6 +731,44 @@
.ma-ico { width: 32px; height: 32px; }
}
/* ============ Swap-sheet error chip ============ */
/* Compact error surface on the review sheet: red mark + short code chip +
* one-line headline + copy / details toggle. Full text lives in a
* collapsible block below so raw broadcast/covenant dumps don't blow up
* the sheet vertically. */
.wc-sheet-err {
font-family: var(--font-mono); font-size: 11px;
color: #ff7a8a;
background: rgba(255,122,138,.06);
border: 1px solid rgba(255,122,138,.28);
border-radius: 10px;
padding: 10px 12px; margin: 10px 0;
line-height: 1.55;
}
.wc-sheet-err .wc-err-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.wc-sheet-err .wc-err-mark { color: #ff7a8a; font-size: 13px; }
.wc-sheet-err .wc-err-code {
font-family: var(--font-mono); font-size: 9px; letter-spacing: .08em;
padding: 2px 6px; border-radius: 4px;
color: #ff7a8a; border: 1px solid rgba(255,122,138,.32);
background: rgba(255,122,138,.08);
white-space: nowrap; flex: none;
}
.wc-sheet-err .wc-err-head { flex: 1; min-width: 0; color: var(--ink-bright); font-size: 12px; }
.wc-sheet-err .wc-err-btn {
background: transparent; border: 1px solid rgba(255,122,138,.22);
color: rgba(255,122,138,.7); border-radius: 5px;
padding: 2px 6px; font-family: var(--font-mono); font-size: 11px; line-height: 1;
cursor: pointer; transition: color .15s, border-color .15s;
flex: none;
}
.wc-sheet-err .wc-err-btn:hover { color: #ff7a8a; border-color: rgba(255,122,138,.5); }
.wc-sheet-err .wc-err-full {
margin-top: 8px; padding-top: 8px; border-top: 1px dashed rgba(255,122,138,.22);
font-family: var(--font-mono); font-size: 10.5px; color: var(--ink-soft);
max-height: 240px; overflow: auto; word-break: break-word; white-space: pre-wrap;
}
/* ============ Tx-receipt mini-buttons (copy / view on explorer) ============ */
/* Small square action buttons that sit next to a rendered txid across
* the swap done sheet, wizard genesis done, and publish overlay. */
@ -752,6 +790,12 @@
* card, not a half-empty grid behind it. */
#v-swap.v-swap-welcome-mode .od-layout { display: none; }
.welcome-overlay { display: flex; align-items: center; justify-content: center; min-height: calc(100vh - 140px); padding: 40px 20px; }
/* Explicit [hidden] override — an author `display: flex` rule wins over
* the user-agent stylesheet's `[hidden] { display: none }`, so we have
* to spell it out or the container keeps eating a full viewport of
* vertical space (and shoves the real swap layout to the bottom of the
* page) even when overlay.hidden = true. */
.welcome-overlay[hidden] { display: none !important; }
.welcome-card {
max-width: 640px; width: 100%;
background: var(--surface-1); border: 1px solid var(--border);

View file

@ -788,15 +788,87 @@ function applyOptimisticSwapDelta(kind: 'claim' | 'redeem', amountUnits: bigint,
try { paintOdAll(); } catch { /* noop */ }
}
/** Classify a raw error message into a short code + one-line headline the
* user can scan without reading a wall of text. Broadcast errors from
* Bitauth / cashscript often dump the failing tx as a multi-KB hex blob;
* the review sheet is not the place for that. Full text stays in the
* details section for copy-and-paste debugging. */
function classifySwapError(raw: string): { code: string; headline: string; full: string } {
const full = raw.trim();
// Pattern → { code, headline extractor }
const patterns: Array<{ re: RegExp; code: string; head?: (m: RegExpMatchArray) => string }> = [
{ re: /^insufficient\s+([a-z0-9]+):.*need\s+(\d+).*have\s+(\d+)/i, code: 'E-INSUFFICIENT',
head: (m) => `Not enough ${m[1].toUpperCase()} — need ${m[2]}, have ${m[3]}` },
{ re: /amount too small|below dust|feeSats\s*>=?/i, code: 'E-DUST',
head: () => 'Amount below the covenant’s dust floor' },
{ re: /change below dust/i, code: 'E-CHANGE-DUST',
head: () => 'BCH change would fall below the dust threshold' },
{ re: /covenant may be draining|no state utxo/i, code: 'E-COVENANT-STATE',
head: () => 'Registry covenant state UTXO missing' },
{ re: /64: bad-txns-inputs-missingorspent|missing inputs/i, code: 'E-STALE-UTXO',
head: () => 'Chosen UTXOs already spent — endpoint index is stale' },
{ re: /min relay fee|not enough fee/i, code: 'E-FEE-LOW',
head: () => 'Miner fee too low to relay' },
{ re: /timeout|connect failed|not connected|electrum:/i, code: 'E-NET',
head: () => 'Electrum endpoint unreachable' },
{ re: /require\s+statement\s+failed|failed\s+at\s+input/i, code: 'E-COVENANT',
head: (m) => {
const m2 = full.match(/DeviantOpenMintFT\.cash:(\d+)/i);
return m2 ? `Covenant require() failed at line ${m2[1]}` : 'Covenant require() failed';
} },
{ re: /leftover dust padding/i, code: 'E-LEFTOVER-DUST',
head: () => 'Leftover after redeem would fall below dust' },
{ re: /^redeem too small/i, code: 'E-REDEEM-SMALL',
head: () => 'Redeem amount too small to cover miner fee' },
];
for (const p of patterns) {
const m = full.match(p.re);
if (m) return { code: p.code, headline: p.head ? p.head(m) : m[0], full };
}
// Fallback — take the first sentence-ish chunk (up to first period, dash,
// or 90 chars) as the headline; everything else lives in details.
const first = full.split(/[.\n]|—/, 1)[0].trim().slice(0, 90);
return { code: 'E-OTHER', headline: first || 'Swap failed', full };
}
function setSheetError(msg: string): void {
let el = document.getElementById('wc-sheet-err') as HTMLElement | null;
if (!el) {
el = document.createElement('div'); el.id = 'wc-sheet-err';
el.style.cssText = 'font-family:var(--font-mono);font-size:11px;color:#ff7a8a;background:rgba(255,122,138,.08);border:1px solid rgba(255,122,138,.28);border-radius:8px;padding:8px 10px;margin:10px 0;';
const rev = document.getElementById('reviewSheet');
rev?.insertBefore(el, rev.firstChild);
}
el.textContent = '✗ ' + msg;
el.className = 'wc-sheet-err';
const { code, headline, full } = classifySwapError(msg);
const detailsCollapsed = full.length > 120;
el.innerHTML =
'<div class="wc-err-row">' +
'<span class="wc-err-mark">✗</span>' +
'<span class="wc-err-code" title="Error reference">' + escHtml(code) + '</span>' +
'<span class="wc-err-head">' + escHtml(headline) + '</span>' +
'<button class="wc-err-btn" data-err-copy="1" title="Copy full error text">⧉</button>' +
(detailsCollapsed
? '<button class="wc-err-btn" data-err-toggle="1" title="Show / hide full text">⌄</button>'
: '') +
'</div>' +
(detailsCollapsed
? '<div class="wc-err-full" hidden>' + escHtml(full) + '</div>'
: (full !== headline ? '<div class="wc-err-full">' + escHtml(full) + '</div>' : ''));
el.querySelector<HTMLButtonElement>('[data-err-copy]')?.addEventListener('click', async (ev) => {
ev.stopPropagation();
try {
await navigator.clipboard.writeText('[' + code + '] ' + full);
const btn = ev.currentTarget as HTMLButtonElement;
const orig = btn.textContent;
btn.textContent = '✓';
setTimeout(() => { btn.textContent = orig; }, 1000);
} catch { /* clipboard blocked — silent */ }
});
el.querySelector<HTMLButtonElement>('[data-err-toggle]')?.addEventListener('click', (ev) => {
ev.stopPropagation();
const details = el!.querySelector<HTMLElement>('.wc-err-full');
if (details) details.hidden = !details.hidden;
});
}
function wireSwapConfirmInterceptor(): void {