- 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>
123 lines
5.5 KiB
HTML
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>
|