Files
radio-explorer/server/public/assets/admin-BRU0y9A4.js
Marco Mooren 00246389bc Add API documentation and underground station importer
- Introduced a new HTML documentation page for the oradio API, including a JavaScript file to handle dynamic content and API requests.
- Added a CSS file for styling the documentation page.
- Implemented an underground station importer script that fetches data from Radio-Browser and writes it to a JSON file.
- Created a stats module to compute and manage vote and play statistics for radio stations.
- Added a polyfill for modulepreload to ensure compatibility with older browsers.
2026-05-11 02:06:48 +02:00

2 lines
9.9 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{a as i,c as g,e}from"./dom-BvorgAdo.js";const b=document.getElementById("app"),n={user:null,view:"stations",stations:[],users:[],system:null,search:""};async function v(){try{n.user=await i.get("/api/auth/me")}catch{return C()}if(n.user.role!=="admin"){b.innerHTML=`<div class="login"><div><h1>Admin only</h1><p>Signed in as ${n.user.username} (${n.user.role}).</p></div></div>`;return}await u(),m()}async function u(){const a=[i.get("/api/stations?all=1")];n.view==="users"&&a.push(i.get("/api/auth/users")),n.view==="system"&&a.push(i.get("/api/admin/system"));const[t,s,p]=await Promise.all(a);n.stations=t,n.view==="users"&&(n.users=s||[]),n.view==="system"&&(n.system=s||p||null)}function C(){g(b),b.appendChild(e("div",{class:"login"},e("form",{onSubmit:async a=>{a.preventDefault();const t=new FormData(a.target);try{n.user=await i.post("/api/auth/login",{username:t.get("username"),password:t.get("password")}),await v()}catch(s){a.target.querySelector(".err").textContent=s.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 m(){g(b);const a=e("aside",{class:"side"},e("h1",{},"Online Radio Explorer"),...["stations","import","users","system"].map(s=>e("button",{class:`nav ${n.view===s?"active":""}`,onClick:async()=>{n.view=s,await u(),m()}},k(s))),e("div",{class:"me"},`Signed in as ${n.user.username}`,e("br"),e("a",{href:"#",onClick:async s=>{s.preventDefault(),await i.post("/api/auth/logout"),location.reload()}},"Sign out"))),t=e("main",{class:"main"});n.view==="stations"?S(t):n.view==="import"?I(t):n.view==="users"?$(t):n.view==="system"&&E(t),b.appendChild(e("div",{class:"shell"},a,t))}function k(a){return{stations:"Stations",import:"Import",users:"Users",system:"System"}[a]}function S(a){a.appendChild(e("div",{class:"bar"},e("input",{placeholder:"Search…",value:n.search,onInput:s=>{n.search=s.target.value,w()}}),e("button",{class:"btn primary",onClick:()=>f()},"+ Add station"),e("button",{class:"btn",onClick:async()=>{await i.post("/api/admin/health-check"),alert("Health check finished"),await u(),m()}},"Run health check")));const t=e("div",{id:"tableWrap"});a.appendChild(t),w()}function w(){const a=document.getElementById("tableWrap");if(!a)return;g(a);const t=n.search.toLowerCase(),s=n.stations.filter(o=>!t||o.name.toLowerCase().includes(t)||(o.country||"").toLowerCase().includes(t)||(o.genres||[]).some(c=>c.toLowerCase().includes(t))),p=e("table",{},e("thead",{},e("tr",{},e("th",{},"Name"),e("th",{},"Source"),e("th",{},"Genres"),e("th",{},"Country"),e("th",{},"Enabled"),e("th",{},"Actions"))),e("tbody",{},...s.map(o=>e("tr",{},e("td",{},e("strong",{},o.name),e("br"),e("small",{},o.homepage||"")),e("td",{},o.source),e("td",{},...(o.genres||[]).slice(0,4).map(c=>e("span",{class:"tag"},c))),e("td",{},o.country||""),e("td",{},o.enabled?"✅":"⛔"),e("td",{},e("button",{class:"btn",onClick:()=>f(o.id)},"Edit")," ",e("button",{class:"btn danger",onClick:async()=>{confirm(`Delete ${o.name}?`)&&(await i.del(`/api/stations/${o.id}`),await u(),m())}},"Delete"))))));a.appendChild(p)}async function f(a){const t=a?await i.get(`/api/stations/${a}`):{name:"",genres:[],streams:[],enabled:!0},s=e("dialog"),p=e("div",{class:"streams"});function o(){var l;g(p),p.appendChild(e("div",{style:{fontWeight:600,marginBottom:"6px"}},"Streams")),(l=t.streams)!=null&&l.length||p.appendChild(e("div",{style:{color:"#6b7280"}},"No streams yet."));for(const r of t.streams||[])p.appendChild(e("div",{class:"stream-row"},e("select",{onChange:d=>r.format=d.target.value},...["mp3","aac","hls","m3u","pls","ogg","unknown"].map(d=>e("option",{value:d,selected:r.format===d},d))),e("input",{value:r.url,placeholder:"https://…",onInput:d=>r.url=d.target.value}),e("input",{type:"number",placeholder:"kbps",value:r.bitrate||"",onInput:d=>r.bitrate=Number(d.target.value)||null}),e("input",{value:r.label||"",placeholder:"Label",onInput:d=>r.label=d.target.value}),r.last_status?e("span",{class:`pill ${r.last_status==="up"?"up":"down"}`},r.last_status):e("span"),e("button",{class:"btn danger",type:"button",onClick:()=>{t.streams=t.streams.filter(d=>d!==r),o()}},"×")));p.appendChild(e("button",{class:"btn",type:"button",onClick:()=>{var r;t.streams=[...t.streams||[],{url:"",format:"mp3",priority:((r=t.streams)==null?void 0:r.length)||0}],o()}},"+ Add stream"))}const c=e("form",{method:"dialog",onSubmit:async l=>{l.preventDefault();const r={name:t.name,homepage:t.homepage,country:t.country,genres:t.genres,description:t.description,image_url:t.image_url,enabled:t.enabled};if(a){await i.patch(`/api/stations/${a}`,r);const d=await i.get(`/api/stations/${a}`);for(const y of d.streams||[])await i.del(`/api/stations/${a}/streams/${y.id}`);for(const y of t.streams||[])y.url&&await i.post(`/api/stations/${a}/streams`,y)}else r.streams=(t.streams||[]).filter(d=>d.url),await i.post("/api/stations",r);s.close(),await u(),m()}},e("h2",{},a?"Edit station":"Add station"),e("div",{class:"row"},e("label",{},"Name"),e("input",{value:t.name,onInput:l=>t.name=l.target.value,required:!0})),e("div",{class:"row"},e("label",{},"Homepage"),e("input",{value:t.homepage||"",onInput:l=>t.homepage=l.target.value})),e("div",{class:"row"},e("label",{},"Country"),e("input",{value:t.country||"",maxlength:4,onInput:l=>t.country=l.target.value})),e("div",{class:"row"},e("label",{},"Genres"),e("input",{value:(t.genres||[]).join(", "),onInput:l=>t.genres=l.target.value.split(",").map(r=>r.trim()).filter(Boolean)})),e("div",{class:"row"},e("label",{},"Image URL"),e("input",{value:t.image_url||"",onInput:l=>t.image_url=l.target.value})),e("div",{class:"row col"},e("textarea",{rows:2,placeholder:"Description",onInput:l=>t.description=l.target.value},t.description||"")),e("div",{class:"row"},e("label",{},"Enabled"),e("input",{type:"checkbox",checked:t.enabled,onChange:l=>t.enabled=l.target.checked})),p,e("div",{class:"actions"},e("button",{class:"btn",type:"button",onClick:()=>s.close()},"Cancel"),e("button",{class:"btn primary",type:"submit"},"Save")));o(),s.appendChild(c),document.body.appendChild(s),s.showModal(),s.addEventListener("close",()=>s.remove())}function I(a){let t=[];const s=e("div");a.appendChild(e("h2",{},"Import from Radio-Browser")),a.appendChild(e("div",{class:"bar"},e("input",{id:"rbq",placeholder:"Search by name…"}),e("input",{id:"rbcountry",placeholder:"Country (e.g. NL)",style:{minWidth:"120px"}}),e("input",{id:"rbtag",placeholder:"Tag/genre"}),e("button",{class:"btn primary",onClick:async()=>{const o=new URLSearchParams({q:document.getElementById("rbq").value,country:document.getElementById("rbcountry").value,tag:document.getElementById("rbtag").value});t=await i.get(`/api/stations/sources/radiobrowser/search?${o}`),p()}},"Search"))),a.appendChild(s);function p(){if(g(s),!t.length){s.appendChild(e("p",{},"No results yet."));return}const o=e("table",{},e("thead",{},e("tr",{},e("th",{},"Name"),e("th",{},"Country"),e("th",{},"Tags"),e("th",{},"Stream"),e("th",{},""))),e("tbody",{},...t.map(c=>{var l,r;return e("tr",{},e("td",{},c.name),e("td",{},c.country||""),e("td",{},...(c.genres||[]).slice(0,4).map(d=>e("span",{class:"tag"},d))),e("td",{},e("small",{},(((l=c.streams[0])==null?void 0:l.format)||"")+" "+(((r=c.streams[0])==null?void 0:r.bitrate)||""))),e("td",{},e("button",{class:"btn primary",onClick:async()=>{await i.post("/api/stations/sources/radiobrowser/import",c),alert(`Imported ${c.name}`)}},"Import")))})));s.appendChild(o)}}function $(a){a.appendChild(e("div",{class:"bar"},e("h2",{style:{margin:0,flex:1}},"Users"),e("button",{class:"btn primary",onClick:D},"+ Add user"))),a.appendChild(e("table",{},e("thead",{},e("tr",{},e("th",{},"Username"),e("th",{},"Role"),e("th",{},"Created"),e("th",{},""))),e("tbody",{},...n.users.map(t=>e("tr",{},e("td",{},t.username),e("td",{},t.role),e("td",{},t.created_at),e("td",{},e("button",{class:"btn",onClick:async()=>{const s=prompt(`New password for ${t.username}:`);s&&(await i.patch(`/api/auth/users/${t.id}`,{password:s}),alert("Updated"))}},"Reset PW")," ",e("button",{class:"btn",onClick:async()=>{const s=t.role==="admin"?"user":"admin";await i.patch(`/api/auth/users/${t.id}`,{role:s}),await u(),m()}},"Toggle role")," ",t.id!==n.user.id?e("button",{class:"btn danger",onClick:async()=>{confirm(`Delete ${t.username}?`)&&(await i.del(`/api/auth/users/${t.id}`),await u(),m())}},"Delete"):null))))))}function D(){const a=e("dialog");a.appendChild(e("form",{method:"dialog",onSubmit:async t=>{t.preventDefault();const s=new FormData(t.target);await i.post("/api/auth/users",{username:s.get("username"),password:s.get("password"),role:s.get("role")}),a.close(),await u(),m()}},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:()=>a.close()},"Cancel"),e("button",{class:"btn primary",type:"submit"},"Create")))),document.body.appendChild(a),a.showModal(),a.addEventListener("close",()=>a.remove())}function E(a){const t=n.system||{};a.appendChild(e("h2",{},"System")),a.appendChild(e("div",{class:"system-grid"},h("Stations",t.stations),h("Streams",t.streams),h("Users",t.users),h("Favorites",t.favorites),h("Node",t.node),h("Uptime (s)",t.uptime_s))),a.appendChild(e("div",{class:"bar",style:{marginTop:"16px"}},e("button",{class:"btn",onClick:async()=>{await i.post("/api/admin/health-check"),alert("Health check finished"),await u(),m()}},"Run health check"),e("button",{class:"btn",onClick:async()=>{const s=await i.post("/api/admin/reseed");alert(JSON.stringify(s))}},"Reseed (if empty)")))}function h(a,t){return e("div",{class:"stat"},e("div",{class:"k"},a),e("div",{class:"v"},t??"—"))}v();