(async function () {
const pingEl = document.querySelector('[data-player-ping]');
const statusEl = document.querySelector('[data-player-status]');
const worldEl = document.querySelector('[data-player-world]');
const gamemodeEl = document.querySelector('[data-player-gamemode]');
if (!pingEl) return;
const uuid = pingEl.getAttribute('data-uuid');
if (!uuid) return;
const { renderItem } = await import('/assets/blockrenderer.js');
// ---- Helpers ----
function escapeHtml(s) {
if (s == null) return '';
return String(s)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function toTitle(snake) {
if (!snake) return '';
return snake.toLowerCase().replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
}
const ROMAN = ['', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X'];
function toRoman(n) {
return ROMAN[n] || String(n);
}
function pingColor(ping) {
if (ping < 80) return 'text-success';
if (ping < 200) return 'text-warning';
return 'text-destructive';
}
// ---- Live header (ping/world/gamemode/status) ----
function setOffline() {
pingEl.textContent = '—';
pingEl.classList.remove('text-success', 'text-warning', 'text-destructive');
if (statusEl) {
statusEl.textContent = 'offline';
statusEl.classList.remove('text-success');
statusEl.classList.add('text-muted-foreground');
}
if (worldEl) worldEl.textContent = '—';
if (gamemodeEl) gamemodeEl.textContent = '—';
}
function setOnline(p) {
pingEl.textContent = (p.ping | 0) + 'ms';
pingEl.classList.remove('text-success', 'text-warning', 'text-destructive');
pingEl.classList.add(pingColor(p.ping));
if (statusEl) {
statusEl.textContent = 'online';
statusEl.classList.remove('text-muted-foreground');
statusEl.classList.add('text-success');
}
if (worldEl) worldEl.textContent = p.world || '—';
if (gamemodeEl) gamemodeEl.textContent = p.gamemode ? p.gamemode.toLowerCase() : '—';
}
function handle(state) {
const players = Array.isArray(state.players) ? state.players : [];
const match = players.find(p => p.uuid === uuid);
if (match) setOnline(match);
else setOffline();
}
const staffSrc = new EventSource('/api/players/stream/staff');
staffSrc.addEventListener('message', (evt) => {
try { handle(JSON.parse(evt.data)); }
catch (e) {}
});
// ---- Action dialog wiring ----
const dialog = document.getElementById('action-dialog');
const form = document.getElementById('action-form');
if (dialog && form) {
const actionInput = form.querySelector('[data-action-input]');
const actionLabel = form.querySelector('[data-action-label]');
const durationField = form.querySelector('[data-duration-field]');
const reasonField = form.querySelector('[data-reason-field]');
const reasonInput = form.querySelector('input[name="reason"]');
const slotInput = form.querySelector('[data-slot-input]');
const actionDescription = form.querySelector('[data-action-description]');
document.querySelectorAll('[data-admin-action]').forEach(btn => {
btn.addEventListener('click', () => {
const action = btn.getAttribute('data-admin-action');
const isTemp = btn.getAttribute('data-admin-temp') === 'true';
const noReason = btn.getAttribute('data-admin-no-reason') === 'true';
const selectedRequired = btn.getAttribute('data-selected-required') === 'true';
if (selectedRequired && !selectedKey) return;
actionInput.value = action;
actionLabel.textContent = action.replace(/-/g, ' ');
if (slotInput) slotInput.value = selectedRequired ? selectedKey : '';
durationField.hidden = !isTemp;
durationField.querySelector('select').disabled = !isTemp;
if (reasonField) reasonField.hidden = noReason;
if (reasonInput) {
reasonInput.disabled = noReason;
reasonInput.required = !noReason;
reasonInput.value = '';
}
if (actionDescription) {
const target = 'Target: ' + escapeHtml(document.querySelector('h1')?.textContent || 'player') + '';
actionDescription.innerHTML = selectedRequired
? target + '
Slot: ' + escapeHtml(selectedKey) + ''
: target;
}
if (typeof dialog.showModal === 'function') dialog.showModal();
else dialog.setAttribute('open', '');
if (!noReason && reasonInput) setTimeout(() => reasonInput.focus(), 0);
});
});
form.querySelectorAll('[data-dialog-cancel]').forEach(btn => {
btn.addEventListener('click', () => dialog.close());
});
}
// ---- Live inventory ----
const invRoot = document.getElementById('inv-root');
if (!invRoot) return;
// Latest inventory snapshot, used by the click handler.
let lastInv = null;
// Slot currently rendered in the detail panel (key like "storage-5"); kept across re-renders so the highlight survives data refreshes.
let selectedKey = null;
function updateInventoryActionButtons() {
const selectedItem = getItemBySlotKey(lastInv, selectedKey);
const enabled = !!(lastInv && lastInv.online && selectedKey && selectedItem);
document.querySelectorAll('[data-selected-required]').forEach(btn => {
btn.disabled = !enabled;
if (enabled) btn.removeAttribute('disabled');
else btn.setAttribute('disabled', '');
btn.title = enabled ? 'Clear ' + selectedKey : 'Select an occupied inventory slot first';
});
}
function renderDurabilityBar(item) {
if (!item.maxDamage) return '';
const damage = item.damage || 0;
const remaining = (item.maxDamage - damage) / item.maxDamage;
if (remaining >= 0.999) return '';
const cls = remaining > 0.5 ? 'bg-success' : remaining > 0.25 ? 'bg-warning' : 'bg-destructive';
const pct = Math.max(0, Math.min(100, remaining * 100));
return `
Main
Armor
Offhand
${renderSlot(inv.offhand, 'offhand')}${safeName}
` : ''}${safeType}
Count: ${item.amount}
Lore
Enchantments
Durability
Tags
Plugin NBT keys
NBT
${escapeHtml(item.nbt)}
Player is offline.
`; updateInventoryActionButtons(); return; } invRoot.innerHTML = `