overte-www/tablet/poker-admin.html
2026-03-19 02:36:39 +00:00

552 lines
16 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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 + "...");
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() {
setStatus("Table " + id + " deleted", "ok");
loadTables();
})
.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;
}
} 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>