// collect β€” frontend const $ = (id) => document.getElementById(id); const log = (msg) => { const el = $("log"); if (!el) return; const now = new Date().toISOString().replace("T"," ").replace("Z",""); el.textContent += `[${now}] ${msg}\n`; el.scrollTop = el.scrollHeight; }; let CFG = null; let RATES = null; let CURRENT_QUOTE = null; let CURRENT_TX = null; let SELECTED_ACCOUNT = null; // ===== utils ===== function bytesHuman(n=0){ const u = ["B","KB","MB","GB","TB"]; let i = 0; let v = Number(n||0); while (v >= 1024 && i < u.length-1){ v/=1024; i++; } return `${v.toFixed(i?2:0)} ${u[i]}`; } function middleEllipsis(s="", keep=6){ s = String(s); if (s.length <= keep*2) return s; return `${s.slice(0,keep)}…${s.slice(-keep)}`; } async function api(path, opt={}){ const res = await fetch(path, { ...opt, headers: { "content-type":"application/json", ...(opt.headers||{}) }}); if (!res.ok) throw new Error(`${path} ${res.status}`); return res.json(); } function isImageFilename(name = "") { const n = name.toLowerCase(); return n.endsWith(".png") || n.endsWith(".jpg") || n.endsWith(".jpeg") || n.endsWith(".gif") || n.endsWith(".webp") || n.endsWith(".bmp") || n.endsWith(".svg"); } function ipfsUrlFromPath(path) { if (!path) return ""; const gw = (CFG && CFG.gateway) ? CFG.gateway.replace(/\/+$/,"") : ""; return gw ? `${gw}${path}` : path; } // ===== wallet UI ===== function updateWalletUI() { const btn = $("btnConnectTop"); const acctEl = $("tbAcct"); if (!btn || !acctEl) return; if (!SELECTED_ACCOUNT) { btn.disabled = !window.ethereum; btn.textContent = window.ethereum ? "MetaMask Login" : "No MetaMask"; acctEl.textContent = "wallet: not connected"; } else { btn.disabled = false; btn.textContent = "MetaMask Logout"; acctEl.textContent = `wallet: ${SELECTED_ACCOUNT}`; } } $("btnConnectTop").addEventListener("click", async () => { try { if (!window.ethereum) { updateWalletUI(); return; } if (!SELECTED_ACCOUNT) { const accounts = await ethereum.request({ method: "eth_requestAccounts" }); SELECTED_ACCOUNT = (accounts && accounts[0]) ? accounts[0] : null; log(`βœ… Login ${SELECTED_ACCOUNT || "(none)"}`); updateWalletUI(); if (SELECTED_ACCOUNT) await loadFiles(); } else { log("πŸ‘‹ Logout"); SELECTED_ACCOUNT = null; CURRENT_QUOTE = null; CURRENT_TX = null; updateWalletUI(); await loadFiles(); } } catch (e) { log(`wallet toggle error: ${e.message}`); } }); // ===== config & health & rates ===== async function loadConfigAndHealth(){ CFG = await api("/api/config/public"); $("cfgChain").textContent = CFG.chain || "–"; $("cfgRpc").textContent = CFG.rpc || "–"; $("cfgPayTo").textContent = CFG.payTo || "–"; $("cfgGateway").textContent = CFG.gateway || "–"; $("cfgTier").textContent = (CFG.tierBytes ? `${Math.round(CFG.tierBytes/1024/1024)} MB` : "–"); $("cfgPpt").textContent = (CFG.priceEgazPerTier ? `${CFG.priceEgazPerTier} ${CFG.currencyBase||"EGAZ"}` : "–"); $("cfgConf").textContent = CFG.requiredConfirmations ?? "–"; const h = await api("/api/health"); $("healthBlock").textContent = h.latestBlock ?? "–"; $("healthReq").textContent = h.requiredConfirmations ?? "–"; $("healthNow").textContent = h.now ?? "–"; try{ RATES = await api("/api/rates"); $("rateSrc").textContent = RATES.source || "fallback"; $("rateEgazUsd").textContent = RATES.egazUsd ? `$${RATES.egazUsd}` : "–"; $("rateEtiUsd").textContent = RATES.etiUsd ? `$${RATES.etiUsd}` : "–"; $("rateEtiPerEgaz").textContent = RATES.etiPerEgaz ? `${RATES.etiPerEgaz.toFixed(6)} ETI` : "–"; log(`Rates src=${RATES.source||"?"} egazUsd=${RATES.egazUsd||0} etiUsd=${RATES.etiUsd||0}`); }catch(e){ $("rateSrc").textContent = "unavailable"; $("rateEgazUsd").textContent = "–"; $("rateEtiUsd").textContent = "–"; $("rateEtiPerEgaz").textContent = "–"; log(`rates error: ${e.message}`); } } // ===== files ===== async function loadFiles(){ const status = $("filesStatus"); try{ status.textContent = "loading…"; const addr = SELECTED_ACCOUNT || "0x0000000000000000000000000000000000000000"; const res = await api(`/api/files?address=${addr}`); const files = res.files || []; renderFiles(files); renderGallery(files); status.textContent = `showing ${files.length} file(s) for ${middleEllipsis(addr)}`; // recent preview const recent = files[0]; const box = $("recentBox"); const who = $("recentFor"); who.textContent = `Showing files for ${middleEllipsis(addr)}`; if (!recent){ box.textContent = "No preview"; return; } const href = ipfsUrlFromPath(recent.path || ""); if (href && isImageFilename(recent.filename||"")){ box.innerHTML = ``; } else { box.innerHTML = `${recent.filename||"file"}`; } }catch(e){ status.textContent = `error: ${e.message}`; renderFiles([]); renderGallery([]); } } function renderFiles(files){ const wrap = $("filesTableWrap"); const empty = $("filesEmpty"); const tbody = $("filesTbody"); tbody.innerHTML = ""; if (!files || files.length === 0){ empty.classList.remove("hidden"); wrap.classList.add("hidden"); return; } empty.classList.add("hidden"); wrap.classList.remove("hidden"); for (const f of files){ const tr = document.createElement("tr"); const size = f.size_bytes ?? f.sizeBytes ?? 0; const cid = f.cid || ""; const path = f.path || ""; const label = f.label || ""; const when = f.created_at || f.createdAt || ""; const status = f.status || "stored"; tr.innerHTML = ` ${f.filename || "β€”"} ${bytesHuman(size)} ${cid || "β€”"} ${path || "β€”"} ${label || "β€”"} ${status} ${when ? new Date(when).toLocaleString() : "β€”"} `; tbody.appendChild(tr); } } function renderGallery(files){ const wrap = $("gallery"); const status = $("galleryStatus"); wrap.innerHTML = ""; if (!files || files.length === 0){ status.textContent = "No files yet."; return; } status.textContent = `Showing ${files.length} file(s).`; for (const f of files){ const card = document.createElement("div"); card.className = "card-thumb"; const head = document.createElement("div"); head.className = "small"; head.style.display = "flex"; head.style.justifyContent = "space-between"; head.style.gap = "8px"; head.innerHTML = ` ${f.filename || "β€”"} ${(f.currency || "EGAZ").toUpperCase()} `; const media = document.createElement("div"); media.className = "thumb-box"; const link = document.createElement("a"); const href = ipfsUrlFromPath(f.path || ""); link.href = href || "#"; link.target = "_blank"; link.rel = "noopener"; if (href && isImageFilename(f.filename || "")) { const img = document.createElement("img"); img.src = href; img.alt = f.filename || "image"; img.loading = "lazy"; img.onerror = () => { media.innerHTML = `
Preview unavailable
${(f.path||"").slice(0,40)}…
`; }; link.appendChild(img); } else { link.innerHTML = `
No preview
${(f.path||"").slice(0,40)}…
`; } media.appendChild(link); const foot = document.createElement("div"); foot.className = "small"; foot.style.display = "flex"; foot.style.justifyContent = "space-between"; const size = f.size_bytes ?? f.sizeBytes ?? 0; const when = f.created_at || f.createdAt || ""; foot.innerHTML = ` ${bytesHuman(size)} ${when ? new Date(when).toLocaleString() : ""} `; card.appendChild(head); card.appendChild(media); card.appendChild(foot); wrap.appendChild(card); } } // ===== wallet ensure ===== async function ensureWallet(){ updateWalletUI(); if (!window.ethereum) return; const accts = await ethereum.request({ method: "eth_accounts" }); SELECTED_ACCOUNT = (accts && accts[0]) ? accts[0] : null; updateWalletUI(); ethereum.removeAllListeners?.("accountsChanged"); ethereum.on?.("accountsChanged", async (accounts) => { SELECTED_ACCOUNT = (accounts && accounts[0]) ? accounts[0] : null; updateWalletUI(); await loadFiles(); }); ethereum.removeAllListeners?.("chainChanged"); ethereum.on?.("chainChanged", async (_chainId) => { await loadConfigAndHealth(); await loadFiles(); }); } // ===== init ===== (async function init(){ try{ updateWalletUI(); // immediate correct button label await loadConfigAndHealth(); await ensureWallet(); await loadFiles(); }catch(e){ log(`init error: ${e.message}`); } })();