Files
radio-explorer/server/public/assets/admin-DN0aiXMa.js
2026-05-27 12:54:56 +02:00

2 lines
28 KiB
JavaScript
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.

import"./modulepreload-polyfill-B5Qt9EMX.js";import{P,a as c,m as q,c as y,e}from"./debug-B-FoNBZ5.js";const C=document.getElementById("app"),l={user:null,view:"stations",stations:[],users:[],rooms:[],leaderboard:[],system:null,search:"",sourceFilter:"",selected:new Set,discoverResults:[],discoverQuery:{q:"",country:"",tag:""}},S=new P({onState:s=>{Object.assign(f,s),T()}}),f={stationId:null,playing:!1,loading:!1};async function L(){try{l.user=await c.get("/api/auth/me")}catch{return B()}if(l.user.role!=="admin"){C.innerHTML=`<div class="login"><div><h1>Admin only</h1><p>Signed in as ${l.user.username} (${l.user.role}).</p></div></div>`;return}await h(),q({player:S,clock:null,ws:null,role:"admin"}),g()}async function h(){const s=[c.get("/api/stations?all=1")];l.view==="users"&&s.push(c.get("/api/auth/users")),l.view==="system"&&s.push(c.get("/api/admin/system")),l.view==="rooms"&&s.push(c.get("/api/admin/rooms")),l.view==="leaderboard"&&s.push(c.get("/api/admin/leaderboard"));const t=await Promise.all(s);l.stations=t[0],l.view==="users"&&(l.users=t[1]||[]),l.view==="system"&&(l.system=t[1]||null),l.view==="rooms"&&(l.rooms=t[1]||[]),l.view==="leaderboard"&&(l.leaderboard=t[1]||[])}function B(){y(C),C.appendChild(e("div",{class:"login"},e("form",{onSubmit:async s=>{s.preventDefault();const t=new FormData(s.target);try{l.user=await c.post("/api/auth/login",{username:t.get("username"),password:t.get("password")}),await L()}catch(a){s.target.querySelector(".err").textContent=a.message}}},e("h1",{},"Admin sign in"),e("input",{name:"username",placeholder:"Username",required:!0}),e("input",{name:"password",type:"password",placeholder:"Password",required:!0}),e("div",{class:"err"}),e("button",{class:"btn primary",type:"submit"},"Sign in"))))}function g(){y(C);const s=["stations","discover","leaderboard","rooms","users","system"],t=e("aside",{class:"side"},e("h1",{},"OnlineRadio · Admin"),...s.map(n=>e("button",{class:`nav ${l.view===n?"active":""}`,onClick:async()=>{l.view=n,l.selected.clear(),await h(),g()}},N(n))),e("div",{class:"me"},`Signed in as ${l.user.username}`,e("br"),e("a",{href:"/master",target:"_blank"},"Open master ↗"),e("br"),e("a",{href:"/",target:"_blank"},"Open kiosk ↗"),e("br"),e("a",{href:"#",onClick:async n=>{n.preventDefault(),await c.post("/api/auth/logout"),location.reload()}},"Sign out"))),a=e("main",{class:"main"});C.appendChild(e("div",{class:"shell"},t,a)),l.view==="stations"?U(a):l.view==="discover"?A(a):l.view==="leaderboard"?H(a):l.view==="rooms"?G(a):l.view==="users"?K(a):l.view==="system"&&Q(a)}const N=s=>({stations:"Stations",discover:"Discover",leaderboard:"Leaderboard",rooms:"Rooms",users:"Users",system:"System"})[s];function U(s){s.appendChild(e("div",{class:"bar"},e("input",{placeholder:"Search name / country / genre…",value:l.search,onInput:t=>{l.search=t.target.value,w()}}),e("select",{onChange:t=>{l.sourceFilter=t.target.value,w()}},e("option",{value:""},"All sources"),...M(l.stations).map(t=>e("option",{value:t,selected:l.sourceFilter===t},t))),e("button",{class:"btn primary",onClick:()=>R()},"+ Add station"),e("button",{class:"btn",onClick:async()=>{if(!confirm("Run health check on every stream?"))return;const t=await c.post("/api/admin/health-check");alert(`Checked ${t.checked} streams.`),await h(),g()}},"Health check"))),s.appendChild(e("div",{id:"bulkSlot"})),s.appendChild(e("div",{id:"tableWrap"})),w()}function M(s){return[...new Set(s.map(t=>t.source).filter(Boolean))].sort()}function w(){const s=document.getElementById("tableWrap"),t=document.getElementById("bulkSlot");if(!s||!t)return;y(s),y(t);const a=l.search.toLowerCase(),n=l.sourceFilter,i=l.stations.filter(o=>(!n||o.source===n)&&(!a||o.name.toLowerCase().includes(a)||(o.country||"").toLowerCase().includes(a)||(o.genres||[]).some(m=>m.toLowerCase().includes(a))));l.selected.size&&t.appendChild(O());const r=i.length&&i.every(o=>l.selected.has(o.id)),d=e("table",{},e("thead",{},e("tr",{},e("th",{style:{width:"32px"}},e("input",{type:"checkbox",checked:r,onChange:o=>{o.target.checked?i.forEach(m=>l.selected.add(m.id)):i.forEach(m=>l.selected.delete(m.id)),w()}})),e("th",{},"Name"),e("th",{},"Source"),e("th",{},"Genres"),e("th",{},"Country"),e("th",{},"Streams"),e("th",{},"▲/▼/▶"),e("th",{},"On"),e("th",{},"Actions"))),e("tbody",{},...i.map(o=>{const m=o.image_display_url||o.image_url;return e("tr",{"data-id":o.id},e("td",{},e("input",{type:"checkbox",checked:l.selected.has(o.id),onChange:p=>{p.target.checked?l.selected.add(o.id):l.selected.delete(o.id),w()}})),e("td",{},e("div",{class:"station-cell"},e("div",{class:"station-art-thumb"+(m?"":" empty")},m?e("img",{src:m,alt:"",loading:"lazy",referrerpolicy:"no-referrer",onError:p=>{const u=p.target.parentNode;p.target.remove(),u&&u.classList.add("empty")}}):null),e("div",{class:"meta"},e("strong",{},o.name),e("small",{},o.homepage||o.uuid)))),e("td",{},o.source||""),e("td",{},...(o.genres||[]).slice(0,3).map(p=>e("span",{class:"tag"},p))),e("td",{},o.country||""),e("td",{},String(o.stream_count??"—")),e("td",{},`${o.up||0}/${o.down||0}/${o.plays||0}`),e("td",{},o.enabled?"✓":"✗"),e("td",{},z(o)," ",e("button",{class:"btn",onClick:()=>R(o.id)},"Edit")," ",e("button",{class:"btn danger",onClick:async()=>{await x({stations:[o]})&&(await c.del(`/api/admin/stations/${o.id}`),await h(),g())}},"×")))})));s.appendChild(d)}function O(s){const t=[...l.selected];async function a(i,r){if(r&&!confirm(`${r} (${t.length} stations)`))return;const d=await c.post("/api/admin/stations/bulk",{ids:t,action:i});alert(`${i}: ${d.ok} ok / ${d.failed} failed`),l.selected.clear(),await h(),g()}async function n(){const i=l.stations.filter(d=>l.selected.has(d.id));if(!i.length||!await x({stations:i}))return;const r=await c.post("/api/admin/stations/bulk",{ids:i.map(d=>d.id),action:"delete"});alert(`delete: ${r.ok} ok / ${r.failed} failed`),l.selected.clear(),await h(),g()}return e("div",{class:"bulkbar"},e("span",{class:"count"},`${t.length} selected`),e("button",{class:"btn",onClick:()=>a("enable")},"Enable"),e("button",{class:"btn",onClick:()=>a("disable")},"Disable"),e("button",{class:"btn",onClick:()=>a("scrape-icon","Scrape icons for")},"Scrape icons"),e("button",{class:"btn",onClick:()=>a("refetch-image","Refetch images for")},"Refetch images"),e("button",{class:"btn danger",onClick:()=>n()},"Delete"),e("button",{class:"btn",onClick:()=>{l.selected.clear(),w()}},"Clear"))}function z(s){const t=f.stationId===s.id&&f.playing;return e("span",{class:"preview-player"+(t?" playing":""),"data-preview-station":s.id},e("button",{title:"Preview",onClick:a=>{a.stopPropagation(),f.stationId===s.id&&f.playing?(S.stop(),f.stationId=null,f.playing=!1):(f.stationId=s.id,S.play(s)),T()}},t?"❚❚":"▶"))}function T(){document.querySelectorAll(".preview-player").forEach(s=>{const t=Number(s.getAttribute("data-preview-station")),a=f.stationId===t&&f.playing;s.classList.toggle("playing",a);const n=s.querySelector("button");n&&(n.textContent=a?"❚❚":f.stationId===t&&f.loading?"…":"▶")})}async function R(s){const t=s?await c.get(`/api/stations/${s}`):{name:"",genres:[],streams:[],enabled:!0,image_url:"",country:"",homepage:""},a=e("dialog",{class:"wide"});let n="details";const i=e("div",{class:"tab-body"}),r=e("div",{class:"tabs"},...[["details","Details"],["streams","Streams"],["image","Image"],["stats","Stats"]].map(([m,p])=>e("button",{type:"button",class:n===m?"active":"",onClick:()=>{n=m,d()}},p)));function d(){y(i),r.querySelectorAll("button").forEach((m,p)=>{const u=["details","streams","image","stats"][p];m.classList.toggle("active",u===n)}),n==="details"?F(i,t):n==="streams"?W(i,t):n==="image"?j(i,t,async()=>{s&&Object.assign(t,await c.get(`/api/stations/${s}`)),d()}):n==="stats"&&$(i,t,s)}d();const o=e("div",{class:"actions"},s?e("button",{class:"btn danger",type:"button",onClick:async()=>{await x({stations:[t]})&&(await c.del(`/api/admin/stations/${s}`),a.close(),await h(),g())}},"Delete"):null,e("button",{class:"btn",type:"button",onClick:()=>a.close()},"Cancel"),e("button",{class:"btn primary",type:"button",onClick:async()=>{const m={name:t.name,homepage:t.homepage,country:t.country,genres:t.genres,description:t.description,image_url:t.image_url,category:t.category,enabled:t.enabled};if(s)await c.patch(`/api/admin/stations/${s}`,m);else{m.streams=(t.streams||[]).filter(u=>u.url);const p=await c.post("/api/stations",m);Object.assign(t,p)}a.close(),await h(),g()}},"Save"));a.appendChild(e("form",{method:"dialog",onSubmit:m=>m.preventDefault()},e("h2",{},s?`Edit · ${t.name}`:"Add station"),r,i,o)),document.body.appendChild(a),a.showModal(),a.addEventListener("close",()=>a.remove())}function F(s,t){if(s.appendChild(e("div",{class:"row"},e("label",{},"Name"),e("input",{value:t.name||"",onInput:a=>t.name=a.target.value,required:!0}))),s.appendChild(e("div",{class:"row"},e("label",{},"Homepage"),e("input",{value:t.homepage||"",onInput:a=>t.homepage=a.target.value}))),s.appendChild(e("div",{class:"row"},e("label",{},"Country"),e("input",{value:t.country||"",maxlength:4,onInput:a=>t.country=a.target.value}))),s.appendChild(e("div",{class:"row"},e("label",{},"Genres (CSV)"),e("input",{value:(t.genres||[]).join(", "),onInput:a=>t.genres=a.target.value.split(",").map(n=>n.trim()).filter(Boolean)}))),s.appendChild(e("div",{class:"row"},e("label",{},"Category"),e("input",{value:t.category||"",onInput:a=>t.category=a.target.value}))),s.appendChild(e("div",{class:"row"},e("label",{},"Description"),e("textarea",{rows:4,placeholder:"Short description shown to listeners",onInput:a=>t.description=a.target.value},t.description||""))),s.appendChild(e("div",{class:"row"},e("label",{},"Enabled"),e("input",{type:"checkbox",checked:t.enabled,onChange:a=>t.enabled=a.target.checked}))),t.id){const a=(n,i)=>e("div",{class:"meta-row"},e("span",{class:"meta-k"},n),e("span",{class:"meta-v mono"},i==null||i===""?"—":String(i)));s.appendChild(e("div",{class:"readonly-meta"},e("div",{class:"readonly-meta-head"},"Read-only metadata (used by the public API)"),a("id",t.id),a("uuid",t.uuid),a("slug",t.slug),a("source",t.source),a("source_ref",t.source_ref),a("image_source",t.image_source),a("image_path",t.image_path),a("created_at",t.created_at),a("updated_at",t.updated_at)))}}function W(s,t){if(!t.id){const r=e("div",{class:"streams"}),d=()=>{y(r),r.appendChild(e("div",{style:{fontWeight:700,marginBottom:"6px"}},"Streams"));for(const o of t.streams||[])r.appendChild(I(o,()=>{t.streams=t.streams.filter(m=>m!==o),d()}));r.appendChild(e("button",{class:"btn",type:"button",onClick:()=>{var o;t.streams=[...t.streams||[],{url:"",format:"mp3",priority:((o=t.streams)==null?void 0:o.length)||0}],d()}},"+ Add stream"))};d(),s.appendChild(r);return}const a=e("div",{class:"streams"}),n=async()=>{t.streams=await c.get(`/api/admin/stations/${t.id}/streams`),i()},i=()=>{y(a),a.appendChild(e("div",{style:{fontWeight:700,marginBottom:"6px"}},"Streams"));for(const r of t.streams||[])a.appendChild(I(r,async()=>{confirm("Delete stream?")&&(await c.del(`/api/admin/streams/${r.id}`),await n())},async()=>{await c.patch(`/api/admin/streams/${r.id}`,{url:r.url,format:r.format,bitrate:r.bitrate||null,label:r.label||null,priority:r.priority||0})},async()=>{const d=await c.post(`/api/admin/streams/${r.id}/probe`);r.last_status=d.status,i()}));a.appendChild(e("button",{class:"btn",type:"button",onClick:async()=>{var d;const r=await c.post(`/api/admin/stations/${t.id}/streams`,{url:"",format:"mp3",priority:((d=t.streams)==null?void 0:d.length)||0});t.streams=[...t.streams||[],r],i()}},"+ Add stream"))};i(),s.appendChild(a)}function I(s,t,a,n){const i=(r,d)=>{s[r]=d};return e("div",{class:"stream-row"},e("select",{onChange:r=>i("format",r.target.value)},...["mp3","aac","hls","m3u","pls","ogg","unknown"].map(r=>e("option",{value:r,selected:s.format===r},r))),e("input",{value:s.url||"",placeholder:"https://…",onInput:r=>i("url",r.target.value)}),e("input",{type:"number",placeholder:"kbps",value:s.bitrate||"",onInput:r=>i("bitrate",Number(r.target.value)||null)}),e("input",{value:s.label||"",placeholder:"Label",onInput:r=>i("label",r.target.value)}),s.last_status?e("span",{class:`pill ${s.last_status==="up"?"up":s.last_status==="down"?"down":"unknown"}`},s.last_status):e("span"),e("span",{style:{display:"flex",gap:"4px"}},n?e("button",{class:"btn",type:"button",onClick:n},"Test"):null,a?e("button",{class:"btn",type:"button",onClick:a},"Save"):null,e("button",{class:"btn danger",type:"button",onClick:t},"×")))}function j(s,t,a){if(!t.id){s.appendChild(e("p",{},"Save the station first, then come back to upload an image."));return}const n=t.image_display_url||t.image_url,i=e("div",{class:"image-area"}),r=e("div",{class:"preview",style:n?{backgroundImage:`url("${n}")`}:{}},n?"":"No image"),d=e("div",{class:"dropzone"},"Drop image file here, or click to upload"),o=e("input",{type:"file",accept:"image/*",style:{display:"none"}});async function m(u){if(!u)return;if(u.size>5*1024*1024)return alert("File is too large (5 MB max).");const v=await u.arrayBuffer(),D=await fetch(`/api/admin/stations/${t.id}/image`,{method:"PUT",headers:{"Content-Type":u.type||"application/octet-stream"},credentials:"same-origin",body:v});if(!D.ok){alert("Upload failed: "+D.status);return}await a()}d.addEventListener("click",()=>o.click()),d.addEventListener("dragover",u=>{u.preventDefault(),d.classList.add("over")}),d.addEventListener("dragleave",()=>d.classList.remove("over")),d.addEventListener("drop",u=>{var v;u.preventDefault(),d.classList.remove("over"),m((v=u.dataTransfer.files)==null?void 0:v[0])}),o.addEventListener("change",()=>{var u;return m((u=o.files)==null?void 0:u[0])});const p=e("div",{class:"actions-col"},e("div",{class:"row"},e("label",{},"Image URL"),e("input",{value:t.image_url||"",onInput:u=>t.image_url=u.target.value})),d,o,e("div",{style:{display:"flex",gap:"6px",flexWrap:"wrap"}},e("button",{class:"btn",type:"button",onClick:async()=>{await c.patch(`/api/admin/stations/${t.id}`,{image_url:t.image_url});const u=await fetch(`/api/admin/stations/${t.id}/image/refetch`,{method:"POST",headers:{"Content-Type":"application/json"},credentials:"same-origin",body:JSON.stringify({url:t.image_url})});if(!u.ok){alert("Refetch failed: "+u.status);return}await a()}},"Refetch from URL"),e("button",{class:"btn",type:"button",onClick:async()=>{const u=await c.post(`/api/admin/stations/${t.id}/scrape-icon`).catch(v=>({error:v.message}));u.error&&alert("Scrape failed: "+u.error),await a()}},"Auto-scrape"),e("button",{class:"btn danger",type:"button",onClick:async()=>{confirm("Drop the cached image?")&&(await c.del(`/api/admin/stations/${t.id}/image`),await a())}},"Clear cache")),e("div",{style:{fontSize:"11px",color:"var(--muted)"}},`Source: ${t.image_source||"—"} · Path: ${t.image_path||"—"}`));i.appendChild(r),i.appendChild(p),s.appendChild(i)}function $(s,t,a){if(!a){s.appendChild(e("p",{},"Stats are available after the station is created."));return}s.appendChild(e("div",{class:"system-grid"},b("Votes up",t.up||0),b("Votes down",t.down||0),b("Score",(t.score||0).toFixed(2)),b("Plays",t.plays||0),b("Streams",(t.streams||[]).length))),s.appendChild(e("div",{style:{marginTop:"12px",display:"flex",gap:"8px",flexWrap:"wrap"}},e("button",{class:"btn danger",onClick:async()=>{confirm("Delete all votes for this station?")&&(await c.del(`/api/admin/stations/${a}/votes`),Object.assign(t,await c.get(`/api/stations/${a}`)),$(_(s),t,a))}},"Reset votes"),e("button",{class:"btn danger",onClick:async()=>{confirm("Delete all plays for this station?")&&(await c.del(`/api/admin/stations/${a}/plays`),Object.assign(t,await c.get(`/api/stations/${a}`)),$(_(s),t,a))}},"Reset plays")))}function _(s){return y(s),s}function b(s,t){return e("div",{class:"stat"},e("div",{class:"v"},String(t)),e("div",{class:"k"},s))}function x({stations:s}){const t=Array.isArray(s)?s:[s],a=t.length===1?t[0]:null,n=a?a.name:"DELETE";return new Promise(i=>{const r=e("dialog",{class:"danger-confirm"});let d="";const o=e("input",{placeholder:`Type "${n}" to confirm`,autocomplete:"off",onInput:p=>{d=p.target.value,m.disabled=d.trim()!==n}}),m=e("button",{class:"btn danger",type:"button",disabled:!0,onClick:()=>{r.close(),i(!0)}},a?"Permanently delete":`Permanently delete ${t.length} stations`);r.appendChild(e("form",{method:"dialog",onSubmit:p=>p.preventDefault()},e("div",{class:"danger-header"},e("div",{class:"danger-icon"},"⚠"),e("h2",{},a?`Delete "${a.name}"?`:`Delete ${t.length} stations?`)),e("div",{class:"danger-body"},e("p",{class:"lede"},"This is irreversible and ",e("b",{},"will break the public API")," for any client (kiosk, master, third-party integration, bookmarks, scripts) ","that references these stations by id, uuid or slug."),e("ul",{class:"impact"},e("li",{},e("code",{},"GET /api/v1/stations/{id}")," will return 404"),e("li",{},e("code",{},"GET /api/v1/stations/{uuid}")," will return 404"),e("li",{},"All streams, votes, plays and favorites attached to ",a?"this station":"these stations"," are dropped (cascade)"),e("li",{},"Active listeners playing ",a?"this station":"one of these"," will receive a stop event"),e("li",{},"Cached image files are unlinked from disk")),t.length>1||a?e("div",{class:"impact-list"},e("div",{class:"impact-list-head"},"Targets:"),e("ul",{},...t.slice(0,12).map(p=>e("li",{},e("b",{},p.name),p.uuid?e("span",{class:"mono"},` · ${p.uuid}`):null,e("span",{class:"mono"},` · id=${p.id}`))),t.length>12?e("li",{class:"more"},`…and ${t.length-12} more`):null)):null,e("label",{class:"type-to-confirm"},"Type ",e("code",{},n)," to confirm:",o)),e("div",{class:"actions"},e("button",{class:"btn",type:"button",onClick:()=>{r.close(),i(!1)}},"Cancel"),m))),document.body.appendChild(r),r.addEventListener("close",()=>r.remove()),r.showModal(),setTimeout(()=>o.focus(),50)})}function A(s){s.appendChild(e("h2",{},"Discover · Radio-Browser"));const t=l.discoverQuery;async function a(){const n=new URLSearchParams;t.q&&n.set("q",t.q),t.country&&n.set("country",t.country),t.tag&&n.set("tag",t.tag),n.set("limit","50");const i=document.getElementById("discoverWrap");i&&(y(i),i.appendChild(e("p",{class:"muted"},"Loading…")));try{l.discoverResults=await c.get(`/api/stations/sources/radiobrowser/search?${n}`)}catch(r){l.discoverResults=[],i&&(y(i),i.appendChild(e("p",{class:"err"},r.message||"Search failed")));return}k()}s.appendChild(e("div",{class:"bar"},e("input",{placeholder:"Name…",value:t.q,onInput:n=>t.q=n.target.value,onKeyDown:n=>{n.key==="Enter"&&a()}}),e("input",{placeholder:"Country (e.g. NL)",value:t.country,onInput:n=>t.country=n.target.value,onKeyDown:n=>{n.key==="Enter"&&a()},style:{minWidth:"120px"}}),e("input",{placeholder:"Tag/genre",value:t.tag,onInput:n=>t.tag=n.target.value,onKeyDown:n=>{n.key==="Enter"&&a()}}),e("button",{class:"btn primary",onClick:a},"Search"),e("button",{class:"btn",onClick:()=>{t.q="",t.country="",t.tag="",A(_(s))}},"Reset"),e("button",{class:"btn",onClick:async()=>{const n=l.discoverResults.filter(i=>i.__import);if(!n.length)return alert("Nothing selected.");if(confirm(`Import ${n.length} stations?`)){for(const i of n)await c.post("/api/stations/sources/radiobrowser/import",i).catch(()=>{});alert("Done."),await h(),await a()}}},"Import selected"))),s.appendChild(e("div",{id:"discoverWrap"})),l.discoverResults.length?k():a()}function k(){const s=document.getElementById("discoverWrap");if(!s)return;y(s);const t=l.discoverResults;if(!t.length){s.appendChild(e("p",{class:"muted"},"No results. Try a different query — leave fields blank for the top stations on Radio-Browser."));return}const a=new Set(l.stations.map(i=>i.uuid).filter(Boolean)),n=t.length&&t.every(i=>i.__import);s.appendChild(e("table",{},e("thead",{},e("tr",{},e("th",{style:{width:"32px"}},e("input",{type:"checkbox",checked:!!n,onChange:i=>{t.forEach(r=>{r.__import=i.target.checked&&!a.has(r.uuid)}),k()}})),e("th",{},"Name"),e("th",{},"Country"),e("th",{},"Tags"),e("th",{},"Stream"),e("th",{},"Status"),e("th",{},""))),e("tbody",{},...t.map(i=>{var o,m;const r=a.has(i.uuid),d=i.image_url;return e("tr",{class:r?"discover-existing":""},e("td",{},e("input",{type:"checkbox",checked:!!i.__import,disabled:r,onChange:p=>{i.__import=p.target.checked}})),e("td",{},e("div",{class:"station-cell"},e("div",{class:"station-art-thumb"+(d?"":" empty")},d?e("img",{src:d,alt:"",loading:"lazy",referrerpolicy:"no-referrer",onError:p=>{const u=p.target.parentNode;p.target.remove(),u&&u.classList.add("empty")}}):null),e("div",{class:"meta"},e("strong",{},i.name),e("small",{},i.homepage||i.uuid)))),e("td",{},i.country||""),e("td",{},...(i.genres||[]).slice(0,4).map(p=>e("span",{class:"tag"},p))),e("td",{},e("small",{},(((o=i.streams[0])==null?void 0:o.format)||"")+" "+((m=i.streams[0])!=null&&m.bitrate?`· ${i.streams[0].bitrate}kbps`:""))),e("td",{},r?e("span",{class:"pill up"},"in library"):e("span",{class:"pill unknown"},"new")),e("td",{},r?e("button",{class:"btn",disabled:!0},"Imported"):e("button",{class:"btn primary",onClick:async p=>{p.target.disabled=!0;try{await c.post("/api/stations/sources/radiobrowser/import",i),await h(),k()}catch(u){alert("Import failed: "+u.message)}}},"Import")))}))))}function H(s){s.appendChild(e("h2",{},"Leaderboard")),s.appendChild(e("p",{style:{color:"var(--muted)",marginTop:0}},"Top stations by total listen time. Use the reset buttons to moderate runaway counters."));const t=e("div",{class:"leaderboard"});l.leaderboard.forEach((a,n)=>{const i=a.image_display_url||(a.image_path?`/media/${a.image_path}`:a.image_url);t.appendChild(e("div",{class:"leader-row"},e("div",{class:"rank"},String(n+1)),e("div",{class:"art"+(i?"":" empty")},i?e("img",{src:i,alt:"",loading:"lazy",referrerpolicy:"no-referrer",onError:r=>{const d=r.target.parentNode;r.target.remove(),d&&d.classList.add("empty")}}):null),e("div",{class:"name"},e("b",{},a.name),e("br"),e("small",{},a.country||"")),e("div",{class:"stat-num",title:"Total listen time"},`${E(a.total_play_ms)}`),e("div",{class:"stat-num",title:"Average session length"},`Ø ${E(a.avg_session_ms)}`),e("div",{class:"stat-num",title:"Play taps"},`${a.plays}`),e("div",{class:"stat-num",title:"Sessions credited"},`· ${a.sessions}`),e("div",{class:"stat-num"},`${a.up}`),e("div",{class:"stat-num"},`${a.down}`)))}),s.appendChild(t)}function E(s){const t=Math.max(0,Math.round((s||0)/1e3));if(t<60)return`${t}s`;const a=Math.floor(t/60);if(a<60)return`${a}m${String(t%60).padStart(2,"0")}s`;const n=Math.floor(a/60);return n<24?`${n}h${String(a%60).padStart(2,"0")}m`:`${Math.floor(n/24)}d${String(n%24).padStart(2,"0")}h`}function G(s){s.appendChild(e("h2",{},"Rooms")),s.appendChild(e("table",{},e("thead",{},e("tr",{},e("th",{},"Slug"),e("th",{},"Name"),e("th",{},"Members"),e("th",{},"Active station"),e("th",{},"Created"),e("th",{},""))),e("tbody",{},...l.rooms.map(t=>e("tr",{},e("td",{},e("code",{},t.slug)),e("td",{},t.name),e("td",{},String(t.members)),e("td",{},t.active?"●":"—"),e("td",{},e("small",{},t.created_at)),e("td",{},t.slug.startsWith("u-")?e("small",{style:{color:"var(--muted)"}},"personal"):e("button",{class:"btn danger",onClick:async()=>{confirm(`Delete room ${t.slug}?`)&&(await c.del(`/api/admin/rooms/${t.slug}`),await h(),g())}},"Delete")))))))}function K(s){s.appendChild(e("div",{class:"bar"},e("h2",{style:{margin:0,flex:1}},"Users"),e("button",{class:"btn",onClick:V},"🔑 Trust this device")," ",e("button",{class:"btn primary",onClick:J},"+ Add user"))),s.appendChild(e("table",{},e("thead",{},e("tr",{},e("th",{},"Username"),e("th",{},"Role"),e("th",{},"Main"),e("th",{},"Avatar"),e("th",{},"Created"),e("th",{},""))),e("tbody",{},...l.users.map(t=>e("tr",{},e("td",{},t.username),e("td",{},t.role),e("td",{},t.is_main?"★ main":e("button",{class:"btn",title:"Promote to main (shared) user",onClick:async()=>{confirm(`Make ${t.username} the shared/main user?`)&&(await c.patch(`/api/auth/users/${t.id}`,{is_main:!0}),await h(),g())}},"Make main")),e("td",{},e("span",{style:{display:"inline-grid",placeItems:"center",width:"24px",height:"24px",background:t.avatar_color||"#ff7a3d",color:"#1a0a00",fontWeight:"800",fontSize:"12px"}},t.avatar_emoji||t.username.slice(0,1).toUpperCase())," ",e("button",{class:"btn",onClick:async()=>{const a=prompt(`Avatar emoji / letter for ${t.username} (leave empty to clear):`,t.avatar_emoji||"");if(a===null)return;const n=prompt("Avatar color (hex, e.g. #ff7a3d):",t.avatar_color||"#ff7a3d");n!==null&&(await c.patch(`/api/auth/users/${t.id}`,{avatar_emoji:a,avatar_color:n}),await h(),g())}},"Edit")),e("td",{},t.created_at),e("td",{},e("button",{class:"btn",onClick:async()=>{const a=prompt(`New password for ${t.username}:`);a&&(await c.patch(`/api/auth/users/${t.id}`,{password:a}),alert("Updated"))}},"Reset PW")," ",e("button",{class:"btn",onClick:async()=>{const a=t.role==="admin"?"user":"admin";await c.patch(`/api/auth/users/${t.id}`,{role:a}),await h(),g()}},"Toggle role")," ",t.id!==l.user.id?e("button",{class:"btn danger",onClick:async()=>{confirm(`Delete ${t.username}?`)&&(await c.del(`/api/auth/users/${t.id}`),await h(),g())}},"Delete"):null))))))}async function V(){let s={trusted:!1,users:[],label:""};try{s=await c.get("/api/auth/devices/me")}catch{}const t=new Set((s.users||[]).map(n=>n.id)),a=e("dialog");a.appendChild(e("form",{method:"dialog",onSubmit:async n=>{n.preventDefault();const i=new FormData(n.target),r=[...i.getAll("user_id")].map(Number),d=i.get("label")||null;try{s.trusted?(await c.patch("/api/auth/devices/me",{label:d,user_ids:r}),alert("Device whitelist updated.")):(await c.post("/api/auth/devices/trust",{label:d,user_ids:r}),alert("This device is now trusted. Fast-switching is enabled for the selected users."))}catch(o){alert(o.message||"Failed");return}a.close()}},e("h2",{},s.trusted?"Edit trusted device":"Trust this device"),e("p",{style:{color:"#8a90a0",fontSize:"13px"}},"Listed users can fast-switch on this device without a password. ","The cookie is HttpOnly and lasts 1 year."),e("div",{class:"row"},e("label",{},"Device label"),e("input",{name:"label",value:s.label||"",placeholder:"e.g. Kitchen kiosk"})),e("div",{style:{maxHeight:"300px",overflowY:"auto",border:"1px solid #262b36",padding:"8px"}},...l.users.map(n=>e("label",{style:{display:"flex",alignItems:"center",gap:"8px",padding:"4px 6px",cursor:"pointer"}},e("input",{type:"checkbox",name:"user_id",value:String(n.id),checked:t.has(n.id)||n.id===l.user.id}),e("span",{},n.username),n.is_main?e("span",{style:{color:"#ffb37a",fontSize:"11px"}}," ★ main"):null,n.id===l.user.id?e("span",{style:{color:"#5d6373",fontSize:"11px"}}," (you)"):null))),e("div",{class:"actions"},e("button",{class:"btn",type:"button",onClick:()=>a.close()},"Cancel"),e("button",{class:"btn primary",type:"submit"},s.trusted?"Update":"Trust device")))),document.body.appendChild(a),a.showModal(),a.addEventListener("close",()=>a.remove())}function J(){const s=e("dialog");s.appendChild(e("form",{method:"dialog",onSubmit:async t=>{t.preventDefault();const a=new FormData(t.target);await c.post("/api/auth/users",{username:a.get("username"),password:a.get("password"),role:a.get("role")}),s.close(),await h(),g()}},e("h2",{},"New user"),e("div",{class:"row"},e("label",{},"Username"),e("input",{name:"username",required:!0})),e("div",{class:"row"},e("label",{},"Password"),e("input",{name:"password",type:"password",required:!0})),e("div",{class:"row"},e("label",{},"Role"),e("select",{name:"role"},e("option",{value:"user"},"user"),e("option",{value:"admin"},"admin"))),e("div",{class:"actions"},e("button",{class:"btn",type:"button",onClick:()=>s.close()},"Cancel"),e("button",{class:"btn primary",type:"submit"},"Create")))),document.body.appendChild(s),s.showModal(),s.addEventListener("close",()=>s.remove())}function Q(s){var a;s.appendChild(e("h2",{},"System"));const t=l.system||{};s.appendChild(e("div",{class:"system-grid"},b("Stations",t.stations||0),b("Streams",t.streams||0),b("Users",t.users||0),b("Favorites",t.favorites||0),b("Cached images",((a=t.image_cache)==null?void 0:a.files)||0),b("Cache size (MB)",t.image_cache?(t.image_cache.bytes/1048576).toFixed(1):"0"),b("Node",t.node||""),b("Uptime (s)",t.uptime_s||0))),s.appendChild(e("div",{style:{marginTop:"16px",display:"flex",gap:"8px"}},e("button",{class:"btn",onClick:async()=>{if(!confirm("Re-seed from data/seed/?"))return;const n=await c.post("/api/admin/reseed");alert(JSON.stringify(n)),await h(),g()}},"Re-seed"),e("button",{class:"btn",onClick:async()=>{const n=await c.post("/api/admin/scrape-icons?all=1");alert(`Updated ${n.updated}, failed ${n.failed}`),await h(),g()}},"Scrape all icons")))}L();