function initAdmin(token){
const overlay = document.getElementById('authOverlay');
const authMsg = document.getElementById('authMsg');
const tokenInput = document.getElementById('tokenInput');
const setTokenBtn = document.getElementById('setTokenBtn');
const statusEl = document.getElementById('statuspill');
function showOverlay(msg){
if(authMsg && msg){ authMsg.textContent = msg; }
if(overlay){ overlay.classList.add('show'); }
if(setTokenBtn && tokenInput){
setTokenBtn.onclick = () => {
const t = (tokenInput.value||'').trim();
if(!t) return;
const url = new URL(location.href);
url.searchParams.set('token', t);
location.href = url.toString();
};
}
}
function hideOverlay(){ if(overlay){ overlay.classList.remove('show'); } }
function setStatus(txt, cls){ if(!statusEl) return; statusEl.textContent = txt; statusEl.className = 'statuspill ' + (cls||''); }
let state=null, since=0, presets=[], groups=[];
const rows = document.getElementById('rows');
const presetlist = document.getElementById('presetlist');
const pg_list = document.getElementById('pg_list');
const g_checks = document.getElementById('g_checks');
function openBoard(){ window.open('/board?token='+encodeURIComponent(token), '_blank'); }
window.openBoard = openBoard;
function iconImg(name){ return `
`; }
function typeChip(t){ return `${t}`; }
function row(a, idx){
const main = `
| ${idx+1} |
${a.name} |
${a.init} |
${a.hp} |
${a.ac}${a.reveal_ac?'':' (hidden)'} |
${typeChip(a.type)} |
|
|
`;
const active = (a.effects||[]);
const KNOWN_CONDITIONS = ['poisoned','prone','grappled','restrained','stunned','blinded','deafened','charmed','frightened','paralyzed','petrified','unconscious'];
const cond = `${
KNOWN_CONDITIONS.map(c=>``).join(' ')
} |
`;
return main + cond;
}
function render(){
document.getElementById('round').textContent = state.round;
rows.innerHTML = '';
(state.actors||[]).forEach((a, idx)=>{ rows.insertAdjacentHTML('beforeend', row(a, idx)); });
}
function renderPresets(){
presetlist.innerHTML = '';
presets.forEach(p=>{
const div = document.createElement('div'); div.className='preset';
if(p.avatar){ const img=document.createElement('img'); img.className='pav'; img.alt=p.name; img.src=(p.avatar.startsWith('http')||p.avatar.startsWith('/'))?p.avatar:('/avatars/'+encodeURIComponent(p.avatar)); div.appendChild(img); }
const span = document.createElement('span'); span.innerHTML = `${typeChip(p.type)} ${p.name}${p.ac? ' · AC '+p.ac: ''}${p.hp? ' · HP '+p.hp: ''}`; div.appendChild(span);
const addb = document.createElement('button'); addb.className='btn'; addb.textContent='➕ Add'; addb.onclick=()=>presetApply(p.id); div.appendChild(addb);
const delb = document.createElement('button'); delb.className='btn btn-danger'; delb.textContent='🗑'; delb.style.marginLeft='6px'; delb.onclick=()=>presetRemove(p.id); div.appendChild(delb);
presetlist.appendChild(div);
});
renderGroupChecks();
}
function renderGroupChecks(){
g_checks.innerHTML = '';
if(!presets.length){ g_checks.innerHTML = 'No presets yet.
'; return; }
presets.forEach(p=>{
const id = 'chk_'+p.id; const w=document.createElement('div');
w.innerHTML = ``;
g_checks.appendChild(w);
});
}
function renderGroups(){
pg_list.innerHTML = '';
groups.forEach(g=>{
const div = document.createElement('div'); div.className='preset';
const names = g.member_ids.map(id=>{ const p=presets.find(x=>x.id===id); return p? p.name : 'unknown'; }).join(', ');
const span = document.createElement('span'); span.innerHTML = `${g.name} (${g.member_ids.length} members) — ${names}`;
div.appendChild(span);
const addb = document.createElement('button'); addb.className='btn'; addb.textContent='➕ Add Group'; addb.onclick=()=>groupApply(g.id); div.appendChild(addb);
const delb = document.createElement('button'); delb.className='btn btn-danger'; delb.textContent='🗑'; delb.style.marginLeft='6px'; delb.onclick=()=>groupRemove(g.id); div.appendChild(delb);
pg_list.appendChild(div);
});
}
async function post(p, body){
const r = await fetch(p+`?token=${encodeURIComponent(token)}`, { method:'POST', headers:{'Content-Type':'application/json'}, body: body?JSON.stringify(body):null });
if(r.status===401){ setStatus('Unauthorized', 'bad'); showOverlay('Unauthorized: token missing or invalid. Fix the token and reload.'); return false; }
if(!r.ok){ const t = await r.text(); alert('Error '+r.status+' '+t); setStatus('Reconnecting…','warn'); }
return r.ok;
}
async function get(p){
const r = await fetch(p+`?token=${encodeURIComponent(token)}`);
if(r.status===401){ setStatus('Unauthorized', 'bad'); showOverlay('Unauthorized: token missing or invalid. Fix the token and reload.'); return null; }
if(!r.ok){ const t = await r.text(); alert('Error '+r.status+' '+t); setStatus('Reconnecting…','warn'); return null; }
return r.json();
}
// Upload helpers
async function uploadAvatar(fileInputId, targetInputId, previewId){
const fileEl = document.getElementById(fileInputId);
if(fileEl && fileEl.files && fileEl.files.length){
await uploadAvatarFile(fileEl.files[0], targetInputId, previewId);
fileEl.value = '';
} else { alert('Select or drop a file first.'); }
}
async function uploadAvatarFile(file, targetInputId, previewId){
const fd = new FormData(); fd.append('file', file);
const r = await fetch('/api/upload_avatar?token='+encodeURIComponent(token), { method: 'POST', body: fd });
if(r.status===401){ setStatus('Unauthorized', 'bad'); showOverlay('Unauthorized: token missing or invalid.'); return; }
if(!r.ok){ const t = await r.text(); alert('Upload error '+r.status+' '+t); setStatus('Reconnecting…','warn'); return; }
const j = await r.json();
document.getElementById(targetInputId).value = j.filename;
if(previewId){ setPreview(j.filename, previewId); }
}
function setPreview(val, previewId){
const img = document.getElementById(previewId);
if(!img) return; if(!val){ img.src=''; img.style.opacity=.3; return; }
const src = (val.startsWith('http')||val.startsWith('/')) ? val : ('/avatars/'+encodeURIComponent(val));
img.src = src; img.style.opacity=1;
}
function bindPreview(inputId, previewId){ const el = document.getElementById(inputId); el.addEventListener('input', ()=> setPreview(el.value.trim(), previewId)); setPreview(el.value.trim(), previewId); }
function makeDropzone(zoneId, targetInputId, previewId){
const dz = document.getElementById(zoneId);
['dragenter','dragover'].forEach(ev=> dz.addEventListener(ev, e=>{ e.preventDefault(); dz.classList.add('drag'); }));
['dragleave','drop'].forEach(ev=> dz.addEventListener(ev, e=>{ e.preventDefault(); dz.classList.remove('drag'); }));
dz.addEventListener('drop', async (e)=>{ const files = e.dataTransfer.files; if(!files || !files.length) return; await uploadAvatarFile(files[0], targetInputId, previewId); });
}
// Mutations
async function add(){
const name = document.getElementById('name').value.trim();
const init = parseFloat(document.getElementById('init').value||'0');
const hp = parseInt(document.getElementById('hp').value||'0');
const ac = parseInt(document.getElementById('ac').value||'0');
const type = document.getElementById('type').value;
const note = document.getElementById('note').value.trim();
const avatar = document.getElementById('avatar').value.trim();
await post('/api/add', { name, init, hp, ac, type, note, avatar });
}
async function clearAll(){ await post('/api/clear'); }
async function toggleVisible(){ await post('/api/toggle_visible'); }
async function next(){ await post('/api/next'); }
async function prev(){ await post('/api/prev'); }
async function toggle(id, field){ await post('/api/update', { id, [field]: 'toggle' }); }
async function toggleEffect(id, effect){ await post('/api/toggle_effect', { id, effect }); }
async function clearEffects(id){ await post('/api/clear_effects', { id }); }
async function removeA(id){ await post('/api/remove', { id }); }
async function setDeadMode(mode){ await post('/api/dead_mode', { mode }); updateDeadSeg(mode); }
function updateDeadSeg(mode){ const seg = document.getElementById('deadseg'); if(!seg) return; seg.querySelectorAll('button[data-mode]').forEach(b=>{ b.classList.toggle('on', b.getAttribute('data-mode')===mode); }); }
// Presets
async function presetAdd(){
const name = document.getElementById('p_name').value.trim();
const type = document.getElementById('p_type').value;
const hp = parseInt(document.getElementById('p_hp').value||'0');
const ac = parseInt(document.getElementById('p_ac').value||'0');
const note = document.getElementById('p_note').value.trim();
const avatar = document.getElementById('p_avatar').value.trim();
await post('/api/preset/add', { name, type, hp, ac, note, avatar });
await loadPresets(); await loadPresetGroups();
}
async function presetRemove(id){ await post('/api/preset/remove', { id }); await loadPresets(); await loadPresetGroups(); }
async function presetApply(id){
const init = prompt('Initiative roll for this actor?', '10');
const p = parseFloat(init||'0');
await post('/api/preset/apply', { id, init: p });
}
// Groups
async function groupAdd(){
const name = document.getElementById('g_name').value.trim();
const member_ids = Array.from(g_checks.querySelectorAll('input[type=checkbox]:checked')).map(x=>x.getAttribute('data-id'));
if(!name){ alert('Name required'); return; }
if(!member_ids.length){ alert('Select at least one preset'); return; }
await post('/api/preset/group/add', { name, member_ids });
document.getElementById('g_name').value='';
g_checks.querySelectorAll('input[type=checkbox]').forEach(c=> c.checked=false);
await loadPresetGroups();
}
async function groupRemove(id){ await post('/api/preset/group/remove', { id }); await loadPresetGroups(); }
async function groupApply(id){
const r = await fetch('/api/preset_groups?token='+encodeURIComponent(token));
if(r.status===401){ setStatus('Unauthorized', 'bad'); showOverlay('Unauthorized: token missing or invalid.'); return; }
const all = await r.json(); groups = all.items||[];
const g = groups.find(x=>x.id===id); if(!g){ alert('Group not found'); return; }
const mode = (prompt('Group add mode: single | list | per | step','single')||'single').trim().toLowerCase();
let payload = { id };
if(mode==='list'){
const raw = prompt('Comma-separated initiatives in group order','12,11,10');
if(raw && raw.trim().length){ payload.inits = raw.split(',').map(s=>parseFloat((s||'0').trim())); }
} else if(mode==='per'){
const arr = [];
g.member_ids.forEach((pid, i)=>{
const p = presets.find(x=>x.id===pid); const lbl = p? p.name : ('Member '+(i+1));
const v = prompt('Initiative for '+lbl,'10'); arr.push(parseFloat(v||'0'));
});
payload.inits = arr;
} else if(mode==='step'){
const base = parseFloat(prompt('Base initiative for first member','12')||'12');
const step = parseFloat(prompt('Step per next member (e.g. -1 for descending)','-1')||'-1');
payload.inits = g.member_ids.map((_,i)=> base + i*step);
} else {
const v = prompt('Single initiative value for all members','12');
payload.init = parseFloat(v||'12');
}
await post('/api/preset/group/apply', payload);
}
async function loadPresets(){ const r = await get('/api/presets'); if(r) { presets = r.items||[]; renderPresets(); } }
async function loadPresetGroups(){ const r = await get('/api/preset_groups'); if(r){ groups = r.items||[]; renderGroups(); } }
async function poll(){
try{
const r = await fetch(`/api/state?since=${since}&token=${encodeURIComponent(token)}`);
if(r.status===401){ setStatus('Unauthorized', 'bad'); showOverlay('Unauthorized: token missing or invalid. Fix the token and reload.'); setTimeout(poll, 1500); return; }
if(r.ok){ hideOverlay(); setStatus('Connected','ok'); state = await r.json(); since = state.version; render(); updateDeadSeg(state.dead_mode||'normal'); }
else { setStatus('Reconnecting…','warn'); }
} catch(e){ setStatus('Reconnecting…','warn'); }
setTimeout(poll, 300);
}
// init
window.clearAll = clearAll;
window.toggleVisible = toggleVisible;
window.next = next;
window.prev = prev;
window.toggle = toggle;
window.toggleEffect = toggleEffect;
window.clearEffects = clearEffects;
window.removeA = removeA;
window.setDeadMode = setDeadMode;
window.presetAdd = presetAdd;
window.presetRemove = presetRemove;
window.presetApply = presetApply;
window.groupAdd = groupAdd;
window.groupRemove = groupRemove;
window.groupApply = groupApply;
bindPreview('avatar','avatar_preview');
bindPreview('p_avatar','p_avatar_preview');
makeDropzone('avatar_drop','avatar','avatar_preview');
makeDropzone('p_avatar_drop','p_avatar','p_avatar_preview');
loadPresets();
loadPresetGroups();
setStatus('Connecting…','warn');
poll();
}