← back to Costa Rica
public/build.html
99 lines
<!doctype html>
<html lang="en"><head>
<script>
// Credential-safe fetch guard (fleet drop-in) — if this page is opened with
// credentials in the URL (a saved bookmark / Chrome-remembered basic-auth:
// https://user:pass@host/…), the browser poisons document.baseURI, so any
// relative or root-relative fetch('/api/…') throws "Request cannot be constructed
// from a URL that includes credentials" and callers silently fall to an empty
// state. Resolve every non-absolute request URL against the credential-free
// location instead of baseURI. Placed first so it wraps window.fetch before any
// app script runs. Ref: creds-in-url-fetch-guard-fleet-pattern.
(function () {
var _fetch = window.fetch.bind(window);
var cleanBase = function () { return location.origin + location.pathname; };
window.fetch = function (input, init) {
try {
if (typeof input === 'string' && !/^[a-z]+:\/\//i.test(input) && input.indexOf('//') !== 0) {
input = new URL(input, cleanBase()).href;
} else if (input instanceof Request && !/^[a-z]+:\/\//i.test(input.url)) {
input = new Request(new URL(input.url, cleanBase()).href, input);
}
} catch (_) { /* fall through to native */ }
return _fetch(input, init);
};
})();
</script>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>CR Marketplace — Build Ops</title>
<style>
:root{--g:#0a7d55;--ink:#1a2b22;--mut:#6b756e;--line:#e2e5e0;--bad:#a33;--warn:#b8860b;}
*{box-sizing:border-box;} body{margin:0;font:14px/1.45 -apple-system,Segoe UI,Roboto,sans-serif;background:#f6f7f5;color:var(--ink);}
header{background:var(--g);color:#fff;padding:14px 20px;display:flex;gap:14px;align-items:center;}
header h1{font-size:17px;margin:0;} .ts{margin-left:auto;font-size:12px;opacity:.85;}
main{padding:16px 20px 48px;display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));}
.panel{background:#fff;border:1px solid var(--line);border-radius:12px;padding:16px;}
.panel h2{font-size:13px;text-transform:uppercase;letter-spacing:.04em;color:var(--mut);margin:0 0 12px;}
.kpis{display:flex;flex-wrap:wrap;gap:10px;} .kpi b{display:block;font-size:20px;color:var(--g);} .kpi span{font-size:11px;color:var(--mut);}
.kpi{min-width:84px;} .pill{display:inline-flex;align-items:center;gap:6px;padding:3px 10px;border-radius:20px;font-size:12px;font-weight:600;}
.on{background:#e7f5ee;color:var(--g);} .off{background:#fdeaea;color:var(--bad);}
.bar{height:8px;border-radius:4px;background:#eef1ee;overflow:hidden;margin-top:6px;} .bar>i{display:block;height:100%;background:var(--g);}
ul{list-style:none;padding:0;margin:0;} li{padding:6px 0;border-bottom:1px solid #f0f2ef;display:flex;justify-content:space-between;gap:8px;}
.chk{display:flex;align-items:center;gap:8px;padding:5px 0;} .chk .dot{width:16px;text-align:center;}
code{font-family:ui-monospace,Menlo,monospace;font-size:12px;color:#456;} .mut{color:var(--mut);}
.src{font-variant-numeric:tabular-nums;}
</style></head>
<body>
<header><h1>🇨🇷 CR Marketplace — Build Ops</h1><span class="ts" id="ts"></span></header>
<main id="main"><p>Loading…</p></main>
<script>
const money=(m)=>`$${(m/100).toFixed(2)}`;
const pill=(ok,txt)=>`<span class="pill ${ok?'on':'off'}">${ok?'●':'○'} ${txt}</span>`;
async function load(){
const d=await (await fetch('/api/build/status')).json();
document.getElementById('ts').textContent='updated '+new Date(d.ts).toLocaleTimeString();
const L=d.live, D=d.data;
const hacPct=D.hacienda+D.hacienda_pending?Math.round(100*D.hacienda/(D.hacienda+D.hacienda_pending)):100;
document.getElementById('main').innerHTML=`
<div class="panel"><h2>Integrations</h2>
${pill(L.payment.live,`Payments: ${L.payment.provider} ${L.payment.live?'LIVE':'sandbox'}`)}
${pill(L.whatsapp.live,`WhatsApp ${L.whatsapp.live?'LIVE':'sandbox'}`)}
${pill(L.plaid.live,`Plaid ${L.plaid.live?'LIVE':L.plaid.env}`)}
${pill(L.apple.configured,'Apple sign-in')}
</div>
<div class="panel"><h2>Data pipeline</h2>
<div class="kpis">
<div class="kpi"><b>${(+D.places).toLocaleString()}</b><span>listings</span></div>
<div class="kpi"><b>${(+D.geo).toLocaleString()}</b><span>geo</span></div>
<div class="kpi"><b>${(+D.active_biz).toLocaleString()}</b><span>active biz</span></div>
<div class="kpi"><b>${D.bookable}</b><span>bookable</span></div>
</div>
<div class="mut" style="margin-top:10px">Hacienda enrichment ${(+D.hacienda).toLocaleString()} / ${(+D.hacienda+ +D.hacienda_pending).toLocaleString()} (${hacPct}%)</div>
<div class="bar"><i style="width:${hacPct}%"></i></div>
</div>
<div class="panel"><h2>Marketplace (sandbox)</h2>
<div class="kpis">
<div class="kpi"><b>${D.bookings}</b><span>bookings</span></div>
<div class="kpi"><b>${money(D.revenue_minor)}</b><span>fee revenue</span></div>
<div class="kpi"><b>${D.users}</b><span>users</span></div>
<div class="kpi"><b>${D.hosts}</b><span>hosts</span></div>
</div>
<div class="mut" style="margin-top:8px">⚠️ sandbox/test data — not real money</div>
</div>
<div class="panel"><h2>Go-live checklist</h2>
${d.checklist.map(c=>`<div class="chk"><span class="dot">${c.done?'✅':'🔴'}</span><span>${c.label}</span></div>`).join('')}
</div>
<div class="panel"><h2>Listings by source</h2>
<ul>${d.bySource.map(s=>`<li><span>${s.source||'—'}</span><span class="src">${(+s.count).toLocaleString()}</span></li>`).join('')}</ul>
</div>
<div class="panel"><h2>Recent ingest runs</h2>
<ul>${d.runs.map(r=>`<li><span>${r.source} <span class="mut">${r.status}</span></span><span class="src">+${r.rows_added??0}</span></li>`).join('')||'<li class="mut">none</li>'}</ul>
</div>
<div class="panel" style="grid-column:1/-1"><h2>Recent commits</h2>
<ul>${d.commits.map(c=>`<li><span><code>${c.hash}</code> ${c.subject}</span><span class="mut">${c.when}</span></li>`).join('')}</ul>
</div>`;
}
load(); setInterval(load, 15000);
</script>
</body></html>