[object Object]

← back to Costa Rica

costa-rica: build/ops dashboard (/build, gated) — integration live-state, data pipeline + enrichment %, go-live checklist, ingest runs, recent commits, auto-refresh — TK-10346 yoloforever c2

1cc9a07a410a5100c02d84b855234deafc9eea4c · 2026-08-07 10:20:31 -0700 · Steve

Files touched

Diff

commit 1cc9a07a410a5100c02d84b855234deafc9eea4c
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Aug 7 10:20:31 2026 -0700

    costa-rica: build/ops dashboard (/build, gated) — integration live-state, data pipeline + enrichment %, go-live checklist, ingest runs, recent commits, auto-refresh — TK-10346 yoloforever c2
---
 public/build.html | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 routes/build.js   | 60 ++++++++++++++++++++++++++++++++++++++++++++
 server.js         |  2 ++
 3 files changed, 136 insertions(+)

diff --git a/public/build.html b/public/build.html
new file mode 100644
index 0000000..a90fb01
--- /dev/null
+++ b/public/build.html
@@ -0,0 +1,74 @@
+<!doctype html>
+<html lang="en"><head>
+<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>
diff --git a/routes/build.js b/routes/build.js
new file mode 100644
index 0000000..f03d282
--- /dev/null
+++ b/routes/build.js
@@ -0,0 +1,60 @@
+'use strict';
+// Build / ops dashboard API — behind the basic-auth gate. One aggregate status
+// endpoint for the whole build: integration live-state, data pipeline, go-live
+// checklist, recent ingest runs + commits.
+const express = require('express');
+const { execSync } = require('child_process');
+const path = require('path');
+const { pool } = require('../lib/db');
+const router = express.Router();
+
+const liveState = () => ({
+  payment: { provider: require('../lib/payments').getProvider().name, live: require('../lib/payments').getProvider().liveMode },
+  whatsapp: { live: require('../lib/whatsapp').liveMode },
+  plaid: { live: require('../lib/plaid').liveMode, env: require('../lib/plaid').ENV },
+  apple: { configured: true }, // verifier always available; needs App ID capability at build
+});
+
+router.get('/status', async (_req, res) => {
+  const data = {};
+  try {
+    const { rows: [d] } = await pool.query(`
+      SELECT (SELECT count(*) FROM places) places,
+             (SELECT count(*) FROM places WHERE lat IS NOT NULL) geo,
+             (SELECT count(*) FROM places WHERE hacienda_enriched_at IS NOT NULL) hacienda,
+             (SELECT count(*) FROM places WHERE cedula_juridica IS NOT NULL AND hacienda_enriched_at IS NULL) hacienda_pending,
+             (SELECT count(*) FROM places WHERE hacienda_situacion='Inscrito') active_biz,
+             (SELECT count(*) FROM place_booking WHERE is_active) bookable,
+             (SELECT count(*) FROM bookings) bookings,
+             (SELECT COALESCE(SUM(platform_fee),0) FROM bookings WHERE status IN ('confirmed','completed')) revenue_minor,
+             (SELECT count(*) FROM app_users) users,
+             (SELECT count(*) FROM hosts) hosts`);
+    Object.assign(data, d);
+  } catch (e) { data.error = e.message; }
+
+  let bySource = [], runs = [], commits = [];
+  try { ({ rows: bySource } = await pool.query(`SELECT source, count(*) FROM places GROUP BY source ORDER BY 2 DESC LIMIT 12`)); } catch {}
+  try { ({ rows: runs } = await pool.query(`SELECT source, status, rows_in, rows_added, started_at, finished_at FROM ingest_runs ORDER BY id DESC LIMIT 8`)); } catch {}
+  try {
+    commits = execSync('git log --oneline -8 --format="%h|%s|%cr"', { cwd: path.join(__dirname, '..') })
+      .toString().trim().split('\n').map(l => { const [h, s, w] = l.split('|'); return { hash: h, subject: s, when: w }; });
+  } catch {}
+
+  const checklist = [
+    { id: 'schema', label: 'Marketplace schema (migration 004/005)', done: true },
+    { id: 'payments', label: 'Payment layer (Tilopay/ONVO, sandbox)', done: true },
+    { id: 'whatsapp', label: 'WhatsApp client + webhook (sandbox)', done: true },
+    { id: 'apple', label: 'Sign in with Apple (backend + app)', done: true },
+    { id: 'app', label: 'Expo native app scaffold', done: true },
+    { id: 'admin', label: 'Admin dashboard', done: true },
+    { id: 'gate_wa', label: '🔴 GATED: Meta WABA number + token (Steve)', done: false },
+    { id: 'gate_tilopay', label: '🔴 GATED: Tilopay merchant keys (Steve)', done: false },
+    { id: 'gate_plaid', label: '🔴 GATED: Plaid production keys (Steve)', done: false },
+    { id: 'gate_eas', label: '🔴 GATED: eas login → build/submit (Steve)', done: false },
+    { id: 'gate_deploy', label: '🔴 GATED: Kamatera deploy + DNS (Steve)', done: false },
+  ];
+
+  res.json({ ok: true, live: liveState(), data, bySource, runs, commits, checklist, ts: new Date().toISOString() });
+});
+
+module.exports = router;
diff --git a/server.js b/server.js
index 967eece..a540f81 100644
--- a/server.js
+++ b/server.js
@@ -58,6 +58,8 @@ app.get('/health', (_req, res) => res.json({ ok: true, site: SITE_NAME, ts: new
 // Admin (behind the basic-auth gate) — host-claim approvals + bookings oversight.
 app.use('/api/admin', require('./routes/admin'));
 app.get('/admin', (_req, res) => res.sendFile(path.join(__dirname, 'public', 'admin.html')));
+app.use('/api/build', require('./routes/build'));
+app.get('/build', (_req, res) => res.sendFile(path.join(__dirname, 'public', 'build.html')));
 
 const VERTICALS = {
   tourism: ['tourism_hotel','tourism_tour','tourism_beach','tourism_restaurant','tourism_surf'],

← aa3c1e2 costa-rica: Sign in with Apple (backend) — lib/apple RS256 J  ·  back to Costa Rica  ·  costa-rica: Cody gate c2 — only auto-link Apple identity to 0689909 →