diff options
| author | plutorocks <> | 2026-02-26 21:30:32 +0000 |
|---|---|---|
| committer | plutorocks <> | 2026-02-26 21:30:32 +0000 |
| commit | 3db298ec3eca0ed94cb7912f660df7dd1f4582e0 (patch) | |
| tree | d406e1d64aa10c8027bf4bf4a5914f1378fb495e /admin_page.go | |
Diffstat (limited to 'admin_page.go')
| -rw-r--r-- | admin_page.go | 152 |
1 files changed, 152 insertions, 0 deletions
diff --git a/admin_page.go b/admin_page.go new file mode 100644 index 0000000..b132c1e --- /dev/null +++ b/admin_page.go @@ -0,0 +1,152 @@ +// admin_page.go +package main + +import "net/http" + +const adminHTML = `<!doctype html> +<html> +<head> + <meta charset="utf-8" /> + <title>Bridge Admin</title> + <style> + body { font-family: system-ui, sans-serif; margin: 1rem; background:#111; color:#eee; } + h1 { font-size: 1.4rem; } + .section { margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid #333; } + label { display:block; margin-bottom:0.25rem; } + input, button { padding:0.4rem 0.6rem; margin:0.1rem 0; background:#222; border:1px solid #555; color:#eee; border-radius:4px; } + button { cursor:pointer; } + table { border-collapse: collapse; width: 100%; font-size:0.9rem; } + th, td { border:1px solid #333; padding:0.25rem 0.4rem; } + th { background:#222; } + #log { background:#000; border:1px solid #333; padding:0.5rem; height:300px; overflow-y:scroll; font-family:monospace; font-size:0.85rem; } + .revoked { opacity:0.5; text-decoration:line-through; } + </style> +</head> +<body> + <h1>Bridge Admin</h1> + + <div class="section"> + <h2>New Token</h2> + <form id="newTokenForm"> + <label for="name">Internal name (e.g. jeff)</label> + <input id="name" name="name" required /> + <button type="submit">Create token</button> + </form> + <div id="newTokenOutput"></div> + </div> + + <div class="section"> + <h2>Tokens</h2> + <button onclick="loadTokens()">Reload tokens</button> + <table> + <thead> + <tr><th>Name</th><th>Token</th><th>Revoked</th><th>Actions</th></tr> + </thead> + <tbody id="tokensBody"></tbody> + </table> + </div> + + <div class="section"> + <h2>Events (Chat + Location)</h2> + <div id="log"></div> + </div> + +<script> +let lastEventId = 0; + +async function loadTokens() { + const res = await fetch('/admin/api/tokens'); + if (!res.ok) return; + const tokens = await res.json(); + const tbody = document.getElementById('tokensBody'); + tbody.innerHTML = ''; + tokens.forEach(t => { + const tr = document.createElement('tr'); + if (t.revoked) tr.classList.add('revoked'); + const tdName = document.createElement('td'); + tdName.textContent = t.name; + const tdToken = document.createElement('td'); + tdToken.textContent = t.token; + const tdRev = document.createElement('td'); + tdRev.textContent = t.revoked ? 'yes' : 'no'; + const tdAct = document.createElement('td'); + if (!t.revoked) { + const btn = document.createElement('button'); + btn.textContent = 'Revoke'; + btn.onclick = () => revokeToken(t.token); + tdAct.appendChild(btn); + } + tr.appendChild(tdName); + tr.appendChild(tdToken); + tr.appendChild(tdRev); + tr.appendChild(tdAct); + tbody.appendChild(tr); + }); +} + +async function revokeToken(token) { + if (!confirm('Revoke this token?')) return; + await fetch('/admin/api/tokens/revoke', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ token }) + }); + loadTokens(); +} + +const form = document.getElementById('newTokenForm'); +form.addEventListener('submit', async (e) => { + e.preventDefault(); + const name = document.getElementById('name').value.trim(); + if (!name) return; + const res = await fetch('/admin/api/tokens', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name }) + }); + if (!res.ok) { + alert('Failed to create token'); + return; + } + const data = await res.json(); + document.getElementById('newTokenOutput').textContent = 'New token for ' + data.name + ': ' + data.token; + form.reset(); + loadTokens(); +}); + +async function pollEvents() { + try { + const res = await fetch('/admin/api/events?since=' + lastEventId); + if (!res.ok) return; + const events = await res.json(); + const logEl = document.getElementById('log'); + events.forEach(ev => { + if (ev.id > lastEventId) lastEventId = ev.id; + const line = document.createElement('div'); + const ts = new Date(ev.timestamp * 1000).toISOString(); + if (ev.type === 'chat') { + line.textContent = '[' + ts + '] [CHAT] ' + ev.fromNick + '(' + ev.fromInternal + '): ' + ev.message; + } else if (ev.type === 'location') { + line.textContent = '[' + ts + '] [LOC][' + ev.serverId + '] ' + ev.fromNick + '(' + ev.fromInternal + '): ' + + ' x=' + ev.x + ' y=' + ev.y + ' z=' + ev.z + ' dim=' + ev.dimension; + } else { + line.textContent = '[' + ts + '] [?] ' + JSON.stringify(ev); + } + logEl.appendChild(line); + logEl.scrollTop = logEl.scrollHeight; + }); + } catch (e) { + console.error('pollEvents error', e); + } +} + +loadTokens(); +setInterval(pollEvents, 2000); +</script> +</body> +</html>` + +func adminPageHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + _, _ = w.Write([]byte(adminHTML)) +}
\ No newline at end of file |
