javascript:(async()=>{const match=location.href.match(/^https:\/\/www\.rollspel\.nu\/threads\/([^/?#]+?\.\d+)(?:[/?#]|$)/i);if(!match)return alert("Öppna bookmarkleten i en tråd på rollspel.nu.");const slug=match[1],U=`https://www.rollspel.nu/threads/${slug}/`,R=/\bröst\s*:\s*/i;const D=new DOMParser(),tagVotes=[],docs=[];const $=s=>document.querySelector(s),$$=s=>[...document.querySelectorAll(s)];const esc=s=>String(s??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");const fmt=t=>t?new Date(t).toLocaleString("sv-SE",{dateStyle:"short",timeStyle:"short"}):"–";const sorted=a=>a.slice().sort((x,y)=>(+new Date(x.ts)-+new Date(y.ts))||(Number(x.post||0)-Number(y.post||0)));const latest=a=>{const m={};sorted(a).forEach(v=>m[v.from]=v);return Object.values(m)};const csv=s=>`"${String(s??"").replace(/"/g,'""')}"`;try{for(let page=1;page<=500;page++){const r=await fetch(page===1?U:`${U}page-${page}`,{credentials:"include",cache:"no-store"});if(!r.ok)throw Error(`Sida ${page} gav HTTP ${r.status}`);const doc=D.parseFromString(await r.text(),"text/html");docs.push([page,doc]);const max=Number(doc.querySelector("input.js-pageJumpPage")?.max||1);if(page>=max&&!doc.querySelector(".pageNav-jump--next"))break;}}catch(e){return alert("Kunde inte läsa tråden: "+e.message)}function fragments(html){return html.replace(/<br\s*\/?>|<\/p\s*>|<\/li\s*>/gi,"\n").split("\n").map(s=>s.trim()).filter(Boolean);}function taggedTarget(fragment){const box=document.createElement("div");box.innerHTML=fragment;const walker=document.createTreeWalker(box,NodeFilter.SHOW_TEXT|NodeFilter.SHOW_ELEMENT);let seen=false,buffer="";while(walker.nextNode()){const n=walker.currentNode;if(n.nodeType===Node.TEXT_NODE){buffer+=n.nodeValue||"";if(!seen&&R.test(buffer))seen=true;if(buffer.length>300)buffer=buffer.slice(-300);}else if(seen&&n.nodeType===Node.ELEMENT_NODE){const u=n.getAttribute?.("data-username");if(u)return u.replace(/^@/,"").trim();}}return "";}for(const [page,doc] of docs){[...doc.querySelectorAll("article[data-author]")].forEach((post,index)=>{if(page===1&&index===0)return;const from=(post.getAttribute("data-author")||"").trim();const pid=(post.id||"").replace("js-post-","").trim();const ts=post.querySelector("time.u-dt")?.getAttribute("datetime")||"";let msg=post.querySelector(".message-content");if(!from||!pid||!msg)return;msg=msg.cloneNode(true);msg.querySelectorAll("blockquote,.bbCodeBlock--quote,.bbCodeBlock--unfurl,[data-unfurl='true']").forEach(x=>x.remove());for(const fragment of fragments(msg.innerHTML)){const tmp=document.createElement("div");tmp.innerHTML=fragment;const text=(tmp.textContent||"").replace(/\s+/g," ").trim();const found=R.exec(text);R.lastIndex=0;if(!found)continue;const tagged=taggedTarget(fragment);if(tagged)tagVotes.push({from,to:tagged,ts,post:pid,page});}});}tagVotes.splice(0,tagVotes.length,...sorted(tagVotes));document.title="Varulv Rösträknare";document.head.innerHTML=`<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>body{font-family:Arial,sans-serif;margin:2em;background:#f9f9f9;color:#333}label,button,select,input{margin:8px 10px 8px 0}select,button,input{padding:6px}button{cursor:pointer}table{border-collapse:collapse;width:100%;margin-top:12px;background:#fff;border:1px solid #ccc}th,td{padding:8px 10px;border:1px solid #ccc;text-align:center;vertical-align:top}th{background:#3c8dbc;color:#fff;cursor:pointer}.summary{font-weight:bold;font-size:18px;margin:12px 0}.row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:8px 0}.small{font-size:12px;opacity:.8}details{margin:12px 0;background:#fff;border:1px solid #ccc;padding:10px}summary{font-weight:bold;cursor:pointer}.manual{display:flex;flex-wrap:wrap;gap:8px 16px}.manual label{display:inline-flex;align-items:center;gap:6px;margin:0}.manual input{margin:0}#chart{max-width:620px;width:100%;height:auto}#slider{padding:0;min-width:260px}@media(max-width:700px){body{margin:10px}th,td{padding:5px;font-size:12px}}</style>`;document.body.innerHTML=`<h1>🐺 Varulv Rösträknare</h1><div class="small">Källa: <a target="_blank" rel="noopener" href="${esc(U)}">${esc(U)}</a></div><div id="status" class="small"></div><button id="exportBtn">Exportera CSV</button><details open><summary>Inställningar</summary><label><input type="radio" name="view" value="latest" checked> Endast senaste röst</label><label><input type="radio" name="view" value="all"> Alla röster</label><br><label>Filter per spelare <select id="filter"></select></label><label>Färger <select id="colorMode"><option value="auto">Auto</option><option value="bw">Svartvitt</option><option value="manual">Manuellt</option></select></label><div id="manual" class="manual" hidden></div><div class="row"><button id="play">▶ Animera röster</button><label>Hastighet <input id="delay" type="number" value="200" min="0" style="width:65px"> ms</label></div><div class="row"><label for="slider">Visa röster fram till:</label><input id="slider" type="range" min="0" max="0" value="0" list="sliderTicks"><datalist id="sliderTicks"></datalist><span id="sliderLabel">–</span></div></details><div id="summary" class="summary"></div><canvas id="chart" width="620" height="260"></canvas><h2>Röster</h2><table id="table"><thead><tr><th data-sort="from">Röstgivare</th><th>Röst</th><th data-sort="ts">Tidpunkt</th><th>Riskerar att åka ut</th><th>Därefter</th></tr></thead><tbody></tbody></table>`;const S={votes:tagVotes,players:[],colors:{},manual:{},colorMode:"auto",filter:"",sort:"",index:null,limit:null,timeline:[],timer:null};const L={status:$("#status"),exp:$("#exportBtn"),views:$$('input[name="view"]'),filter:$("#filter"),colorMode:$("#colorMode"),manual:$("#manual"),play:$("#play"),delay:$("#delay"),slider:$("#slider"),ticks:$("#sliderTicks"),sliderLabel:$("#sliderLabel"),summary:$("#summary"),tbody:$("#table tbody"),ths:$$("#table th"),canvas:$("#chart")};const view=()=>L.views.find(x=>x.checked)?.value||"latest";const color=n=>S.colorMode==="bw"?"#000000":S.colorMode==="manual"?(S.manual[n]||S.colors[n]||"#000000"):(S.colors[n]||"#000000");function makeColors(){const a=[...new Set(S.players)].sort((x,y)=>x.localeCompare(y,"sv"));S.colors={};a.forEach((n,i)=>S.colors[n]=`hsl(${Math.round(i*360/Math.max(1,a.length))},70%,42%)`)}function fillControls(){S.players=[...new Set(S.votes.flatMap(v=>[v.from,v.to]))];makeColors();L.filter.innerHTML='<option value="">Alla</option>';S.players.slice().sort((a,b)=>a.localeCompare(b,"sv")).forEach(n=>{const o=document.createElement("option");o.value=o.textContent=n;o.style.color=color(n);o.style.fontWeight="bold";L.filter.append(o)});if(!S.players.includes(S.filter))S.filter="";L.filter.value=S.filter;L.manual.hidden=S.colorMode!=="manual";L.manual.innerHTML="";if(S.colorMode==="manual")S.players.slice().sort((a,b)=>a.localeCompare(b,"sv")).forEach(n=>{S.manual[n]||=S.colors[n];const l=document.createElement("label"),sp=document.createElement("span"),inp=document.createElement("input");sp.textContent=n;inp.type="color";inp.dataset.player=n;inp.value=S.manual[n];l.append(sp,inp);L.manual.append(l)});L.status.textContent=`${tagVotes.length} taggröster, ${docs.length} sidor lästa.`;}function rebuild(){S.timeline=sorted(S.votes.filter(v=>v.ts&&!isNaN(+new Date(v.ts)))).map(v=>new Date(v.ts));L.ticks.innerHTML="";S.timeline.forEach((_,i)=>{const o=document.createElement("option");o.value=String(i);L.ticks.append(o)});const max=Math.max(0,S.timeline.length-1);L.slider.max=String(max);if(S.index==null||S.index>max)S.index=max;if(S.index<0)S.index=0;L.slider.value=String(S.index);S.limit=S.timeline.length?S.timeline[S.index]:null;L.sliderLabel.textContent=S.timeline.length?`${S.index+1}/${S.timeline.length} röster (${fmt(S.limit)})`:"–";render()}function subset(){let a=S.votes;if(S.limit)a=a.filter(v=>+new Date(v.ts)<=+S.limit);if(view()==="latest")a=latest(a);if(S.filter)a=a.filter(v=>v.from===S.filter);return a}function standingAt(source,row){const active={},count={},reached={};source.filter(v=>{const a=+new Date(v.ts),b=+new Date(row.ts);return a<b||(a===b&&Number(v.post||0)<=Number(row.post||0))}).forEach(v=>{const old=active[v.from];if(old){count[old]=(count[old]||0)-1;if(count[old]<=0){delete count[old];delete reached[old]}else reached[old]={ts:v.ts,post:Number(v.post||0)}}active[v.from]=v.to;count[v.to]=(count[v.to]||0)+1;reached[v.to]={ts:v.ts,post:Number(v.post||0)}});return Object.entries(count).sort((a,b)=>b[1]-a[1]||(+new Date(reached[a[0]]?.ts||0)-+new Date(reached[b[0]]?.ts||0))||((reached[a[0]]?.post||0)-(reached[b[0]]?.post||0)))}function bars(entries){const c=L.canvas.getContext("2d"),W=L.canvas.width,H=L.canvas.height;c.clearRect(0,0,W,H);if(!entries.length)return;const left=180,pad=10,bh=Math.max(14,Math.floor((H-pad*2)/entries.length)-3),max=Math.max(...entries.map(x=>x[1]));c.font="16px Arial";entries.forEach((e,i)=>{const y=pad+i*(bh+3),w=Math.floor((W-left-pad)*e[1]/max),v=String(e[1]);c.fillStyle=color(e[0]);c.fillText(e[0],pad,y+bh-2);c.fillRect(left,y,w,bh);c.fillStyle="#fff";c.fillText(v,Math.max(left+4,left+w-c.measureText(v).width-4),y+bh-2)})}function sortRows(){if(!S.sort)return;const [key,dir]=S.sort.split("-"),rows=$$("#table tbody tr");rows.sort((a,b)=>{const A=key==="from"?a.children[0].textContent.trim():a.dataset.ts||"",B=key==="from"?b.children[0].textContent.trim():b.dataset.ts||"",n=key==="from"?A.localeCompare(B,"sv"):A.localeCompare(B);return dir==="desc"?-n:n});L.tbody.replaceChildren(...rows)}function render(override=null,sourceOverride=null){const rows=override??subset(),source=sorted(sourceOverride||S.votes.filter(v=>!S.limit||+new Date(v.ts)<=+S.limit)),current=latest(source),count={},first={};sorted(current).forEach(v=>{count[v.to]=(count[v.to]||0)+1;if(!first[v.to]||+new Date(v.ts)<+new Date(first[v.to]))first[v.to]=v.ts});const order=Object.entries(count).sort((a,b)=>b[1]-a[1]||(+new Date(first[a[0]]||0)-+new Date(first[b[0]]||0)));if(!order.length){L.summary.textContent="Inga röster att visa.";L.tbody.innerHTML="";bars([]);return}const [danger,n]=order[0],last=current.reduce((a,v)=>!a||+new Date(v.ts)>+new Date(a)?v.ts:a,null);L.summary.textContent=`⚠️ Risk för utröstning: ${danger} (${n} röster, sedan ${fmt(first[danger])}). Senast röst lagd ${fmt(last)}.`;L.tbody.innerHTML="";const history={};sorted(rows).forEach(v=>{const stand=standingAt(source,v),lead=stand[0]?`${stand[0][0]} (${stand[0][1]})`:"–",runner=stand[1]?`${stand[1][0]} (${stand[1][1]})`:"–";history[v.from]||=[];if(history[v.from].at(-1)!==v.to)history[v.from].push(v.to);const chain=history[v.from].map((n,i,a)=>i===a.length-1?`<a target="_blank" rel="noopener" href="${esc(U+"post-"+v.post)}" style="color:${color(n)};font-weight:bold">${esc(n)}</a>`:`<span style="color:${color(n)}">${esc(n)}</span>`).join(" → ");const tr=document.createElement("tr");tr.dataset.ts=v.ts||"";tr.innerHTML=`<td style="color:${color(v.from)};font-weight:bold">${esc(v.from)}</td><td>${chain}</td><td>${fmt(v.ts)}</td><td>${esc(lead)}</td><td>${esc(runner)}</td>`;L.tbody.append(tr)});sortRows();bars(order);}function stop(){if(S.timer){clearTimeout(S.timer);S.timer=null}L.play.disabled=false}L.exp.onclick=()=>{const a=document.createElement("a"),text=["Röstgivare,Röst,Tidpunkt,Post,Page",...subset().map(v=>[v.from,v.to,v.ts,v.post,v.page].map(csv).join(","))].join("\n"),url=URL.createObjectURL(new Blob(["\ufeff"+text],{type:"text/csv;charset=utf-8"}));a.href=url;a.download="rostdata.csv";a.click();setTimeout(()=>URL.revokeObjectURL(url),1000)};L.views.forEach(r=>r.onchange=()=>{stop();S.index=null;rebuild()});L.filter.onchange=()=>{stop();S.filter=L.filter.value;S.index=null;rebuild()};L.colorMode.onchange=()=>{S.colorMode=L.colorMode.value;fillControls();render()};L.manual.oninput=e=>{if(e.target.type==="color"){S.manual[e.target.dataset.player]=e.target.value;fillControls();render()}};L.slider.oninput=()=>{stop();S.index=Number(L.slider.value);S.limit=S.timeline[S.index]||null;L.sliderLabel.textContent=S.timeline.length?`${S.index+1}/${S.timeline.length} röster (${fmt(S.limit)})`:"–";render()};L.play.onclick=()=>{stop();L.play.disabled=true;const all=sorted(S.votes.filter(v=>!S.limit||+new Date(v.ts)<=+S.limit));let i=0;const delay=Math.max(0,Number(L.delay.value)||200);(function step(){if(i>all.length){stop();render();return}const source=all.slice(0,i);let show=view()==="all"?source:latest(source);if(S.filter)show=show.filter(v=>v.from===S.filter);render(show,source);i++;S.timer=setTimeout(step,delay)})()};L.ths.forEach(th=>{const key=th.dataset.sort;if(key)th.onclick=()=>{S.sort=key+"-"+(S.sort===key+"-asc"?"desc":"asc");render()}});fillControls();rebuild();})()