sirius/portal.html
Local Dev 016095a604 sirius.x: portal (sign-in + names), Theseus subpage, drop .com public mention
* portal.html — sign in with a seed phrase (in-memory only, wiped on sign
    out), fetch owned NFT categories across all watched addresses, cross-join
    against BCNR buildIndex to show the wallet's names + records. Read-only
    for now; record edit (UPD) will piggyback on the register bundle's signing
    path. WizardConnect button stubbed with a "soon" pill — the adapter
    already exists in register.html and will be wired in the next pass.
  * theseus/index.html — download page for Theseus Navigator (v0.0.4) and
    Ariadne Resolver (Windows setup + Android APK). Same design system as the
    landing. Links to dl.silentmode.st for the actual binaries and to
    silentmode.st/releases for the manifest.
  * index.html — nav now has 🧭 Theseus and 🔑 Portal chips; "Try it" section
    adds cards for the portal and Theseus.
  * site/tlds/ — .com row removed from the public table; count updated to 14;
    supersedes the "held by operator" language per operator direction.
  * site-sirius-x/index.html — softened ICANN-adjacent note.

All routes verified live: silentmode.st/{tlds,sirius-x,sirius-x/portal.html,
sirius-x/theseus,bns/sirius.x} return 200. Sia mirror updated for all three
new/changed sirius.x pages.
2026-08-29 21:57:33 +02:00

264 lines
12 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Portal — your names on sirius.x</title>
<meta name="description" content="Sign in with your Bitcoin Cash seed phrase to see every BCNR name your wallet holds and manage records. Your keys never leave the browser.">
<style>
:root{
--bg:#0b0e14; --panel:#141a24; --panel2:#18202c; --line:rgba(255,255,255,.09);
--ink:#e7eaf1; --mut:#8b98a9; --dim:#5e6678; --acid:#d6ff3d;
--ok:#4fd1a5; --taken:#f6768a; --warn:#ffc75f;
}
*{box-sizing:border-box}
body{margin:0;background:radial-gradient(1100px 560px at 50% -12%,#16202e,var(--bg));
color:var(--ink);font:16px/1.65 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;min-height:100vh}
a{color:var(--acid)}
.wrap{max-width:820px;margin:0 auto;padding:0 1.2rem}
.topnav{position:sticky;top:0;z-index:20;display:flex;gap:2px;align-items:center;flex-wrap:wrap;
padding:10px 1.2rem;background:rgba(11,14,20,.82);backdrop-filter:blur(10px);border-bottom:1px solid var(--line)}
.topnav .brand{font-weight:600;color:var(--ink);text-decoration:none;margin-right:14px;font-size:15px;letter-spacing:.2px}
.topnav .brand .g{color:var(--acid)}
.topnav a{color:var(--mut);text-decoration:none;padding:6px 12px;border-radius:8px;font-size:14px}
.topnav a:hover{color:var(--ink);background:var(--panel)}
.topnav a.here{color:var(--acid);background:rgba(214,255,61,.08)}
header.hero{text-align:center;padding:3rem 1.2rem 1rem}
.mark{font-size:44px;line-height:1}
h1{font-size:clamp(1.6rem,4.5vw,2.2rem);margin:.3rem 0 .3rem}
h1 .g{color:var(--acid)}
.tag{color:var(--mut);font-size:1rem;max-width:580px;margin:0 auto}
h2{font-size:1.25rem;margin:0 0 .4rem}
section{padding:2rem 0;border-top:1px solid var(--line);margin-top:2rem}
section:first-of-type{border-top:none;margin-top:1rem}
.card{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:20px 22px;margin-top:1rem}
label{display:block;font-size:13px;color:var(--mut);margin:14px 0 5px}
textarea,input[type=password],input[type=text]{
width:100%;padding:11px 13px;border-radius:10px;border:1px solid #ffffff22;background:#0e131b;
color:var(--ink);font-size:14px;font-family:inherit;outline:none
}
textarea{min-height:74px;resize:vertical;font-family:ui-monospace,monospace;font-size:13px}
textarea:focus,input:focus{border-color:#4b7bec}
.btn{display:inline-block;text-decoration:none;padding:10px 18px;border-radius:10px;background:#4b7bec;color:#fff;font-size:14px;border:none;cursor:pointer;font-family:inherit}
.btn.ghost{background:transparent;border:1px solid var(--line);color:var(--ink)}
.btn.acid{background:var(--acid);color:#0b0e14;font-weight:600}
.btn.small{padding:6px 12px;font-size:12.5px}
.btn:hover{filter:brightness(1.12)}
.btn:disabled{opacity:.45;cursor:not-allowed;filter:none}
.row{display:flex;gap:10px;flex-wrap:wrap;margin-top:16px;align-items:center}
.muted{color:var(--mut);font-size:13px}
.dim{color:var(--dim);font-size:12.5px}
code,.mono{background:#0e131b;border:1px solid var(--line);border-radius:6px;padding:1px 6px;font-family:ui-monospace,monospace;font-size:13px;word-break:break-all}
.badge{display:inline-block;font-size:11px;padding:2px 10px;border-radius:999px;font-weight:600}
.b-x{background:rgba(214,255,61,.14);color:var(--acid)}
.b-warn{background:rgba(255,199,95,.14);color:var(--warn)}
.err{color:var(--taken);font-size:13.5px;margin-top:10px}
.status{background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:10px 14px;font-size:13px;color:var(--mut);margin-top:12px}
.status.ok{border-left:3px solid var(--ok)}
.status.err{border-left:3px solid var(--taken);color:var(--taken)}
.hidden{display:none}
.authbox{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:14px}
.authbox .card{margin:0}
.authbox h3{margin:0 0 .3rem;font-size:1rem}
.authbox p{margin:0;color:var(--mut);font-size:13px}
.authbox .card > .cta{margin-top:14px}
.name-row{display:grid;grid-template-columns:1fr auto auto;gap:12px;align-items:center;
padding:14px 16px;background:var(--panel2);border:1px solid var(--line);border-radius:12px;margin-top:8px}
.name-row .n{font-family:ui-monospace,monospace;font-size:15px}
.name-row .rec{color:var(--mut);font-family:ui-monospace,monospace;font-size:12px;grid-column:1/-1;margin-top:6px;word-break:break-all}
.name-row .rec .k{color:var(--dim)}
footer{border-top:1px solid var(--line);padding:2rem 0 3rem;color:var(--dim);font-size:13px;text-align:center;margin-top:2.5rem}
@media (max-width:520px){
.name-row{grid-template-columns:1fr auto}
}
</style>
</head>
<body>
<nav class="topnav">
<a class="brand" href="./"><span class="g">sirius.x</span></a>
<a href="./#try">Try it</a>
<a href="./#docs">Docs</a>
<a href="./theseus/">🧭 Theseus</a>
<a href="./portal.html" class="here">🔑 Portal</a>
<a href="https://silentmode.st/" rel="noopener">Silent Mode ↗</a>
</nav>
<header class="hero">
<div class="mark">🔑</div>
<h1>Your <span class="g">names</span> portal</h1>
<p class="tag">Sign in with your Bitcoin Cash seed phrase or connect an external wallet to see
every BCNR name your wallet holds. Your keys never leave this browser tab.</p>
</header>
<div class="wrap">
<section id="signin">
<h2>Sign in</h2>
<p class="muted">Pick one. Nothing is transmitted — resolution is a read-only chain query,
and signing (for record edits) happens locally in the browser or in your external wallet.</p>
<div class="authbox" style="margin-top:14px">
<div class="card">
<h3>Import a seed phrase</h3>
<p>Paste your 12- or 24-word BIP-39 recovery phrase. It stays in memory only, wiped when you
close the tab or sign out.</p>
<label for="seed">Recovery phrase</label>
<textarea id="seed" placeholder="word1 word2 … word12" spellcheck="false" autocomplete="off"></textarea>
<div class="row">
<button class="btn acid" id="signin-btn">Sign in →</button>
<span class="dim">Chipnet only for now.</span>
</div>
<div id="signin-err" class="err hidden"></div>
</div>
<div class="card">
<h3>Connect an external wallet</h3>
<p>Use Cashonize 0.9+ or Paytaca via WizardConnect. The wallet signs record edits — the seed
never touches this page.</p>
<div class="row">
<button class="btn ghost" id="wc-btn" disabled>WizardConnect
<span class="badge b-warn" style="margin-left:6px">soon</span></button>
</div>
<p class="dim" style="margin-top:8px">Portal-side WizardConnect wiring is in progress; the
register.html flow already ships the WizardConnect adapter, and the portal will reuse it.</p>
</div>
</div>
<div class="status" style="margin-top:14px">
Don't have a wallet? Create one during
<a href="https://silentmode.st/sirius/register.html">name registration</a> — the recovery phrase
is generated in your browser and encrypted with your password.
</div>
</section>
<section id="me" class="hidden">
<h2>Signed in</h2>
<div class="card">
<label>Receive address</label>
<div><code id="me-addr"></code></div>
<label style="margin-top:14px">Token address (holds your name certificates)</label>
<div><code id="me-taddr"></code></div>
<div class="row">
<button class="btn ghost small" id="signout-btn">Sign out</button>
<span class="dim" id="me-info"></span>
</div>
</div>
<h2 style="margin-top:2rem">Names you hold</h2>
<div class="status" id="names-status">Loading names from the chain…</div>
<div id="names-list"></div>
</section>
</div>
<footer>
Your keys, your names, your browser. ·
<a href="./">back to sirius.x</a> ·
<a href="https://silentmode.st/sirius/register.html">register a new name</a>
</footer>
<script type="importmap">
{ "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js" } }
</script>
<script type="module">
// The registrar bundle exports everything we need: BuiltInWallet for local
// wallet unlock, resolveName/buildIndex for chain queries, getUtxosForAddresses
// for holdings. This portal is a read-first surface; record edits (UPD) will
// come next and will piggyback on the same signing path the registrar uses.
import * as BNS from "https://silentmode.st/js/bns-register.js";
const $ = (id) => document.getElementById(id);
const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c]));
let wallet = null;
// ---------- sign in ----------
$("signin-btn").addEventListener("click", async () => {
const btn = $("signin-btn");
const errBox = $("signin-err");
errBox.classList.add("hidden");
const raw = $("seed").value.trim();
if (!raw) { errBox.textContent = "Enter your seed phrase to sign in."; errBox.classList.remove("hidden"); return; }
btn.disabled = true; btn.textContent = "Unlocking…";
try {
wallet = await BNS.BuiltInWallet.fromMnemonic(raw);
} catch (e) {
errBox.textContent = "Could not unlock: " + (e.message || e);
errBox.classList.remove("hidden");
btn.disabled = false; btn.textContent = "Sign in →";
return;
}
// Wipe the textarea immediately — the seed is now in the wallet object only.
$("seed").value = "";
btn.disabled = false; btn.textContent = "Sign in →";
await enterPortal();
});
$("signout-btn").addEventListener("click", () => {
wallet = null;
$("me").classList.add("hidden");
$("signin").classList.remove("hidden");
$("names-list").innerHTML = "";
$("names-status").textContent = "Loading names from the chain…";
});
async function enterPortal() {
$("signin").classList.add("hidden");
$("me").classList.remove("hidden");
$("me-addr").textContent = wallet.address;
$("me-taddr").textContent = wallet.tokenAddress;
$("me-info").textContent = "watched addresses: " + wallet.watchedAddresses.length;
await loadNames();
}
// ---------- load names ----------
// Approach: build the full BCNR index once (small on chipnet), then filter to
// names whose category matches one of the token UTXOs the wallet holds across
// its watched addresses. This mirrors what portfolio.js does server-side.
async function loadNames() {
const status = $("names-status");
const list = $("names-list");
list.innerHTML = "";
status.className = "status";
status.textContent = "Loading names from the chain…";
try {
// Fetch the wallet's token UTXOs across all watched addresses.
const addresses = wallet.watchedAddresses;
const utxos = await BNS.getUtxosForAddresses(addresses);
const ownedCategories = new Set(
utxos.filter((u) => u.token && u.token.nft).map((u) => u.token.category || u.token.tokenId)
);
status.textContent = `Found ${ownedCategories.size} NFT(s) across ${addresses.length} addresses. Cross-checking against the name registry…`;
// Walk the BCNR index and keep entries whose category the wallet holds.
const index = await BNS.buildIndex({ WebSocket });
const mine = [];
for (const entry of index.values()) if (ownedCategories.has(entry.category)) mine.push(entry);
if (!mine.length) {
status.className = "status";
status.textContent = `No BCNR names found for this wallet. If you just registered, wait a minute for confirmation and reload.`;
return;
}
status.className = "status ok";
status.textContent = `You own ${mine.length} name${mine.length === 1 ? "" : "s"}.`;
for (const e of mine.sort((a, b) => a.name.localeCompare(b.name))) {
const div = document.createElement("div");
div.className = "name-row";
const records = e.records && Object.keys(e.records).length
? Object.entries(e.records).map(([k, v]) => `<span class="k">${esc(k)}=</span>${esc(String(v).slice(0, 80))}`).join(" &nbsp;·&nbsp; ")
: `<span class="k">(no records set)</span>`;
div.innerHTML =
`<div class="n">${esc(e.name)}</div>` +
`<span class="badge b-x">on chain</span>` +
`<a class="btn small ghost" href="https://silentmode.st/bns/${encodeURIComponent(e.name)}/" target="_blank" rel="noopener">view →</a>` +
`<div class="rec">${records}</div>`;
list.appendChild(div);
}
} catch (e) {
status.className = "status err";
status.textContent = "Could not load names from the chain: " + (e.message || e);
}
}
</script>
</body>
</html>