astrnvk's picture
витрина: раздел кода — реальная покупка двух редакций бота
73ecd8a verified
Raw
History Blame Contribute Delete
28.2 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Order status — Polymarket microstructure data</title>
<style>
/* Same tokens and components as index.html, duplicated so this page stands
alone. Keep any visual change in sync with the main page. */
:root {
/* Same tokens as index.html so the buyer does not land on a different
product after paying. Names are kept from the previous scheme, only the
values move, which is why nothing further down this file had to change. */
--paper: #f7f6f3;
--paper-sunk: #edebe6;
--ink: #14161a;
--ink-2: #6a6e77;
--ink-3: #8a8f96;
--rule: #dcd8d1;
--rule-strong: #b9b4ab;
--bid: #0a6f63;
--ask: #a33f34;
--flag: #94670f;
/* The storefront is set entirely in monospace; --serif now resolves to the
same stack so this page speaks with one voice instead of two. */
--serif: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
"Liberation Mono", "Courier New", monospace;
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
--measure: 64ch;
--page: 960px;
}
@media (prefers-color-scheme: dark) {
:root {
--paper: #0e0f12;
--paper-sunk: #16181d;
--ink: #e8e6e1;
--ink-2: #888d96;
--ink-3: #6d727a;
--rule: #262a31;
--rule-strong: #3c414a;
--bid: #3ad4bd;
--ask: #d98276;
--flag: #d9a94e;
}
}
:root[data-theme="dark"] {
--paper:#0e0f12; --paper-sunk:#16181d; --ink:#e8e6e1; --ink-2:#888d96;
--ink-3:#6d727a; --rule:#262a31; --rule-strong:#3c414a;
--bid:#3ad4bd; --ask:#d98276; --flag:#d9a94e;
}
:root[data-theme="light"] {
--paper:#f7f6f3; --paper-sunk:#edebe6; --ink:#14161a; --ink-2:#6a6e77;
--ink-3:#8a8f96; --rule:#dcd8d1; --rule-strong:#b9b4ab;
--bid:#0a6f63; --ask:#a33f34; --flag:#94670f;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--paper);
color: var(--ink);
font-family: var(--serif);
font-size: 17px;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
.page { max-width: var(--page); margin: 0 auto; padding: 0 28px; }
p { max-width: var(--measure); margin: 0 0 1.1em; }
p:last-child { margin-bottom: 0; }
a { color: inherit; text-decoration-thickness: 1px; text-underline-offset: 2px; }
.label {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.13em;
text-transform: uppercase;
color: var(--ink-3);
}
.masthead {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 20px;
flex-wrap: wrap;
padding: 22px 0 20px;
border-bottom: 1px solid var(--rule-strong);
}
.masthead .mark {
font-family: var(--mono);
font-size: 12px;
letter-spacing: 0.16em;
text-transform: uppercase;
}
.masthead nav a { text-decoration: none; }
.masthead nav a:hover { text-decoration: underline; }
.opener { padding: 76px 0 60px; }
h1 {
font-family: var(--serif);
font-weight: 400;
font-size: clamp(30px, 4.6vw, 44px);
line-height: 1.12;
letter-spacing: -0.015em;
text-wrap: balance;
margin: 0 0 20px;
max-width: 20ch;
}
h2 {
font-family: var(--serif);
font-weight: 400;
font-size: 24px;
line-height: 1.2;
letter-spacing: -0.01em;
margin: 40px 0 8px;
}
.standfirst {
font-size: 19px;
line-height: 1.5;
color: var(--ink-2);
max-width: 54ch;
}
code {
font-family: var(--mono);
font-size: 0.88em;
background: var(--paper-sunk);
padding: 1px 5px;
border-radius: 2px;
}
/* Status callout: same hairline device the main page uses for its notices.
Colour of the label is the only thing that changes between states. */
.notice {
margin-top: 40px;
padding-left: 18px;
border-left: 2px solid var(--rule-strong);
max-width: var(--measure);
}
.notice .label { display: block; margin-bottom: 7px; }
.notice p { font-size: 16px; color: var(--ink-2); }
.notice.ok { border-left-color: var(--bid); }
.notice.ok .label { color: var(--bid); }
.notice.warn { border-left-color: var(--flag); }
.notice.warn .label { color: var(--flag); }
.notice.err { border-left-color: var(--ask); }
.notice.err .label { color: var(--ask); }
.dim { color: var(--ink-2); font-size: 15px; }
/* -------------------------------------------------------------- file list */
.file-row {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px 20px;
padding: 14px 0;
border-bottom: 1px solid var(--rule);
}
.file-row:first-child { border-top: 1px solid var(--rule-strong); }
.file-row .path {
font-family: var(--mono);
font-size: 13px;
word-break: break-all;
}
.file-row .path .err {
display: block;
font-size: 12px;
color: var(--ask);
margin-top: 4px;
}
.dl-btn {
font-family: var(--mono);
font-size: 12.5px;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 9px 0 7px;
background: none;
border: 0;
border-bottom: 1px solid var(--ink);
color: var(--ink);
cursor: pointer;
white-space: nowrap;
flex-shrink: 0;
}
.dl-btn:hover:not(:disabled) { border-bottom-width: 2px; padding-bottom: 6px; }
.dl-btn:disabled { color: var(--ink-3); border-bottom-color: var(--rule); cursor: not-allowed; }
.fineprint {
margin-top: 22px;
font-size: 13px;
color: var(--ink-3);
max-width: var(--measure);
}
/* ------------------------------------------------------- bulk download */
.grab-all {
margin: 26px 0 30px;
padding: 20px 22px;
background: var(--paper-sunk);
border-left: 2px solid var(--ink);
}
.grab-all .label { display: block; margin-bottom: 8px; }
.grab-all p { font-size: 15px; color: var(--ink-2); max-width: var(--measure); }
.grab-all .fineprint { margin-top: 16px; }
.grab-all .dl-btn { font-size: 15px; }
/* Переключатель ОС над командой. Команды для unix и PowerShell не сводятся
к одной строке, а покупатель на Windows, получив bash, просто решит, что
выдача сломана — так и случилось на первой же реальной продаже. */
.os-tabs { display: flex; gap: 2px; margin-top: 8px; }
.os-tab {
padding: 6px 12px;
border: 1px solid var(--rule);
border-bottom: 0;
background: transparent;
color: var(--ink-3);
font: inherit;
font-size: 12.5px;
cursor: pointer;
}
.os-tab:hover { color: var(--ink-2); }
.os-tab[aria-selected="true"] {
background: var(--paper);
color: var(--ink);
border-color: var(--rule);
}
.os-tabs + .cmd { margin-top: 0; }
/* The command is the one thing here a buyer must copy verbatim, so it gets
its own scroll container: it must never widen the page on a phone. */
.cmd {
margin: 8px 0 0;
padding: 11px 13px;
overflow-x: auto;
background: var(--paper);
border: 1px solid var(--rule);
font-family: var(--mono);
font-size: 13px;
line-height: 1.5;
white-space: pre;
}
#files-details summary {
cursor: pointer;
padding: 10px 0;
color: var(--ink-2);
}
#files-details summary:hover { color: var(--ink); }
#files-details[open] summary { margin-bottom: 6px; }
footer { padding: 44px 0 72px; }
footer p { font-size: 14px; color: var(--ink-3); max-width: 62ch; }
:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) {
* { animation: none !important; transition: none !important; }
}
</style>
</head>
<body>
<div class="page">
<header class="masthead">
<div class="mark">Polymarket microstructure</div>
<nav class="label"><a href="index.html">&larr; Back to dataset</a></nav>
</header>
<div class="opener">
<h1 id="heading">Checking your order&hellip;</h1>
<p class="standfirst" id="subtext">Hang on while we confirm payment and prepare your download links.</p>
<div class="notice" id="status-panel">
<span class="label" id="status-label">Status</span>
<p id="status-text">Looking for your order in this browser&hellip;</p>
</div>
<div id="files-section" hidden>
<h2>Your files</h2>
<p class="dim" id="files-meta"></p>
<!-- Shown instead of the bulk-download block when the order is one of
the two bot editions: a single archive, and the next step is an
installer rather than a data pipeline. -->
<div class="grab-all" id="software-note" hidden>
<span class="label">Install it</span>
<p class="dim">One archive: code, trained models, installer, systemd units and the documentation. Download it below, then on your server:</p>
<pre class="cmd" id="software-cmd"></pre>
<p class="fineprint">Read <span class="m">README.md</span> first, then <span class="m">docs/INSTALL.md</span>. The installer never starts trading — the bot comes up switched off and stays that way until you turn it on from Telegram. Live edition: <span class="m">docs/LIVE_TRADING.md</span> before you connect a wallet.</p>
<p class="fineprint">The archive ships with a manifest carrying a sha256 for every file, and the release sha256 is published next to the download. Keep a copy of the tarball: this link expires with your grant.</p>
</div>
<div class="grab-all" id="grab-all" hidden>
<span class="label">Take the whole range in one go</span>
<p class="dim">A range purchase is one file per asset, per table, per day — clicking through them by hand is not the intended path. Save the link list and hand it to any downloader.</p>
<p><a class="dl-btn" id="urls-link" download="polymarket-dataset-urls.txt">Save link list (<span id="urls-count"></span> files)</a></p>
<p class="fineprint">Then, in the folder where you want the data:</p>
<div class="os-tabs" id="os-tabs">
<button type="button" class="os-tab" data-os="unix">Linux / macOS</button>
<button type="button" class="os-tab" data-os="win">Windows (PowerShell)</button>
</div>
<pre class="cmd" id="grab-cmd"></pre>
<p class="fineprint">The list holds one URL per file and carries your access token, so keep it private. Files land under their full <span class="m">asset_daily_date_table.parquet</span> names.</p>
</div>
<details id="files-details">
<summary class="label">Or download files one by one</summary>
<div id="files-list"></div>
</details>
<p class="fineprint">Links are tied to a fresh token issued just now. Reopening this page later re-claims the order and issues a new token, which invalidates both the saved list and the individual links — grab everything you need in this visit.</p>
<div class="grab-all" id="recovery" hidden>
<span class="label">Save this to get back in</span>
<p class="dim">Your claim secret lives only in this browser. This link carries it, so it restores access from any device — and it is the only way back if you clear your browser data. Treat it like a password: anyone holding it can re-claim this order.</p>
<p><a class="dl-btn" id="recovery-link" download="polymarket-dataset-recovery.txt">Save recovery link</a></p>
<pre class="cmd" id="recovery-url"></pre>
</div>
</div>
<div id="recover-form" hidden>
<h2>Restore access</h2>
<p class="dim">Paste the recovery link you saved when you bought (or the whole address it points to). It contains the claim secret this browser is missing.</p>
<p><input id="recover-input" type="text" placeholder="https://…/paid.html?order=…#s=…" style="width:100%;padding:8px;font:inherit"></p>
<p><button class="dl-btn" id="recover-go">Restore</button></p>
<p class="fineprint" id="recover-err"></p>
</div>
</div>
<footer>
<p>Something not adding up? The order id is in the page URL. Message <a href="https://t.me/Datasets_support_bot">@Datasets_support_bot</a> on Telegram with it and we'll sort it out.</p>
</footer>
</div>
<script>
// Same Supabase functions endpoint and public anon key as index.html.
const FUNCTIONS_URL = "https://otaolntxdltbyojbbyhg.supabase.co/functions/v1";
const ANON = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im90YW9sbnR4ZGx0YnlvamJieWhnIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODQ2NjMxNTIsImV4cCI6MjEwMDIzOTE1Mn0.8Axxbhd2vIRaQzXqOcKf-L6lTzrbuUybZlOHC9a6TaM";
const POLL_INTERVAL_MS = 5000;
const POLL_TIMEOUT_MS = 5 * 60 * 1000; // webhook/reconcile can lag a few minutes
const heading = document.getElementById("heading");
const subtext = document.getElementById("subtext");
const panel = document.getElementById("status-panel");
const label = document.getElementById("status-label");
const text = document.getElementById("status-text");
const filesSection = document.getElementById("files-section");
const filesMeta = document.getElementById("files-meta");
const filesList = document.getElementById("files-list");
// kind: "warn" (waiting/info), "ok" (success), "err" (dead end)
function showStatus(kind, labelText, bodyText) {
panel.hidden = false;
panel.className = "notice " + kind;
label.textContent = labelText;
text.textContent = bodyText;
}
// Recovery link: paid.html?order=<id>#s=<claim secret>. The secret rides in
// the fragment so it never reaches the server — not in our logs, not in a
// Referer header to whoever the buyer clicks through to next.
function secretFromHash(hash) {
const h = (hash != null ? hash : location.hash) || "";
const m = /(?:^#|[#&])s=([^&]+)/.exec(h);
try { return m ? decodeURIComponent(m[1]) : null; } catch { return null; }
}
function recoveryUrl(orderId, secret) {
return location.origin + location.pathname +
"?order=" + encodeURIComponent(orderId) + "#s=" + encodeURIComponent(secret);
}
function showRecovery(orderId, secret) {
const url = recoveryUrl(orderId, secret);
document.getElementById("recovery-url").textContent = url;
const a = document.getElementById("recovery-link");
a.href = URL.createObjectURL(new Blob([url + "\n"], { type: "text/plain" }));
document.getElementById("recovery").hidden = false;
}
// Команда пакетной выкачки, своя на каждую ОС.
//
// Почему не одна на всех, как было раньше. Прежняя подсказка предлагала
// `wget --content-disposition -c -i` — и не работала ни там, ни там. В
// PowerShell `wget` это алиас Invoke-WebRequest, который таких ключей не
// знает и падает на разборе аргументов. А настоящий wget с
// --content-disposition сначала шлёт HEAD, чтобы узнать имя файла, и
// упирается в 405: HEAD в download добавлен, но пока не развёрнут.
//
// Обе команды ниже обходятся одним GET на файл и берут имя из самой ссылки:
// после последнего %2F в ней стоит имя файла, потому что path закодирован
// целиком. Это избавляет и от --content-disposition, и от зависимости вроде
// python (на macOS его может не быть вовсе).
const GRAB_COMMANDS = {
unix: (name) =>
'while read -r u; do\n' +
' [ -z "$u" ] && continue\n' +
' curl -sL -o "${u##*%2F}" "$u"\n' +
"done < " + name,
win: (name) =>
"Get-Content " + name + " | ForEach-Object {\n" +
" $u = $_.Trim()\n" +
" if (-not $u) { return }\n" +
" $n = ($u -split '%2F')[-1]\n" +
" curl.exe -sL -o $n $u\n" +
"}",
};
function renderGrabCommand(listName) {
const pre = document.getElementById("grab-cmd");
const tabs = [...document.querySelectorAll(".os-tab")];
const select = (os) => {
pre.textContent = GRAB_COMMANDS[os](listName);
for (const t of tabs) {
t.setAttribute("aria-selected", String(t.dataset.os === os));
}
};
// Угадываем ОС только чтобы выбрать вкладку по умолчанию — вторая всегда
// рядом и в один клик, так что ошибка угадывания ничего не стоит.
const ua = navigator.userAgent || "";
select(/Windows|Win32|Win64/i.test(ua) ? "win" : "unix");
for (const t of tabs) {
t.addEventListener("click", () => select(t.dataset.os));
}
}
function renderFiles(data) {
heading.textContent = "Payment confirmed";
subtext.textContent = "Your files are ready below.";
panel.hidden = true;
filesSection.hidden = false;
const expires = data.expires_at ? new Date(data.expires_at).toLocaleString() : "unknown";
filesMeta.textContent =
(data.downloads_left != null ? data.downloads_left + " download(s) left" : "") +
(data.expires_at ? " · link expires " + expires : "");
const files = data.files || [];
// "One link" for the whole purchase. A range order is one file per asset,
// per table, per day — up to several hundred — so a wall of <a> tags is
// not a delivery mechanism. We build a single urls.txt in the browser and
// hand it over as one download; any standard downloader takes it from there.
//
// The list holds OUR /download endpoints, not the signed CDN URLs those
// redirect to: the signatures expire in minutes, whereas a download URL
// stays valid for the life of the token, so the saved file keeps working
// for as long as the grant does.
//
// No revokeObjectURL here: the blob must stay alive for the whole visit
// because the buyer may save it at any point, and the page is discarded
// on navigation anyway.
const urlFor = (path) =>
FUNCTIONS_URL + "/download?token=" + encodeURIComponent(data.token) +
"&path=" + encodeURIComponent(path);
// Имя файла со ссылками — по датам самой покупки, а не одно на всех.
// Постоянное "polymarket-dataset-urls.txt" означало, что покупатель,
// взявший два диапазона, получает в загрузках второй файл как
// "...urls(1).txt" и уже не может сказать, где какой.
//
// Даты берём из путей: claim отдаёт только список файлов, без start/end,
// зато дата есть в имени каждого файла
// (btc_daily_2026-06-30_book_snaps.parquet). Значит имя считается целиком
// на клиенте и не требует правок в функции — а её сейчас нечем развернуть.
const listFileName = (paths) => {
const dates = [];
for (const p of paths) {
const m = /(\d{4}-\d{2}-\d{2})/.exec(p);
if (m) dates.push(m[1]);
}
if (!dates.length) return "datasets_polymarket.txt";
dates.sort();
const from = dates[0];
const to = dates[dates.length - 1];
return from === to
? "datasets_polymarket_" + from + ".txt"
: "datasets_polymarket_" + from + "_" + to + ".txt";
};
// A software order is one archive, and what the buyer needs next is the
// three commands that unpack and install it — not a link list and a wget
// recipe written for several hundred parquet files.
const software = files.length > 0 && files.every((p) => p.indexOf("software/") === 0);
if (software) {
const archive = files[0].split("/").pop();
document.getElementById("software-cmd").textContent =
"tar xzf " + archive + "\n" +
"cd " + archive.replace(/\.tar\.gz$/, "") + "\n" +
"./install.sh";
document.getElementById("software-note").hidden = false;
document.getElementById("files-details").open = true;
renderFileRows(files, urlFor);
return;
}
if (files.length > 1) {
const listText = files.map(urlFor).join("\n") + "\n";
const listName = listFileName(files);
const blob = new Blob([listText], { type: "text/plain" });
const urlsLink = document.getElementById("urls-link");
urlsLink.href = URL.createObjectURL(blob);
urlsLink.download = listName;
document.getElementById("urls-count").textContent = String(files.length);
renderGrabCommand(listName);
document.getElementById("grab-all").hidden = false;
// With a bulk path on offer, the per-file list is the fallback, not the
// main event — collapsed by default so it does not bury the page.
document.getElementById("files-details").open = false;
} else {
// A single-file order has nothing to bulk-download; show it plainly.
document.getElementById("files-details").open = true;
}
renderFileRows(files, urlFor);
}
// One row per file: the path, and a link that goes to our download endpoint.
// Split out of renderFiles so the software branch above can reuse it after
// returning early.
function renderFileRows(files, urlFor) {
filesList.innerHTML = "";
files.forEach((path) => {
const row = document.createElement("div");
row.className = "file-row";
const pathEl = document.createElement("div");
pathEl.className = "path";
pathEl.textContent = path;
// The download function has verify_jwt disabled, so a plain top-level
// navigation reaches it without any header — confirmed against the deployed
// function (a bare GET returns the function's own 403/302, not a gateway
// 401). It answers 302 to a signed HF CDN URL; the browser follows it and
// saves the file. A plain <a> is simpler and avoids cross-origin fetch
// limits on the CDN response.
const link = document.createElement("a");
link.className = "dl-btn";
link.textContent = "Download";
link.href = urlFor(path);
link.setAttribute("download", "");
row.appendChild(pathEl);
row.appendChild(link);
filesList.appendChild(row);
});
}
// The download function needs the anon apikey/Authorization headers, which a
// plain <a href> cannot send. So the button fetches with those headers,
// lets fetch follow the 302 to the CDN, and navigates the browser to the
// resolved URL. If that's blocked (e.g. cross-origin fetch restrictions on
// the final CDN response), fall back to a direct, header-less navigation —
// the download function has verify_jwt disabled, so a bare GET may still work.
async function startDownload(btn, pathEl, token, path) {
const url = FUNCTIONS_URL + "/download?token=" + encodeURIComponent(token) +
"&path=" + encodeURIComponent(path);
const original = btn.textContent;
btn.disabled = true;
btn.textContent = "Preparing…";
const existingErr = pathEl.querySelector(".err");
if (existingErr) existingErr.remove();
try {
const res = await fetch(url, {
method: "GET",
redirect: "follow",
headers: { apikey: ANON, Authorization: "Bearer " + ANON },
});
if (!res.ok) {
let msg = "download failed (" + res.status + ")";
try {
const data = await res.json();
if (data && data.error) msg = data.error;
} catch (_) { /* not JSON, keep generic message */ }
throw new Error(msg);
}
window.location = res.url;
btn.textContent = "Started";
setTimeout(() => { btn.disabled = false; btn.textContent = original; }, 3000);
} catch (err) {
btn.disabled = false;
btn.textContent = original;
const errEl = document.createElement("span");
errEl.className = "err";
errEl.textContent = "Download failed (" + err.message + "). ";
const retryLink = document.createElement("a");
retryLink.href = url;
retryLink.textContent = "Try opening the link directly.";
errEl.appendChild(retryLink);
pathEl.appendChild(errEl);
}
}
async function pollClaim(orderId, secret, startedAt) {
let res, data;
try {
res = await fetch(FUNCTIONS_URL + "/claim", {
method: "POST",
// content-type only: apikey/Authorization aren't needed (verify_jwt off)
// and sending them fails the CORS preflight (allow-headers = content-type).
headers: { "content-type": "application/json" },
body: JSON.stringify({ order_id: orderId, claim_secret: secret }),
});
data = await res.json();
} catch (err) {
// Network hiccup: keep retrying within the same timeout budget.
if (Date.now() - startedAt < POLL_TIMEOUT_MS) {
showStatus("warn", "Connection trouble", "Could not reach the server, retrying…");
setTimeout(() => pollClaim(orderId, secret, startedAt), POLL_INTERVAL_MS);
} else {
showStatus("err", "Could not reach the server", "Refresh this page to try again.");
}
return;
}
if (res.ok) {
renderFiles(data);
showRecovery(orderId, secret);
return;
}
if (res.status === 403 && data && data.error === "not_available") {
if (Date.now() - startedAt < POLL_TIMEOUT_MS) {
showStatus("warn", "Waiting for payment confirmation",
"This can take a few minutes — we'll keep checking automatically.");
setTimeout(() => pollClaim(orderId, secret, startedAt), POLL_INTERVAL_MS);
} else {
heading.textContent = "Still waiting";
showStatus("err", "Still not confirmed",
"Payment can take a few minutes to confirm on-chain. Refresh this page later to check again.");
}
return;
}
// Any other error (bad/expired claim secret, unknown order, etc.) is not
// worth retrying automatically.
heading.textContent = "Couldn't claim this order";
showStatus("err", "Error", (data && data.error) ? data.error : "Something went wrong claiming this order.");
}
(function init() {
const params = new URLSearchParams(location.search);
const orderId = params.get("order");
if (!orderId) {
heading.textContent = "Bad link";
subtext.textContent = "";
showStatus("err", "Missing order id", "This link is missing an order id. Go back to the dataset page and buy again.");
return;
}
// Handed out once at purchase time and never sent anywhere else — do not
// log it. Two places it can come from: the fragment of a recovery link
// (any device), or this browser's localStorage (the device that bought).
// The fragment is deliberate: unlike the query string it is never sent to
// the server, so the secret stays out of access logs and Referer headers.
const secret = secretFromHash() || localStorage.getItem("claim:" + orderId);
if (!secret) {
heading.textContent = "Can't find your order";
subtext.textContent = "";
showStatus("err", "No claim secret in this browser",
"The claim secret is stored only in the browser you bought from. Open that browser, or paste your saved recovery link below.");
const form = document.getElementById("recover-form");
const input = document.getElementById("recover-input");
const err = document.getElementById("recover-err");
form.hidden = false;
document.getElementById("recover-go").addEventListener("click", () => {
const raw = input.value.trim();
// Accept the whole link or just the "#s=…"/bare secret pasted alone.
const pasted = secretFromHash(raw.includes("#") ? raw.slice(raw.indexOf("#")) : "#s=" + raw);
if (!pasted) {
err.textContent = "That doesn't look like a recovery link. It should contain #s= followed by your secret.";
return;
}
localStorage.setItem("claim:" + orderId, pasted);
err.textContent = "";
form.hidden = true;
showStatus("warn", "Checking…", "Confirming your payment…");
pollClaim(orderId, pasted, Date.now());
});
return;
}
// Persist so a later visit from this browser works without the link, and
// drop the secret from the address bar so it does not linger in history.
localStorage.setItem("claim:" + orderId, secret);
if (location.hash) history.replaceState(null, "", location.pathname + location.search);
showStatus("warn", "Checking…", "Confirming your payment…");
pollClaim(orderId, secret, Date.now());
})();
</script>
</body>
</html>