RECORDGOD/site/dash.html
type-two 53b1a64a48 feat(nav): light/dark switch every page + store→rack→crate navigator
- nav.js owns a light/dark toggle (persisted, default light) across all owner pages
- /nav API: spaces, store-layout, rack/{id}, crate/{id} over migrated virtual_* geometry
- /search rebuilt as Store→Rack→Crate drill-down (true rack rotation, per-level slot grid,
  crate facing arrows + BACK/FRONT/L/R, search→locate)
- facing convention (directionYaw, slot-grid, rotatedArrows, world composition) in RECORDGOD_NAVIGATOR_PLAN.md
- no-cache HTML routes so deploys show instantly

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 19:33:24 +10:00

123 lines
5.5 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>RecordGod — control room</title>
<script defer src="/nav.js?v=5"></script>
<style>
:root{--pink:#d10f7a;--ink:#f4f5f7;--mut:#5f5f6c;--card:#ffffff;--line:#e2e2ea}
*{box-sizing:border-box}
html,body{margin:0;background:var(--ink);color:#1b1b22;font:15px/1.55 system-ui,sans-serif}
.wrap{max-width:660px;margin:0 auto;padding:6vh 22px}
h1{font-size:28px;font-weight:600;margin:0}
h1 b{color:var(--pink)}
.sub{color:var(--mut);margin:.2em 0 28px}
.card{border:1px solid var(--line);border-radius:12px;padding:18px;background:var(--card);margin:0 0 18px}
.card h2{font-size:15px;font-weight:500;margin:0 0 12px;color:#fff}
label{display:block;font-size:13px;color:var(--mut);margin:12px 0 4px}
input{width:100%;padding:10px;border:1px solid var(--line);border-radius:8px;background:#ffffff;color:#1b1b22;font:14px system-ui}
.field{display:flex;gap:8px;align-items:flex-end}
.field input{flex:1}
button{padding:10px 14px;border:0;border-radius:8px;background:var(--pink);color:#1a0a14;font:500 14px system-ui;cursor:pointer;white-space:nowrap}
button.ghost{background:#f3f3f6;color:#2a2a30;border:1px solid var(--line)}
.ok{color:#1a8f54;font-size:12px;margin-left:6px}
.gate{text-align:center;padding:30px 0}
#app{display:none}
.soon{color:#55555f;font-size:13px}
a{color:var(--pink)}
</style>
</head>
<body>
<div class="wrap">
<h1>Record<b>God</b> · control room</h1>
<div class="sub"><a href="/">← home</a> · <a href="/store/">virtual store</a></div>
<div id="gate" class="card gate">
<h2>Sign in</h2>
<label>Admin token</label>
<div class="field">
<input id="tok" type="password" placeholder="RECORDGOD_TOKEN (from your .env)">
<button onclick="signin()">Enter</button>
</div>
<div id="gerr" class="soon" style="margin-top:10px"></div>
</div>
<div id="app">
<div class="card">
<h2>Credentials <span class="soon">— encrypted at rest, never shown back</span></h2>
<div id="secrets"></div>
</div>
<div class="card">
<h2>Connections <span class="soon">— save the keys above, then test</span></h2>
<div style="display:flex;gap:10px;align-items:center;margin:9px 0">
<button class="ghost" onclick="testConn('discogs')">Test Discogs</button><span id="c-discogs" class="soon"></span></div>
<div style="display:flex;gap:10px;align-items:center;margin:9px 0">
<button class="ghost" onclick="testConn('woo')">Test WooCommerce</button><span id="c-woo" class="soon"></span></div>
<div style="display:flex;gap:10px;align-items:center;margin:9px 0">
<button class="ghost" onclick="testConn('dealgod')">Test DealGod</button><span id="c-dealgod" class="soon"></span></div>
</div>
<div class="card">
<h2>Orders (WooCommerce)</h2>
<p class="soon">Once WooCommerce tests green, live orders land here — managed without wp-admin. (Next build.)</p>
</div>
</div>
</div>
<script>
const $ = s => document.querySelector(s);
let TOKEN = localStorage.getItem('rg_token') || '';
function hdr(){ return { 'Authorization': 'Bearer ' + TOKEN, 'Content-Type': 'application/json' }; }
async function signin(){
TOKEN = $('#tok').value.trim();
const r = await fetch('/settings/secrets', { headers: hdr() });
if(!r.ok){ $('#gerr').textContent = 'invalid token (' + r.status + ')'; return; }
localStorage.setItem('rg_token', TOKEN);
$('#gate').style.display = 'none'; $('#app').style.display = 'block';
renderSecrets(await r.json());
}
function renderSecrets(data){
const box = $('#secrets'); box.innerHTML = '';
data.fields.forEach(f => {
const wrap = document.createElement('div');
const status = f.set ? `<span class="ok">✓ set ${(f.updated_at||'').slice(0,10)}</span>` : '';
wrap.innerHTML = `<label>${f.label} ${status}</label>
<div class="field">
<input type="password" placeholder="${f.set ? '•••••• (replace to update)' : 'paste value'}" data-name="${f.name}">
<button class="ghost" data-save="${f.name}">Save</button>
</div>`;
box.appendChild(wrap);
});
box.querySelectorAll('[data-save]').forEach(btn => btn.onclick = () => save(btn.dataset.save, btn));
}
async function save(name, btn){
const input = document.querySelector(`input[data-name="${name}"]`);
const value = input.value.trim();
if(!value) return;
btn.textContent = '…';
const r = await fetch('/settings/secrets', { method:'POST', headers: hdr(), body: JSON.stringify({ name, value }) });
btn.textContent = r.ok ? 'Saved' : 'Error';
if(r.ok){ input.value=''; input.placeholder='•••••• (replace to update)';
btn.previousElementSibling; setTimeout(()=>btn.textContent='Save', 1500); }
}
async function testConn(svc){
const el = document.getElementById('c-'+svc); el.textContent='testing…'; el.style.color='#5f5f6c';
let r, d;
try { r = await fetch('/settings/test/'+svc, {method:'POST', headers:hdr()}); d = await r.json(); }
catch(e){ el.textContent='✕ network error'; el.style.color='#e66'; return; }
if(!r.ok){ el.textContent='✕ '+(d.detail||r.status); el.style.color='#b06a00'; return; }
if(d.connected){ el.textContent='✓ connected'+(d.as?(' as '+d.as):''); el.style.color='#1a8f54'; }
else { el.textContent='✕ '+(d.detail||('HTTP '+d.status)); el.style.color='#b06a00'; }
}
// auto-resume if a token is already stored
if(TOKEN){ $('#tok').value = TOKEN; signin(); }
</script>
</body>
</html>