Files
radio-explorer/server/public/assets/master-BGIwPPRC.js
Marco Mooren 29423288ca feat: add multi-user support for favorites management and room clock synchronization
- Implemented a new API endpoint for retrieving and managing user favorites in /api/users.
- Added functionality for admins to edit the shared "main" user's favorites.
- Created a one-shot DB smoke test script for verifying multi-user kiosk migrations.
- Introduced a RoomClock class for synchronizing server time across clients using WebSocket.
2026-05-13 13:53:12 +02:00

2 lines
20 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 m,P as ne,m as se,c as J,e as n}from"./debug-DBzSAgZo.js";import{R as re,c as oe,a as ie,s as le}from"./playGate-C1e0nYli.js";let E=null,y=null,U=null,x=null,N=null;function ue(t){const a=window.devicePixelRatio||1,s=t.clientWidth||t.width||1,r=t.clientHeight||t.height||1,i=Math.max(1,Math.round(s*a)),o=Math.max(1,Math.round(r*a));t.width!==i&&(t.width=i),t.height!==o&&(t.height=o)}function ce(t){if(!y&&N&&(y=N.getAnalyser(),y&&(U=new Uint8Array(y.frequencyBinCount))),!y||!U)return;ue(t);const a=t.getContext("2d");if(!a)return;y.getByteFrequencyData(U);const s=t.width,r=t.height;a.clearRect(0,0,s,r);const i=64,o=U.length,f=Math.max(1,Math.floor(s/i/6)),d=(s-f*(i-1))/i,v=a.createLinearGradient(0,r,0,0);v.addColorStop(0,"rgba(80, 220, 255, 0.85)"),v.addColorStop(.6,"rgba(140, 120, 255, 0.85)"),v.addColorStop(1,"rgba(255, 100, 200, 0.95)"),a.fillStyle=v;for(let u=0;u<i;u++){const g=u/i,C=(u+1)/i,b=Math.floor(Math.pow(g,2)*o),q=Math.max(b+1,Math.floor(Math.pow(C,2)*o));let z=0;for(let $=b;$<q&&$<o;$++)z+=U[$];const ee=z/(q-b),te=Math.pow(ee/255,.7),W=Math.max(2,Math.round(te*r)),ae=Math.round(u*(d+f));a.fillRect(ae,r-W,Math.max(1,Math.round(d)),W)}}function X(){if(!x){E=null;return}ce(x),E=requestAnimationFrame(X)}function de(t,a){if(!(!t||!(a!=null&&a.audio))){if(N=a,typeof a._ensureAudioGraph=="function")try{a._ensureAudioGraph()}catch{}y=a.getAnalyser(),y&&(U=new Uint8Array(y.frequencyBinCount)),x=t,E==null&&(E=requestAnimationFrame(X))}}const S=document.getElementById("app"),e={user:null,users:[],mainUser:null,device:{trusted:!1,users:[]},rooms:[],roomSlug:null,room:null,peers:[],devices:{list:[],current:"default"},np:{stationId:null,station:null,playing:!1,loading:!1,volume:.7,error:null},voteStats:null,favorites:[],tabUser:null,tabFavorites:[],tabLoading:!1,favGenre:"",showOutputs:!1,showAvatars:!1,session:null},c=window.oradioNative||null;let h=null,l=null,I=!1;function D(){I=!0}async function pe(t,a){if(I)return!0;if(ie())return I=!0,!0;try{return await le({stationName:t||"Radio",subtitle:a||"Tap Start to enable audio.",onStart:()=>{I=!0}}),I}catch{return!1}}let k=null,F=0;const w=new re;async function Z(){var s,r;try{e.user=await m.get("/api/auth/me")}catch{return Oe()}const a=new URLSearchParams(location.search).get("room");try{e.rooms=await m.get("/api/rooms")}catch{e.rooms=[]}if(e.roomSlug=a||e.rooms[0]&&e.rooms[0].slug||`u-${e.user.id}`,c!=null&&c.isElectron&&c.listOutputs){try{e.devices.list=await c.listOutputs(),e.devices.current=await c.getCurrent()||((s=e.devices.list[0])==null?void 0:s.id)||"default"}catch(i){console.warn("[master] listOutputs failed",i),e.devices.list=[],e.devices.current="default"}(r=c.onCurrentChanged)==null||r.call(c,i=>{e.devices.current=i,R(),p()})}else e.devices.list=[],e.devices.current=null;l=new ne({onState:i=>{const o={...e.np};Object.assign(e.np,i);const f=e.np.stationId,d=!!e.np.playing,v=e.np.volume,u=f!==o.stationId||d!==!!o.playing,g=Math.abs((v??0)-(o.volume??0))>.001,C=i.loading===!0,b=i.playing===!1&&i.stationId==null&&k!=null;!C&&!b&&(u?(M&&(clearTimeout(M),M=null),T()):g&&ge()),Y()}}),l.onPlayingOnce=i=>{if(!h||!i)return;const o=w.now?w.now():Date.now();try{h.send({type:"state",stationId:i.id,playing:!0,volume:e.np.volume,started_at:o})}catch{}l.sync.enabled?l.updateSyncTarget(o):A(o)};try{e.favorites=await m.get("/api/me/favorites")}catch{e.favorites=[]}try{e.users=await m.get("/api/auth/users/public")}catch{e.users=[]}try{e.mainUser=await m.get("/api/auth/main")}catch{e.mainUser=null}try{e.device=await m.get("/api/auth/devices/me")}catch{e.device={trusted:!1,users:[]}}e.tabUser=e.user.username,e.tabFavorites=e.favorites,B(),fe(),se({player:l,clock:w,getWs:()=>h,role:"master"}),p()}typeof window<"u"&&(window.addEventListener("pagehide",()=>_({beacon:!0})),window.addEventListener("beforeunload",()=>_({beacon:!0})));function B(){if(h)try{h.close()}catch{}w.detach(),h=oe(me,{room:e.roomSlug,kind:"display",onOpen:()=>R()}),w.attachWs(h)}let L=null;function fe(){L||(L=setInterval(()=>{var a,s,r,i;if(!h||!l||l.audio.paused||l.audio.readyState<2)return;const t=e.np.stationId;if(Number.isFinite(t))try{h.send({type:"sync-pos",stationId:t,masterCT:l.audio.currentTime,atServerNow:w.now?w.now():Date.now(),pdtMs:((r=(s=(a=l.hls)==null?void 0:a.playingDate)==null?void 0:s.getTime)==null?void 0:r.call(s))??null,bufferMs:((i=l.sync)==null?void 0:i.bufferMs)??null})}catch{}},2e3))}function ve(){L&&(clearInterval(L),L=null)}typeof window<"u"&&window.addEventListener("pagehide",ve);function A(t){l&&(t?l.enableSync({clock:w,startedAt:t}):l.disableSync())}function me(t){var a,s;if(!(!t||!t.type)){if(t.type==="clock-pong"){w.handlePong(t);return}switch(t.type){case"hello":{e.room=t.room,e.peers=t.peers||[],(a=t.you)!=null&&a.kind&&t.you.kind!=="display"&&(e.np.error=`This room already has a display (${ye(t.peers)} active). You were joined as ${t.you.kind}.`);const r=t.state;r!=null&&r.station_id&&r.station&&r.station_id!==e.np.stationId&&G(r.station,{silent:!0}),typeof(r==null?void 0:r.volume)=="number"&&l.setVolume(r.volume),A((r==null?void 0:r.started_at)||null),p();return}case"state":{t.started_at?(s=l==null?void 0:l.sync)!=null&&s.enabled?l.updateSyncTarget(t.started_at):A(t.started_at):A(null);return}case"presence":e.peers=t.peers||[],p();return;case"command":he(t);return;case"vote":case"plays":t.stationId===e.np.stationId&&(e.voteStats={...e.voteStats||{},...t.stats||{}},t.type==="plays"&&(e.voteStats.plays=t.plays),p());return;default:return}}}function he(t){switch(t.action){case"play":{const a=Number(t.stationId);if(!Number.isFinite(a)||a===e.np.stationId||a===k)return;k=a;const s=++F;m.get(`/api/stations/${a}`).then(r=>{s===F&&(k=null,G(r))}).catch(()=>{s===F&&(k=null)});return}case"pause":e.np.playing&&l.togglePause();return;case"stop":if(!e.np.stationId)return;l.stop(),_(),e.np.playing=!1,e.np.stationId=null,T(),p();return;case"volume":typeof t.value=="number"&&Math.abs(t.value-e.np.volume)>.001&&l.setVolume(t.value);return;case"setSink":K(String(t.deviceId||""));return;case"setSyncBuffer":Number.isFinite(t.value)&&l&&l.setSyncBufferMs(t.value);return;default:return}}async function G(t,{silent:a}={}){if(!(!t||(a||D(),F++,k=null,_(),e.np.station=t,e.np.stationId=t.id,e.voteStats={up:t.up||0,down:t.down||0,plays:t.plays||0,score:t.score||0},p(),!await pe(t.name,a?"Tap Start to resume the group audio.":"Tap Start to play.")))&&(await l.play(t),l.audio.paused&&l.audio.play().catch(()=>{}),!a))try{const r=await m.post(`/api/stations/${t.id}/play`);e.np.stationId===t.id?(e.session={id:r.sessionId,stationId:t.id,startedAt:Date.now()},e.voteStats={...e.voteStats,...r}):r.sessionId&&m.post(`/api/stations/${t.id}/play/end`,{sessionId:r.sessionId,duration_ms:0}).catch(()=>{})}catch{}}function _({beacon:t=!1}={}){const a=e.session;if(!a||!a.id)return;e.session=null;const s={sessionId:a.id,duration_ms:Math.max(0,Date.now()-a.startedAt)},r=`/api/stations/${a.stationId}/play/end`;if(t&&typeof navigator<"u"&&navigator.sendBeacon)try{navigator.sendBeacon(r,new Blob([JSON.stringify(s)],{type:"application/json"}));return}catch{}m.post(r,s).catch(()=>{})}function T(){h==null||h.send({type:"state",stationId:e.np.stationId,playing:!!e.np.playing,volume:e.np.volume})}let M=null;function ge(){M||(M=setTimeout(()=>{M=null,T()},80))}let O=!1;function Y(){O||(O=!0,requestAnimationFrame(()=>{O=!1,p()}))}let P=0;function V(t){return t instanceof HTMLInputElement&&t.type==="range"}if(typeof window<"u"){document.addEventListener("pointerdown",a=>{V(a.target)&&P++},!0);const t=a=>{V(a.target)&&P>0&&(P--,Y())};document.addEventListener("pointerup",t,!0),document.addEventListener("pointercancel",t,!0)}function R(){h==null||h.send({type:"devices",list:e.devices.list,current:e.devices.current})}async function K(t){if(t){if(c!=null&&c.setOutput)try{await c.setOutput(t)}catch(a){console.warn("[master] setOutput failed",a);return}if(e.devices.current=t,c!=null&&c.isElectron)try{await l.setSinkId(t)}catch(a){console.warn("[master] setSinkId failed",a)}R(),e.showOutputs=!1,p()}}function ye(t){return(t||[]).filter(a=>a.kind==="display").length}function Q(t){return!!t&&e.favorites.some(a=>a.id===t)}async function we(t){if(!t)return;const a=Q(t);try{a?await m.del(`/api/me/favorites/${t}`):await m.put(`/api/me/favorites/${t}`,{position:e.favorites.length}),e.favorites=await m.get("/api/me/favorites"),e.tabUser===e.user.username&&(e.tabFavorites=e.favorites),p()}catch(s){console.warn("[master] toggleFavorite failed",s)}}function be(){const t=new Map;for(const a of e.tabFavorites)for(const s of a.genres||[])t.set(s,(t.get(s)||0)+1);return[...t.entries()].sort((a,s)=>s[1]-a[1]||a[0].localeCompare(s[0])).map(([a,s])=>({genre:a,count:s}))}function Se(){return e.favGenre?e.tabFavorites.filter(t=>(t.genres||[]).includes(e.favGenre)):e.tabFavorites}function Ce(){const t=[],a=new Set;e.mainUser&&(t.push({...e.mainUser,main:!0}),a.add(e.mainUser.username)),e.user&&!a.has(e.user.username)&&(t.push({...e.user,self:!0}),a.add(e.user.username));for(const s of e.users)a.has(s.username)||(t.push(s),a.add(s.username));for(const s of t)s.username===e.user.username&&(s.self=!0);return t}async function Ue(t){if(!(!t||t===e.tabUser)){if(e.tabUser=t,e.favGenre="",t===e.user.username){e.tabFavorites=e.favorites,p();return}e.tabLoading=!0,p();try{e.tabFavorites=await m.get(`/api/users/${encodeURIComponent(t)}/favorites`)}catch(a){console.warn("[master] failed to load favorites for",t,a),e.tabFavorites=[]}finally{e.tabLoading=!1,p()}}}function ke(){return e.tabUser?e.tabUser===e.user.username?!0:e.user.role==="admin"&&e.mainUser&&e.tabUser===e.mainUser.username:!1}async function Me(t){if(!t||t===e.user.username){e.showAvatars=!1,p();return}try{await m.post("/api/auth/switch",{username:t})}catch(a){console.warn("[master] switch failed",a),alert(a.message||"Could not switch user");return}location.reload()}async function Ie(){if(!e.mainUser)return;const t=`u-${e.mainUser.id}`;e.roomSlug=t,history.replaceState(null,"",`?room=${encodeURIComponent(t)}`);try{e.rooms=await m.get("/api/rooms")}catch{}B(),p()}function p(){var v;if(P>0)return;const t=((v=S.querySelector(".favs-grid"))==null?void 0:v.scrollLeft)??0;J(S);const a=e.np,s=a.station,r=(s==null?void 0:s.image_display_url)||(s==null?void 0:s.image_url)||null,i=Q(s==null?void 0:s.id),o=n("div",{class:"master"},n("header",{class:"topbar"},n("h1",{},"◉ MASTER"),n("div",{class:"pill"},n("span",{},"Room:"),n("select",{onChange:u=>{e.roomSlug=u.target.value,history.replaceState(null,"",`?room=${encodeURIComponent(e.roomSlug)}`),B()}},...e.rooms.map(u=>n("option",{value:u.slug,selected:u.slug===e.roomSlug},u.name)))),n("div",{class:"pill peers"},`${e.peers.length} peer${e.peers.length===1?"":"s"}`),n("div",{class:"pill role-pill",title:"This master is the authoritative audio source for the room."},"◉ Broadcasting"),a.error?n("div",{class:"err-banner"},a.error):null,n("div",{class:"grow"}),c!=null&&c.isElectron?n("button",{class:"pill out-btn"+(e.showOutputs?" active":""),title:"Audio output",onClick:()=>{e.showOutputs=!e.showOutputs,p()}},"🔊 ",Ae()):null,Ee(),Pe()),n("section",{class:"stage"},n("div",{class:"np"},n("div",{class:"art"+(r?"":" empty")},r?n("img",{class:"art-img",src:r,alt:"",referrerpolicy:"no-referrer",onError:u=>{const g=u.target.parentNode;u.target.remove(),g&&g.classList.add("empty")}}):null),n("div",{class:"meta"},n("div",{class:"tiny"},a.loading?"Loading…":a.playing?"Now playing":s?"Paused":"Idle"),n("div",{class:"title-row"},n("h2",{},(s==null?void 0:s.name)||"—"),s?n("button",{class:"fav-toggle"+(i?" on":""),title:i?"Remove favorite":"Add favorite",onClick:()=>we(s.id)},i?"★":"☆"):null),n("div",{class:"genres"},...((s==null?void 0:s.genres)||[]).slice(0,6).map(u=>n("span",{class:"tag"},u))),c!=null&&c.isElectron?n("canvas",{class:"np-spectrum","data-spectrum":"1"}):null,e.voteStats?n("div",{class:"stats"},n("span",{},"▲ ",n("b",{},String(e.voteStats.up||0))),n("span",{},"▼ ",n("b",{},String(e.voteStats.down||0))),n("span",{},"▶ ",n("b",{},String(e.voteStats.plays||0)))):null,s!=null&&s.country?n("div",{class:"stats"},n("span",{},s.country)):null,n("div",{class:"transport"},n("button",{class:"ctrl primary",title:"Play / pause",disabled:!s,onClick:()=>{D(),l.togglePause()}},a.playing?"❚❚":"▶"),n("button",{class:"ctrl",title:"Stop",disabled:!s,onClick:()=>{D(),l.stop(),_(),e.np.playing=!1,T(),p()}},"■"),n("div",{class:"vol"},n("span",{class:"vol-icon"},"🔊"),n("input",{type:"range",min:0,max:1,step:.01,value:a.volume,onInput:u=>{var b;const g=Number(u.target.value);l.setVolume(g);const C=(b=u.target.parentNode)==null?void 0:b.querySelector(".val");C&&(C.textContent=Math.round(g*100)+"%")}}),n("span",{class:"val"},Math.round(a.volume*100)+"%"))),n("div",{class:"peer-line"},n("span",{class:"peer-line-label"},"In room:"),...e.peers.length?e.peers.map(u=>{var g;return n("span",{class:"peer role-"+u.kind},n("span",{class:"role-tag"},u.kind),n("span",{},((g=u.user)==null?void 0:g.username)||"?"))}):[n("span",{class:"peer"},"Just you.")]),_e()))),n("section",{class:"stations-bar"},$e()),c!=null&&c.isElectron&&e.showOutputs?Fe():null,e.showAvatars?Te():null);S.appendChild(o);const f=S.querySelector(".favs-grid");f&&(t&&(f.scrollLeft=t),Le(f));const d=S.querySelector("canvas.np-spectrum");d&&l&&de(d,l)}function Le(t){if(t.dataset.dragBound==="1")return;t.dataset.dragBound="1";let a=!1,s=!1,r=0,i=0,o=-1;t.addEventListener("pointerdown",d=>{d.pointerType==="mouse"&&d.button!==0||(a=!0,s=!1,r=d.clientX,i=t.scrollLeft,o=d.pointerId)}),t.addEventListener("pointermove",d=>{if(!a)return;const v=d.clientX-r;if(!s&&Math.abs(v)>5){s=!0;try{t.setPointerCapture(o)}catch{}t.classList.add("dragging")}s&&(t.scrollLeft=i-v,d.preventDefault())});const f=()=>{if(a=!1,s){const d=v=>{v.stopPropagation(),v.preventDefault()};t.addEventListener("click",d,{capture:!0,once:!0}),setTimeout(()=>t.removeEventListener("click",d,!0),0)}s=!1,t.classList.remove("dragging");try{t.releasePointerCapture(o)}catch{}};t.addEventListener("pointerup",f),t.addEventListener("pointercancel",f),t.addEventListener("pointerleave",()=>{a&&!s&&(a=!1)})}function j(t){const a=S.querySelector(".favs-grid");if(!a)return;const s=Math.max(160,Math.round(a.clientWidth*.8));a.scrollBy({left:t*s,behavior:"smooth"})}const H=new Map;function _e(){if(!h)return null;const t=(e.peers||[]).filter(a=>a.kind!=="display");return t.length?n("div",{class:"zones-panel",title:"Per-zone (local) volume for each connected client."},n("div",{class:"zones-label"},"Zones"),...t.map(a=>{var o,f;const s=(o=a.user)==null?void 0:o.id,r=((f=a.user)==null?void 0:f.username)||"?",i=H.get(s)??.7;return n("div",{class:"zone-row"},n("span",{class:"zone-name",title:`${r} (${a.kind})`},r),n("input",{type:"range",min:0,max:1,step:.05,value:i,"aria-label":`Volume for ${r}`,onInput:d=>{var g;const v=Number(d.target.value);H.set(s,v);try{h.send({type:"command",action:"peerVolume",userId:s,value:v})}catch{}const u=(g=d.target.parentNode)==null?void 0:g.querySelector(".zone-val");u&&(u.textContent=Math.round(v*100)+"%")}}),n("span",{class:"zone-val"},Math.round(i*100)+"%"))})):null}function $e(){const t=Ce(),a=ke(),s=be(),r=Se(),i=e.tabUser===e.user.username?"My favorites":e.mainUser&&e.tabUser===e.mainUser.username?`${e.tabUser} (main)`:e.tabUser;return n("div",{class:"card favs-card"},n("div",{class:"fav-tabs"},...t.map(o=>n("button",{class:"fav-tab"+(o.username===e.tabUser?" active":"")+(o.main?" main":"")+(o.self?" self":""),title:o.username+(o.main?" (main / shared)":"")+(o.self?" (you)":""),onClick:()=>Ue(o.username)},n("span",{class:"fav-tab-glyph"},o.main?"★":o.avatar_emoji||"●"),n("span",{class:"fav-tab-name"},o.username)))),n("div",{class:"favs-header"},n("h3",{},i," ",n("span",{class:"fav-count"},`(${r.length}${e.favGenre?`/${e.tabFavorites.length}`:""})`),a?null:n("span",{class:"fav-readonly",title:"Read-only — switch to your own tab to edit"}," · read-only")),s.length?n("select",{class:"genre-filter",title:"Filter by genre",onChange:o=>{e.favGenre=o.target.value,p()}},n("option",{value:""},"All genres"),...s.map(({genre:o,count:f})=>n("option",{value:o,selected:e.favGenre===o},`${o} (${f})`))):null,n("button",{class:"favs-nav",title:"Scroll left",onClick:()=>j(-1)},""),n("button",{class:"favs-nav",title:"Scroll right",onClick:()=>j(1)},"")),n("div",{class:"favs-grid"},...e.tabLoading?[n("div",{class:"favs-empty"},"Loading…")]:r.length?r.map(o=>{const f=o.image_display_url||o.image_url,d=e.np.stationId===o.id;return n("button",{class:"fav-tile"+(d?" active":""),title:o.name,onClick:()=>G(o)},n("div",{class:"fav-art"+(f?"":" empty"),style:f?{backgroundImage:`url("${f}")`}:{}}),n("div",{class:"fav-name"},o.name))}):[n("div",{class:"favs-empty"},e.favGenre?"No favorites in this genre.":a?"No favorites yet. Star a station to add it.":`${e.tabUser} has no favorites yet.`)]))}function Fe(){return n("div",{class:"out-popover-wrap",onClick:t=>{t.target===t.currentTarget&&(e.showOutputs=!1,p())}},n("div",{class:"out-popover card"},n("div",{class:"out-popover-head"},n("h3",{},"Audio output"),n("button",{class:"close",title:"Close",onClick:()=>{e.showOutputs=!1,p()}},"×")),n("div",{class:"device-list"},...e.devices.list.map(t=>n("button",{class:"device"+(t.id===e.devices.current?" active":""),onClick:()=>{K(t.id)}},n("span",{class:"dot"}),n("span",{class:"name"},t.label),n("span",{class:"kind"},t.kind))))))}function Ae(){const t=e.devices.list.find(a=>a.id===e.devices.current);return t?t.label:"—"}function Pe(){const t=!!e.device.trusted,a=(e.device.users||[]).filter(r=>r.username!==e.user.username),s=t&&a.length>0;return n("button",{class:"pill user-pill"+(e.showAvatars?" active":""),title:s?"Switch user":t?"No other users on this device":"This device is not trusted for fast switching",onClick:()=>{s&&(e.showAvatars=!e.showAvatars,p())}},n("span",{class:"avatar",style:e.user.avatar_color?{background:e.user.avatar_color}:{}},e.user.avatar_emoji||e.user.username.slice(0,1).toUpperCase()),n("span",{},e.user.username),s?n("span",{class:"caret"},"▾"):null)}function Ee(){if(!e.mainUser)return null;const t=`u-${e.mainUser.id}`,a=e.roomSlug===t;return e.user.id===e.mainUser.id&&a?null:n("button",{class:"pill follow-pill"+(a?" active":""),title:a?`Following ${e.mainUser.username}'s group`:`Join ${e.mainUser.username}'s group (the house default)`,onClick:()=>{Ie()}},a?`◉ Following ${e.mainUser.username}`:`↗ Follow ${e.mainUser.username}`)}function Te(){const t=e.device.users||[];return n("div",{class:"avatar-popover-wrap",onClick:a=>{a.target===a.currentTarget&&(e.showAvatars=!1,p())}},n("div",{class:"avatar-popover card"},n("div",{class:"avatar-popover-head"},n("h3",{},"Switch user"),n("button",{class:"close",title:"Close",onClick:()=>{e.showAvatars=!1,p()}},"×")),n("div",{class:"avatar-list"},...t.map(a=>n("button",{class:"avatar-row"+(a.username===e.user.username?" active":""),onClick:()=>Me(a.username)},n("span",{class:"avatar lg",style:a.avatar_color?{background:a.avatar_color}:{}},a.avatar_emoji||a.username.slice(0,1).toUpperCase()),n("span",{class:"avatar-name"},a.username,a.is_main?n("span",{class:"avatar-tag"}," ★ main"):null,a.username===e.user.username?n("span",{class:"avatar-tag dim"}," (signed in)"):null)))),n("div",{class:"avatar-hint"},"Add users via the admin panel → Trust device.")))}function Oe(){J(S),S.appendChild(n("div",{class:"login"},n("form",{onSubmit:async t=>{t.preventDefault();const a=new FormData(t.target);try{e.user=await m.post("/api/auth/login",{username:a.get("username"),password:a.get("password")}),await Z()}catch(s){t.target.querySelector(".err").textContent=s.message}}},n("h1",{},"Master sign in"),n("input",{name:"username",placeholder:"Username",required:!0}),n("input",{name:"password",type:"password",placeholder:"Password",required:!0}),n("div",{class:"err"}),n("button",{type:"submit"},"Sign in"))))}Z();