// 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 = `