← back to Dw Domain Viewer

public/index.html

168 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>designerwallcoverings.com — Domain Viewer</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%231e2a3a'/%3E%3Ctext x='8' y='12' text-anchor='middle' font-family='Georgia,serif' font-size='11' fill='%23f7f1e6'%3Ed%3C/text%3E%3C/svg%3E">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
  :root { --rail: 320px; --bg:#0e1116; --panel:#161b22; --line:#2b313b; --txt:#dfe6ee; --dim:#8b95a3; --accent:#3fb6a8; }
  * { box-sizing: border-box; }
  body { margin:0; height:100vh; display:flex; font:14px/1.45 -apple-system, "SF Pro Text", Helvetica, sans-serif; background:var(--bg); color:var(--txt); }

  #rail { width:var(--rail); min-width:240px; max-width:500px; display:flex; flex-direction:column; border-right:1px solid var(--line); background:var(--panel); }
  #rail header { padding:14px 14px 10px; border-bottom:1px solid var(--line); }
  #rail h1 { margin:0 0 8px; font-size:15px; font-weight:700; letter-spacing:.02em; }
  #rail h1 small { color:var(--dim); font-weight:400; }
  #q { width:100%; padding:7px 10px; border-radius:7px; border:1px solid var(--line); background:var(--bg); color:var(--txt); outline:none; }
  #q:focus { border-color:var(--accent); }
  #recheck { width:100%; margin-top:8px; padding:7px 10px; border-radius:7px; border:1px solid var(--line); background:var(--bg); color:var(--txt); cursor:pointer; font-size:13px; }
  #recheck:hover:not(:disabled) { border-color:var(--accent); color:var(--accent); }
  #recheck:disabled { opacity:.6; cursor:progress; }
  #recheck.busy { color:var(--accent); }
  #lastcheck { display:block; margin-bottom:6px; color:var(--dim); }
  #list { flex:1; overflow-y:auto; padding:6px; }
  .item { display:flex; align-items:center; gap:8px; padding:6px 9px; border-radius:7px; cursor:pointer; color:var(--txt); user-select:none; }
  .item:hover { background:#1e2530; }
  .item.active { background:#213132; outline:1px solid var(--accent); }
  .dot { width:8px; height:8px; border-radius:50%; flex:none; background:#555; }
  .dot.up { background:#3fbf6f; }
  .dot.auth { background:#d9a53a; }
  .dot.down { background:#d95555; }
  .name { flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .name b { color:var(--accent); font-weight:600; }
  .tag { flex:none; font-size:9.5px; letter-spacing:.04em; color:var(--dim); border:1px solid var(--line); border-radius:4px; padding:1px 4px; }
  .ms { color:var(--dim); font-size:11px; flex:none; }
  #foot { padding:8px 14px; border-top:1px solid var(--line); color:var(--dim); font-size:11.5px; }

  #main { flex:1; display:flex; flex-direction:column; min-width:0; }
  #bar { display:flex; align-items:center; gap:10px; padding:8px 12px; border-bottom:1px solid var(--line); background:var(--panel); }
  #cur { font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  #cur a { color:var(--accent); text-decoration:none; }
  #bar .sp { flex:1; }
  button { padding:6px 12px; border-radius:7px; border:1px solid var(--line); background:var(--bg); color:var(--txt); cursor:pointer; font-size:13px; }
  button:hover { border-color:var(--accent); color:var(--accent); }
  #frame { flex:1; border:0; width:100%; background:#fff; }
  #empty { flex:1; display:flex; align-items:center; justify-content:center; color:var(--dim); flex-direction:column; gap:8px; }
  #empty .big { font-size:40px; }
</style>
</head>
<body>
  <div id="rail">
    <header>
      <h1>designerwallcoverings.com <small id="count"></small></h1>
      <input id="q" type="search" placeholder="Filter sites…" autocomplete="off">
      <button id="recheck" title="Re-run the health check on every site (no restart — just re-probes reachability)">↻ Re-check all sites</button>
    </header>
    <div id="list"></div>
    <div id="foot"><span id="lastcheck"></span>click a site → renders in the right panel (unified admin pw auto-applied) · dot: <span style="color:#3fbf6f">up</span> / <span style="color:#d9a53a">auth-gated</span> / <span style="color:#d95555">down or pw rejected</span></div>
  </div>
  <div id="main">
    <div id="bar">
      <span id="cur">— pick a site</span>
      <span class="sp"></span>
      <button id="reload" title="Reload iframe">⟳ Reload</button>
      <button id="newtab" title="Open in a new browser tab">↗ New Tab</button>
    </div>
    <div id="empty"><div class="big">🌐</div><div>Select a site on the left to view it live here.</div></div>
    <iframe id="frame" style="display:none"></iframe>
  </div>

<script>
let domains = [], status = {}, current = localStorage.getItem('dwv-current') || '';
const list = document.getElementById('list'), q = document.getElementById('q');
const frame = document.getElementById('frame'), empty = document.getElementById('empty');
const cur = document.getElementById('cur'), count = document.getElementById('count');

// internal.* and *-internal hosts are curation tools, not customer-facing microsites.
const isInternal = (d) => /^internal\./.test(d) || /-internal\./.test(d);
// Rail shows the short label; the full host lives in the title tooltip.
const shortName = (d) => d === 'designerwallcoverings.com' ? '● apex (DW store)' : d.replace('.designerwallcoverings.com', '');

function render() {
  const f = q.value.trim().toLowerCase();
  const shown = domains.filter(d => d.includes(f));
  count.textContent = `(${shown.length}/${domains.length})`;
  list.innerHTML = '';
  for (const d of shown) {
    const s = status[d];
    const row = document.createElement('div');
    row.className = 'item' + (d === current ? ' active' : '');
    row.title = d;
    const dotCls = !s ? '' : s.authFail ? 'down' : s.gated ? 'auth' : s.up ? 'up' : 'down';
    const label = f ? shortName(d).replace(f, `<b>${f}</b>`) : shortName(d);
    row.innerHTML = `<span class="dot ${dotCls}"></span><span class="name">${label}</span>` +
      (isInternal(d) ? `<span class="tag">INT</span>` : '') +
      (s && s.up ? `<span class="ms">${s.ms}ms</span>` : s ? `<span class="ms">—</span>` : '');
    row.onclick = () => select(d);
    list.appendChild(row);
  }
}

function select(d) {
  current = d;
  localStorage.setItem('dwv-current', d);
  cur.innerHTML = `<a href="https://${d}" target="_blank" rel="noopener noreferrer">${d}</a>`;
  // Load through the server-side proxy — it injects the unified admin credential
  // and strips X-Frame-Options, so every site renders in-panel without prompting.
  empty.style.display = 'none';
  frame.style.display = 'block';
  frame.src = '/site/' + d + '/';
  render();
}

document.getElementById('reload').onclick = () => { if (current) frame.src = '/site/' + current + '/'; };
document.getElementById('newtab').onclick = () => { if (current) window.open('https://' + current, '_blank', 'noopener'); };
q.oninput = render;

const recheck = document.getElementById('recheck');
const lastcheck = document.getElementById('lastcheck');

// Re-run the health probe across every site and refresh the dots. This is a
// re-CHECK, not a restart — it only re-tests reachability, so it's safe to click
// anytime. Guarded against double-fires: the probe takes ~60-90s for 172 sites.
let checking = false;
async function loadStatus() {
  if (checking) return;
  checking = true;
  recheck.disabled = true;
  recheck.classList.add('busy');
  recheck.textContent = '↻ Checking…';
  try {
    const sres = await fetch('/api/status');
    if (!sres.ok) throw new Error('status HTTP ' + sres.status);
    const st = await sres.json();
    for (const s of st) status[s.domain] = s;
    render();
    const up = st.filter(s => s.up).length;
    lastcheck.textContent = `checked ${up}/${st.length} up · ${new Date().toLocaleTimeString([], { hour:'numeric', minute:'2-digit', second:'2-digit' })}`;
  } catch (e) {
    // Aborted mid-flight (tab closed / server restarted) must not surface as an
    // unhandled rejection; the list already rendered, dots are best-effort.
    lastcheck.textContent = 'check failed — click ↻ to retry';
  } finally {
    checking = false;
    recheck.disabled = false;
    recheck.classList.remove('busy');
    recheck.textContent = '↻ Re-check all sites';
  }
}
recheck.onclick = loadStatus;

(async () => {
  try {
    const dres = await fetch('/api/domains');
    if (!dres.ok) throw new Error('domains HTTP ' + dres.status);
    domains = await dres.json();
    render();
    if (current && domains.includes(current)) select(current);
  } catch (e) {
    count.textContent = '(load failed — reload)';
    return;
  }
  loadStatus();
})();
</script>
</body>
</html>