overte-www/tablet/poker-admin.html

561 lines
16 KiB
HTML
Raw Permalink Normal View History

2026-03-19 02:01:23 +00:00
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>POKER ADMIN</title>
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Cinzel:wght@400;600&display=swap" rel="stylesheet">
<style>
:root {
--bg: #0e0e1a;
--panel: #13132a;
--border: #2a2a4a;
--gold: #ffd700;
--gold-dim: #a08800;
--text: #c8c8e0;
--muted: #555570;
--red: #ff4444;
--green: #44cc88;
--mono: 'Share Tech Mono', monospace;
--serif: 'Cinzel', serif;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--text);
font-family: var(--mono);
min-height: 100vh;
display: flex;
flex-direction: column;
overflow-x: hidden;
}
body::before {
content: '';
position: fixed;
inset: 0;
background-image:
linear-gradient(var(--border) 1px, transparent 1px),
linear-gradient(90deg, var(--border) 1px, transparent 1px);
background-size: 40px 40px;
opacity: 0.25;
pointer-events: none;
}
header {
position: sticky;
top: 0;
z-index: 10;
background: var(--bg);
border-bottom: 1px solid var(--border);
padding: 12px 16px 10px;
display: flex;
align-items: center;
gap: 12px;
}
.app-title {
font-family: var(--serif);
font-size: 13px;
letter-spacing: 4px;
color: var(--gold);
flex: 1;
}
.user-badge {
font-size: 9px;
color: var(--muted);
letter-spacing: 2px;
text-transform: uppercase;
}
#status-bar {
font-size: 9px;
color: var(--muted);
text-align: center;
padding: 4px 16px;
border-bottom: 1px solid var(--border);
letter-spacing: 1px;
min-height: 20px;
transition: color 0.3s;
}
#status-bar.error { color: var(--red); }
#status-bar.ok { color: var(--green); }
/* ── tabs ── */
.tabs {
display: flex;
border-bottom: 1px solid var(--border);
}
.tab {
flex: 1;
background: transparent;
border: none;
border-bottom: 2px solid transparent;
color: var(--muted);
font-family: var(--mono);
font-size: 9px;
letter-spacing: 2px;
text-transform: uppercase;
padding: 10px;
cursor: pointer;
transition: color 0.15s, border-color 0.15s;
}
.tab:hover { color: var(--text); }
.tab.active {
color: var(--gold);
border-bottom-color: var(--gold);
}
/* ── panels ── */
.panel {
display: none;
flex-direction: column;
flex: 1;
padding: 16px;
gap: 12px;
}
.panel.active { display: flex; }
/* ── form elements ── */
.field {
display: flex;
flex-direction: column;
gap: 4px;
}
.field label {
font-size: 9px;
color: var(--muted);
letter-spacing: 2px;
text-transform: uppercase;
}
.field input {
background: var(--panel);
border: 1px solid var(--border);
color: var(--text);
font-family: var(--mono);
font-size: 12px;
padding: 8px 10px;
outline: none;
transition: border-color 0.15s;
}
.field input:focus { border-color: var(--gold-dim); }
.field-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.btn {
background: transparent;
border: 1px solid var(--gold-dim);
color: var(--gold);
font-family: var(--mono);
font-size: 10px;
letter-spacing: 2px;
text-transform: uppercase;
padding: 10px;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
width: 100%;
}
.btn:hover { background: rgba(255,215,0,0.08); border-color: var(--gold); }
.btn.danger {
border-color: #662222;
color: var(--red);
}
.btn.danger:hover { background: rgba(255,68,68,0.08); border-color: var(--red); }
.btn:disabled { opacity: 0.4; cursor: default; }
/* ── table list ── */
.table-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.table-card {
background: var(--panel);
border: 1px solid var(--border);
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 6px;
}
.table-card-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.table-name {
font-family: var(--serif);
font-size: 11px;
color: #e8e8ff;
letter-spacing: 1px;
}
.table-id {
font-size: 8px;
color: var(--muted);
letter-spacing: 1px;
}
.table-meta {
font-size: 9px;
color: var(--muted);
letter-spacing: 1px;
}
.table-meta span { color: var(--gold-dim); }
.table-actions {
display: flex;
gap: 6px;
margin-top: 4px;
}
.table-actions .btn {
font-size: 8px;
padding: 6px 8px;
}
.section-header {
font-family: var(--serif);
font-size: 9px;
letter-spacing: 3px;
color: var(--muted);
text-transform: uppercase;
padding-bottom: 8px;
border-bottom: 1px solid var(--border);
}
.divider {
height: 1px;
background: var(--border);
}
#no-tables {
font-size: 10px;
color: var(--muted);
text-align: center;
padding: 24px;
letter-spacing: 1px;
}
</style>
</head>
<body>
<header>
<div class="app-title">POKER ADMIN</div>
<div class="user-badge" id="user-badge"></div>
</header>
<div id="status-bar">Initialising...</div>
<div class="tabs">
<button class="tab active" onclick="switchTab('create')">Create</button>
<button class="tab" onclick="switchTab('manage')">Manage</button>
</div>
<!-- ── Create Tab ── -->
<div class="panel active" id="tab-create">
<div class="section-header">New Table</div>
<div class="field">
<label>Table ID (slug)</label>
<input type="text" id="f-id" placeholder="main" value="main">
</div>
<div class="field">
<label>Display Name</label>
<input type="text" id="f-name" placeholder="Main Table">
</div>
<div class="field-row">
<div class="field">
<label>Seat Count</label>
<input type="number" id="f-seats" value="7" min="2" max="9">
</div>
<div class="field">
<label>Turn Timer (s)</label>
<input type="number" id="f-timer" value="30" min="10" max="120">
</div>
</div>
<div class="field-row">
<div class="field">
<label>Small Blind</label>
<input type="number" id="f-sb" value="5" min="1">
</div>
<div class="field">
<label>Big Blind</label>
<input type="number" id="f-bb" value="10" min="2">
</div>
</div>
<div class="field-row">
<div class="field">
<label>Min Buy-in</label>
<input type="number" id="f-minbuy" value="100" min="1">
</div>
<div class="field">
<label>Max Buy-in</label>
<input type="number" id="f-maxbuy" value="1000" min="1">
</div>
</div>
<div class="divider"></div>
<button class="btn" id="btn-create" onclick="createTable()">Spawn Table at Avatar Position</button>
</div>
<!-- ── Manage Tab ── -->
<div class="panel" id="tab-manage">
<div class="section-header">Tables</div>
<div class="table-list" id="table-list">
<div id="no-tables">Loading...</div>
</div>
<button class="btn" onclick="loadTables()" style="margin-top:auto">Refresh</button>
</div>
<script>
var API_BASE = "https://wizards.cyou/api";
var POKER_BASE = "https://wizards.cyou/poker";
var state = {
username: null,
token: null,
position: null,
rotation: null,
};
// ── tabs ─────────────────────────────────────────────────────
function switchTab(name) {
document.querySelectorAll(".tab").forEach(function(t) { t.classList.remove("active"); });
document.querySelectorAll(".panel").forEach(function(p) { p.classList.remove("active"); });
document.querySelector("[onclick=\"switchTab('" + name + "')\"]").classList.add("active");
document.getElementById("tab-" + name).classList.add("active");
if (name === "manage") loadTables();
}
// ── status ───────────────────────────────────────────────────
function setStatus(msg, cls) {
var el = document.getElementById("status-bar");
el.textContent = msg;
el.className = cls || "";
}
// ── session ──────────────────────────────────────────────────
function ensureSession(cb) {
if (state.token) { cb(null, state.token); return; }
fetch(API_BASE + "/session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: state.username }),
})
.then(function(r) {
if (!r.ok) throw new Error("Session failed (" + r.status + ")");
return r.json();
})
.then(function(d) {
state.token = d.token;
cb(null, state.token);
})
.catch(function(e) { cb(e.message); });
}
// ── create table ─────────────────────────────────────────────
function createTable() {
var id = document.getElementById("f-id").value.trim();
var name = document.getElementById("f-name").value.trim();
var seats = parseInt(document.getElementById("f-seats").value);
var timer = parseInt(document.getElementById("f-timer").value);
var sb = parseInt(document.getElementById("f-sb").value);
var bb = parseInt(document.getElementById("f-bb").value);
var minbuy = parseInt(document.getElementById("f-minbuy").value);
var maxbuy = parseInt(document.getElementById("f-maxbuy").value);
if (!id || !name) { setStatus("ID and Name are required", "error"); return; }
if (bb !== sb * 2) { setStatus("Big blind must be exactly 2x small blind", "error"); return; }
if (minbuy < bb * 10) { setStatus("Min buy-in must be at least 10x big blind (" + (bb*10) + ")", "error"); return; }
if (maxbuy < minbuy) { setStatus("Max buy-in must be >= min buy-in", "error"); return; }
var btn = document.getElementById("btn-create");
btn.disabled = true;
setStatus("Getting session...");
ensureSession(function(err) {
if (err) { setStatus(err, "error"); btn.disabled = false; return; }
// Request a fresh position snapshot from the tablet script
setStatus("Fetching avatar position...");
EventBridge.emitWebEvent(JSON.stringify({ type: "getPosition" }));
// Store config to use once position arrives
state.pendingCreate = {
id: id, name: name, seatCount: seats, turnTimer: timer,
smallBlind: sb, bigBlind: bb, minBuyin: minbuy, maxBuyin: maxbuy,
};
btn.disabled = false;
});
}
function doCreateTable(cfg, position, rotation) {
var btn = document.getElementById("btn-create");
btn.disabled = true;
setStatus("Creating table...");
fetch(POKER_BASE + "/admin/tables", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + state.token,
},
body: JSON.stringify(cfg),
})
.then(function(r) {
if (!r.ok) return r.json().then(function(e) { throw new Error(e.error || r.status); });
return r.json();
})
.then(function(d) {
setStatus("Table created! Spawning entities...", "ok");
// Tell the tablet script to spawn the in-world entities
EventBridge.emitWebEvent(JSON.stringify({
type: "spawnSeats",
pokerID: cfg.id,
seatCount: cfg.seatCount,
position: position,
rotation: rotation,
}));
})
.catch(function(e) {
setStatus(e.message, "error");
btn.disabled = false;
});
}
// ── manage tables ─────────────────────────────────────────────
function loadTables() {
setStatus("Loading tables...");
fetch(POKER_BASE + "/tables")
.then(function(r) { return r.json(); })
.then(function(d) {
renderTables(d.tables || []);
setStatus(d.tables.length + " table(s) loaded", "ok");
})
.catch(function() { setStatus("Could not load tables", "error"); });
}
function renderTables(tables) {
var list = document.getElementById("table-list");
if (tables.length === 0) {
list.innerHTML = '<div id="no-tables">No tables yet</div>';
return;
}
list.innerHTML = "";
tables.forEach(function(t) {
var cfg = t.config || t;
var card = document.createElement("div");
card.className = "table-card";
card.innerHTML =
'<div class="table-card-header">' +
'<div class="table-name">' + cfg.name + '</div>' +
'<div class="table-id">' + cfg.id + '</div>' +
'</div>' +
'<div class="table-meta">' +
'<span>' + cfg.seatCount + '</span> seats · ' +
'blinds <span>' + cfg.smallBlind + '/' + cfg.bigBlind + '</span> · ' +
'buy-in <span>' + cfg.minBuyin + '' + cfg.maxBuyin + '</span>' +
'</div>' +
'<div class="table-actions">' +
'<button class="btn danger" onclick="deleteTable(\'' + cfg.id + '\')">Delete</button>' +
'</div>';
list.appendChild(card);
});
}
function deleteTable(id) {
ensureSession(function(err) {
if (err) { setStatus(err, "error"); return; }
setStatus("Deleting " + id + "...");
2026-03-19 03:17:54 +00:00
// Delete server-side first
2026-03-19 02:01:23 +00:00
fetch(POKER_BASE + "/admin/tables/" + id, {
method: "DELETE",
headers: { "Authorization": "Bearer " + state.token },
})
.then(function(r) {
if (!r.ok) return r.json().then(function(e) { throw new Error(e.error || r.status); });
return r.json();
})
.then(function() {
2026-03-19 03:17:54 +00:00
// Then clean up in-world entities
EventBridge.emitWebEvent(JSON.stringify({
type: "deleteTable",
pokerID: id,
}));
2026-03-19 02:01:23 +00:00
})
.catch(function(e) { setStatus(e.message, "error"); });
});
}
// ── EventBridge ──────────────────────────────────────────────
function init() {
if (typeof EventBridge !== "undefined") {
EventBridge.scriptEventReceived.connect(function(data) {
try {
var msg = JSON.parse(data);
if (msg.type === "init") {
state.username = msg.username;
state.position = msg.position;
state.rotation = msg.rotation;
document.getElementById("user-badge").textContent = msg.username || "unknown";
setStatus("Ready");
if (msg.username !== "nak") {
setStatus("Admin access required", "error");
document.getElementById("btn-create").disabled = true;
}
} else if (msg.type === "position") {
state.position = msg.position;
state.rotation = msg.rotation;
if (state.pendingCreate) {
var cfg = state.pendingCreate;
state.pendingCreate = null;
doCreateTable(cfg, state.position, state.rotation);
}
} else if (msg.type === "spawnComplete") {
setStatus("Table and seats spawned!", "ok");
document.getElementById("btn-create").disabled = false;
2026-03-19 03:17:54 +00:00
} else if (msg.type === "deleteEntitiesComplete") {
var detail = msg.found ? "Table and entities deleted" : "Table deleted (no entities found)";
setStatus(detail, "ok");
loadTables();
2026-03-19 02:01:23 +00:00
}
} catch(e) {
print("poker-admin event error: " + e);
}
});
EventBridge.emitWebEvent(JSON.stringify({ type: "ready" }));
} else {
setStatus("EventBridge not available (dev mode)", "error");
}
}
window.onload = init;
</script>
</body>
</html>